Adding upstream version 0.0~git20250501.71edba4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
c6ff472a6d
commit
c8085bda34
87 changed files with 24009 additions and 0 deletions
15
.build.yml
Normal file
15
.build.yml
Normal file
|
@ -0,0 +1,15 @@
|
|||
image: archlinux
|
||||
packages:
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/go-ap/activitypub
|
||||
environment:
|
||||
GO111MODULE: 'on'
|
||||
tasks:
|
||||
- tests: |
|
||||
cd activitypub
|
||||
make test
|
||||
make TEST_TARGET=./tests test
|
||||
- coverage: |
|
||||
set -a +x
|
||||
cd activitypub && make coverage
|
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Gogland
|
||||
.idea/
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.so
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tools
|
||||
*.out
|
||||
*.coverprofile
|
||||
|
||||
*pkg
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Golang ActitvityPub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
29
Makefile
Normal file
29
Makefile
Normal file
|
@ -0,0 +1,29 @@
|
|||
SHELL := bash
|
||||
.ONESHELL:
|
||||
.SHELLFLAGS := -eu -o pipefail -c
|
||||
.DELETE_ON_ERROR:
|
||||
MAKEFLAGS += --warn-undefined-variables
|
||||
MAKEFLAGS += --no-builtin-rules
|
||||
|
||||
GO ?= go
|
||||
TEST := $(GO) test
|
||||
TEST_FLAGS ?= -v
|
||||
TEST_TARGET ?= .
|
||||
GO111MODULE = on
|
||||
PROJECT_NAME := $(shell basename $(PWD))
|
||||
|
||||
.PHONY: test coverage clean download
|
||||
|
||||
download:
|
||||
$(GO) mod download all
|
||||
$(GO) mod tidy
|
||||
|
||||
test: download
|
||||
$(TEST) $(TEST_FLAGS) $(TEST_TARGET)
|
||||
|
||||
coverage: TEST_TARGET := .
|
||||
coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile
|
||||
coverage: test
|
||||
|
||||
clean:
|
||||
$(RM) -v *.coverprofile
|
80
README.md
Normal file
80
README.md
Normal file
|
@ -0,0 +1,80 @@
|
|||
# About GoActivityPub: Vocabulary
|
||||
|
||||
[](https://raw.githubusercontent.com/go-ap/activitypub/master/LICENSE)
|
||||
[](https://builds.sr.ht/~mariusor/activitypub)
|
||||
[](https://codecov.io/gh/go-ap/activitypub)
|
||||
[](https://goreportcard.com/report/github.com/go-ap/activitypub)
|
||||
|
||||
This project is part of the [GoActivityPub](https://github.com/go-ap) library which helps with creating ActivityPub applications using the Go programming language.
|
||||
|
||||
It contains data types for most of the [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) and the [ActivityPub](https://www.w3.org/TR/activitypub/) extension.
|
||||
They are documented accordingly with annotations from these specifications.
|
||||
|
||||
You can find an expanded documentation about the whole library [on SourceHut](https://man.sr.ht/~mariusor/go-activitypub/go-ap/index.md).
|
||||
|
||||
For discussions about the projects you can write to the discussions mailing list: [~mariusor/go-activitypub-discuss@lists.sr.ht](mailto:~mariusor/go-activitypub-discuss@lists.sr.ht)
|
||||
|
||||
For patches and bug reports please use the dev mailing list: [~mariusor/go-activitypub-dev@lists.sr.ht](mailto:~mariusor/go-activitypub-dev@lists.sr.ht)
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import vocab "github.com/go-ap/activitypub"
|
||||
|
||||
follow := vocab.Activity{
|
||||
Type: vocab.FollowType,
|
||||
Actor: vocab.IRI("https://example.com/alice"),
|
||||
Object: vocab.IRI("https://example.com/janedoe"),
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Note about generics
|
||||
|
||||
The module contains helper functions which make it simpler to deal with the `vocab.Item`
|
||||
interfaces and they come in two flavours: explicit `OnXXX` and `ToXXX` functions corresponding
|
||||
to each type and, a generic pair of functions `On[T]` and `To[T]`.
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
vocab "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
var it vocab.Item = ... // an ActivityPub object unmarshaled from a request
|
||||
|
||||
err := vocab.OnActivity(it, func(act *vocab.Activity) error {
|
||||
if vocab.ContentManagementActivityTypes.Contains(act.Type) {
|
||||
fmt.Printf("This is a Content Management type activity: %q", act.Type)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
err := vocab.On[vocab.Activity](it, func(act *vocab.Activity) error {
|
||||
if vocab.ReactionsActivityTypes.Contains(act.Type) {
|
||||
fmt.Printf("This is a Reaction type activity: %q", act.Type)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
Before using the generic versions you should consider that they come with a pretty heavy performance penalty:
|
||||
|
||||
```
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/go-ap/activitypub
|
||||
cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
|
||||
Benchmark_OnT_vs_On_T/OnObject-8 752387791 1.633 ns/op
|
||||
Benchmark_OnT_vs_On_T/On_T_Object-8 4656264 261.8 ns/op
|
||||
Benchmark_OnT_vs_On_T/OnActor-8 739833261 1.596 ns/op
|
||||
Benchmark_OnT_vs_On_T/On_T_Actor-8 4035148 301.9 ns/op
|
||||
Benchmark_OnT_vs_On_T/OnActivity-8 751173854 1.604 ns/op
|
||||
Benchmark_OnT_vs_On_T/On_T_Activity-8 4062598 285.9 ns/op
|
||||
Benchmark_OnT_vs_On_T/OnIntransitiveActivity-8 675824500 1.640 ns/op
|
||||
Benchmark_OnT_vs_On_T/On_T_IntransitiveActivity-8 4372798 274.1 ns/op
|
||||
PASS
|
||||
ok github.com/go-ap/activitypub 11.350s
|
||||
```
|
948
activity.go
Normal file
948
activity.go
Normal file
|
@ -0,0 +1,948 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Activity Types
|
||||
const (
|
||||
AcceptType ActivityVocabularyType = "Accept"
|
||||
AddType ActivityVocabularyType = "Add"
|
||||
AnnounceType ActivityVocabularyType = "Announce"
|
||||
ArriveType ActivityVocabularyType = "Arrive"
|
||||
BlockType ActivityVocabularyType = "Block"
|
||||
CreateType ActivityVocabularyType = "Create"
|
||||
DeleteType ActivityVocabularyType = "Delete"
|
||||
DislikeType ActivityVocabularyType = "Dislike"
|
||||
FlagType ActivityVocabularyType = "Flag"
|
||||
FollowType ActivityVocabularyType = "Follow"
|
||||
IgnoreType ActivityVocabularyType = "Ignore"
|
||||
InviteType ActivityVocabularyType = "Invite"
|
||||
JoinType ActivityVocabularyType = "Join"
|
||||
LeaveType ActivityVocabularyType = "Leave"
|
||||
LikeType ActivityVocabularyType = "Like"
|
||||
ListenType ActivityVocabularyType = "Listen"
|
||||
MoveType ActivityVocabularyType = "Move"
|
||||
OfferType ActivityVocabularyType = "Offer"
|
||||
QuestionType ActivityVocabularyType = "Question"
|
||||
RejectType ActivityVocabularyType = "Reject"
|
||||
ReadType ActivityVocabularyType = "Read"
|
||||
RemoveType ActivityVocabularyType = "Remove"
|
||||
TentativeRejectType ActivityVocabularyType = "TentativeReject"
|
||||
TentativeAcceptType ActivityVocabularyType = "TentativeAccept"
|
||||
TravelType ActivityVocabularyType = "Travel"
|
||||
UndoType ActivityVocabularyType = "Undo"
|
||||
UpdateType ActivityVocabularyType = "Update"
|
||||
ViewType ActivityVocabularyType = "View"
|
||||
)
|
||||
|
||||
func (a ActivityVocabularyTypes) Contains(typ ActivityVocabularyType) bool {
|
||||
for _, v := range a {
|
||||
if strings.EqualFold(string(v), string(typ)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ContentManagementActivityTypes use case primarily deals with activities that involve the creation, modification or
|
||||
// deletion of content.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-crud
|
||||
//
|
||||
// This includes, for instance, activities such as "John created a new note", "Sally updated an article", and
|
||||
// "Joe deleted the photo".
|
||||
var ContentManagementActivityTypes = ActivityVocabularyTypes{
|
||||
CreateType,
|
||||
DeleteType,
|
||||
UpdateType,
|
||||
}
|
||||
|
||||
// CollectionManagementActivityTypes use case primarily deals with activities involving the management of content within
|
||||
// collections.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-collection
|
||||
//
|
||||
// Examples of collections include things like folders, albums, friend lists, etc.
|
||||
// This includes, for instance, activities such as "Sally added a file to Folder A", "John moved the file from Folder A
|
||||
// to Folder B", etc.
|
||||
var CollectionManagementActivityTypes = ActivityVocabularyTypes{
|
||||
AddType,
|
||||
MoveType,
|
||||
RemoveType,
|
||||
}
|
||||
|
||||
// ReactionsActivityTypes use case primarily deals with reactions to content.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-reactions
|
||||
//
|
||||
// This can include activities such as liking or disliking content, ignoring updates, flagging content as being
|
||||
// inappropriate, accepting or rejecting objects, etc.
|
||||
var ReactionsActivityTypes = ActivityVocabularyTypes{
|
||||
AcceptType,
|
||||
BlockType,
|
||||
DislikeType,
|
||||
FlagType,
|
||||
IgnoreType,
|
||||
LikeType,
|
||||
RejectType,
|
||||
TentativeAcceptType,
|
||||
TentativeRejectType,
|
||||
}
|
||||
|
||||
// EventRSVPActivityTypes use case primarily deals with invitations to events and RSVP type responses.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-rsvp
|
||||
var EventRSVPActivityTypes = ActivityVocabularyTypes{
|
||||
AcceptType,
|
||||
IgnoreType,
|
||||
InviteType,
|
||||
RejectType,
|
||||
TentativeAcceptType,
|
||||
TentativeRejectType,
|
||||
}
|
||||
|
||||
// GroupManagementActivityTypes use case primarily deals with management of groups.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-group
|
||||
//
|
||||
// It can include, for instance, activities such as "John added Sally to Group A", "Sally joined Group A",
|
||||
// "Joe left Group A", etc.
|
||||
var GroupManagementActivityTypes = ActivityVocabularyTypes{
|
||||
AddType,
|
||||
JoinType,
|
||||
LeaveType,
|
||||
RemoveType,
|
||||
}
|
||||
|
||||
// ContentExperienceActivityTypes use case primarily deals with describing activities involving listening to, reading,
|
||||
// or viewing content.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-experience
|
||||
//
|
||||
// For instance, "Sally read the article", "Joe listened to the song".
|
||||
var ContentExperienceActivityTypes = ActivityVocabularyTypes{
|
||||
ListenType,
|
||||
ReadType,
|
||||
ViewType,
|
||||
}
|
||||
|
||||
// GeoSocialEventsActivityTypes use case primarily deals with activities involving geo-tagging type activities.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-geo
|
||||
//
|
||||
// For instance, it can include activities such as "Joe arrived at work", "Sally left work", and
|
||||
// "John is travel from home to work".
|
||||
var GeoSocialEventsActivityTypes = ActivityVocabularyTypes{
|
||||
ArriveType,
|
||||
LeaveType,
|
||||
TravelType,
|
||||
}
|
||||
|
||||
// NotificationActivityTypes use case primarily deals with calling attention to particular objects or notifications.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-notification
|
||||
var NotificationActivityTypes = ActivityVocabularyTypes{
|
||||
AnnounceType,
|
||||
}
|
||||
|
||||
// QuestionActivityTypes use case primarily deals with representing inquiries of any type.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-questions
|
||||
//
|
||||
// See 5.4 Representing Questions for more information.
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#questions
|
||||
var QuestionActivityTypes = ActivityVocabularyTypes{
|
||||
QuestionType,
|
||||
}
|
||||
|
||||
// RelationshipManagementActivityTypes use case primarily deals with representing activities involving the management of
|
||||
// interpersonal and social relationships
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-relationships
|
||||
//
|
||||
// (e.g. friend requests, management of social network, etc). See 5.2 Representing Relationships Between Entities
|
||||
// for more information.
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#connections
|
||||
var RelationshipManagementActivityTypes = ActivityVocabularyTypes{
|
||||
AcceptType,
|
||||
AddType,
|
||||
BlockType,
|
||||
CreateType,
|
||||
DeleteType,
|
||||
FollowType,
|
||||
IgnoreType,
|
||||
InviteType,
|
||||
RejectType,
|
||||
}
|
||||
|
||||
// NegatingActivityTypes use case primarily deals with the ability to redact previously completed activities.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-undo
|
||||
//
|
||||
// See 5.5 Inverse Activities and "Undo" for more information.
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#inverse
|
||||
var NegatingActivityTypes = ActivityVocabularyTypes{
|
||||
UndoType,
|
||||
}
|
||||
|
||||
// OffersActivityTypes use case deals with activities involving offering one object to another.
|
||||
//
|
||||
// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-offers
|
||||
//
|
||||
// It can include, for instance, activities such as "Company A is offering a discount on purchase of Product Z to Sally",
|
||||
// "Sally is offering to add a File to Folder A", etc.
|
||||
var OffersActivityTypes = ActivityVocabularyTypes{
|
||||
OfferType,
|
||||
}
|
||||
|
||||
var IntransitiveActivityTypes = ActivityVocabularyTypes{
|
||||
ArriveType,
|
||||
TravelType,
|
||||
QuestionType,
|
||||
}
|
||||
|
||||
var ActivityTypes = ActivityVocabularyTypes{
|
||||
AcceptType,
|
||||
AddType,
|
||||
AnnounceType,
|
||||
BlockType,
|
||||
CreateType,
|
||||
DeleteType,
|
||||
DislikeType,
|
||||
FlagType,
|
||||
FollowType,
|
||||
IgnoreType,
|
||||
InviteType,
|
||||
JoinType,
|
||||
LeaveType,
|
||||
LikeType,
|
||||
ListenType,
|
||||
MoveType,
|
||||
OfferType,
|
||||
RejectType,
|
||||
ReadType,
|
||||
RemoveType,
|
||||
TentativeRejectType,
|
||||
TentativeAcceptType,
|
||||
UndoType,
|
||||
UpdateType,
|
||||
ViewType,
|
||||
}
|
||||
|
||||
// HasRecipients is an interface implemented by activities to return their audience
|
||||
// for further propagation
|
||||
//
|
||||
// Please take care to the fact that the de-duplication functionality requires a pointer receiver
|
||||
// therefore a valid Item interface that wraps around an Object struct, can not be type asserted
|
||||
// to HasRecipients.
|
||||
type HasRecipients interface {
|
||||
// Recipients is a method that should do a recipients de-duplication step and then return
|
||||
// the remaining recipients.
|
||||
Recipients() ItemCollection
|
||||
// Clean is a method that removes BCC/Bto recipients in preparation for public consumption of
|
||||
// the Object.
|
||||
Clean()
|
||||
}
|
||||
|
||||
type Activities interface {
|
||||
Activity
|
||||
}
|
||||
|
||||
// Activity is a subtype of Object that describes some form of action that may happen,
|
||||
// is currently happening, or has already happened.
|
||||
// The Activity type itself serves as an abstract base type for all types of activities.
|
||||
// It is important to note that the Activity type itself does not carry any specific semantics
|
||||
// about the kind of action being taken.
|
||||
type Activity struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
|
||||
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
|
||||
Actor Item `jsonld:"actor,omitempty"`
|
||||
// Target describes the indirect object, or target, of the activity.
|
||||
// The precise meaning of the target is largely dependent on the type of action being described
|
||||
// but will often be the object of the English preposition "to".
|
||||
// For instance, in the activity "John added a movie to his wishlist",
|
||||
// the target of the activity is John's wishlist. An activity can have more than one target.
|
||||
Target Item `jsonld:"target,omitempty"`
|
||||
// Result describes the result of the activity. For instance, if a particular action results in the creation
|
||||
// of a new resource, the result property can be used to describe that new resource.
|
||||
Result Item `jsonld:"result,omitempty"`
|
||||
// Origin describes an indirect object of the activity from which the activity is directed.
|
||||
// The precise meaning of the origin is the object of the English preposition "from".
|
||||
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
|
||||
Origin Item `jsonld:"origin,omitempty"`
|
||||
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
|
||||
Instrument Item `jsonld:"instrument,omitempty"`
|
||||
// Object When used within an Activity, describes the direct object of the activity.
|
||||
// For instance, in the activity "John added a movie to his wishlist",
|
||||
// the object of the activity is the movie added.
|
||||
// When used within a Relationship describes the entity to which the subject is related.
|
||||
Object Item `jsonld:"object,omitempty"`
|
||||
}
|
||||
|
||||
// GetType returns the ActivityVocabulary type of the current Activity
|
||||
func (a Activity) GetType() ActivityVocabularyType {
|
||||
return a.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for Activity objects
|
||||
func (a Activity) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the Activity object
|
||||
func (a Activity) GetID() ID {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the Activity object
|
||||
func (a Activity) GetLink() IRI {
|
||||
return IRI(a.ID)
|
||||
}
|
||||
|
||||
// IsObject returns true for Activity objects
|
||||
func (a Activity) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Activity objects
|
||||
func (a Activity) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func removeFromCollection(col ItemCollection, items ...Item) ItemCollection {
|
||||
result := make(ItemCollection, 0)
|
||||
if len(items) == 0 {
|
||||
return col
|
||||
}
|
||||
for _, ob := range col {
|
||||
found := false
|
||||
for _, it := range items {
|
||||
if ob.GetID().Equals(it.GetID(), false) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
result = append(result, ob)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func removeFromAudience(a *Activity, items ...Item) error {
|
||||
if a.To != nil {
|
||||
a.To = removeFromCollection(a.To, items...)
|
||||
}
|
||||
if a.Bto != nil {
|
||||
a.Bto = removeFromCollection(a.Bto, items...)
|
||||
}
|
||||
if a.CC != nil {
|
||||
a.CC = removeFromCollection(a.CC, items...)
|
||||
}
|
||||
if a.BCC != nil {
|
||||
a.BCC = removeFromCollection(a.BCC, items...)
|
||||
}
|
||||
if a.Audience != nil {
|
||||
a.Audience = removeFromCollection(a.Audience, items...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Activity's To, Bto, CC and BCC properties
|
||||
func (a *Activity) Recipients() ItemCollection {
|
||||
var alwaysRemove ItemCollection
|
||||
if a.GetType() == BlockType && a.Object != nil {
|
||||
alwaysRemove = append(alwaysRemove, a.Object)
|
||||
}
|
||||
if len(alwaysRemove) > 0 {
|
||||
_ = removeFromAudience(a, alwaysRemove...)
|
||||
}
|
||||
aud := a.Audience
|
||||
return ItemCollectionDeduplication(&a.To, &a.CC, &a.Bto, &a.BCC, &aud)
|
||||
}
|
||||
|
||||
// CleanRecipients checks if the "it" Item has recipients and cleans them if it does
|
||||
func CleanRecipients(it Item) Item {
|
||||
if IsNil(it) {
|
||||
return nil
|
||||
}
|
||||
if s, ok := it.(HasRecipients); ok {
|
||||
s.Clean()
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (a *Activity) Clean() {
|
||||
_ = OnObject(a, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
CleanRecipients(a.Object)
|
||||
CleanRecipients(a.Actor)
|
||||
CleanRecipients(a.Target)
|
||||
}
|
||||
|
||||
type (
|
||||
// Accept indicates that the actor accepts the object. The target property can be used in certain circumstances to indicate
|
||||
// the context into which the object has been accepted.
|
||||
Accept = Activity
|
||||
|
||||
// Add indicates that the actor has added the object to the target. If the target property is not explicitly specified,
|
||||
// the target would need to be determined implicitly by context.
|
||||
// The origin can be used to identify the context from which the object originated.
|
||||
Add = Activity
|
||||
|
||||
// Announce indicates that the actor is calling the target's attention the object.
|
||||
// The origin typically has no defined meaning.
|
||||
Announce = Activity
|
||||
|
||||
// Block indicates that the actor is blocking the object. Blocking is a stronger form of Ignore.
|
||||
// The typical use is to support social systems that allow one user to block activities or content of other users.
|
||||
// The target and origin typically have no defined meaning.
|
||||
Block = Ignore
|
||||
|
||||
// Create indicates that the actor has created the object.
|
||||
Create = Activity
|
||||
|
||||
// Delete indicates that the actor has deleted the object.
|
||||
// If specified, the origin indicates the context from which the object was deleted.
|
||||
Delete = Activity
|
||||
|
||||
// Dislike indicates that the actor dislikes the object.
|
||||
Dislike = Activity
|
||||
|
||||
// Flag indicates that the actor is "flagging" the object.
|
||||
// Flagging is defined in the sense common to many social platforms as reporting content as being
|
||||
// inappropriate for any number of reasons.
|
||||
Flag = Activity
|
||||
|
||||
// Follow indicates that the actor is "following" the object. Following is defined in the sense typically used within
|
||||
// Social systems in which the actor is interested in any activity performed by or on the object.
|
||||
// The target and origin typically have no defined meaning.
|
||||
Follow = Activity
|
||||
|
||||
// Ignore indicates that the actor is ignoring the object. The target and origin typically have no defined meaning.
|
||||
Ignore = Activity
|
||||
|
||||
// Invite is a specialization of Offer in which the actor is extending an invitation for the object to the target.
|
||||
Invite = Offer
|
||||
|
||||
// Join indicates that the actor has joined the object. The target and origin typically have no defined meaning.
|
||||
Join = Activity
|
||||
|
||||
// Leave indicates that the actor has left the object. The target and origin typically have no meaning.
|
||||
Leave = Activity
|
||||
|
||||
// Like indicates that the actor likes, recommends or endorses the object.
|
||||
// The target and origin typically have no defined meaning.
|
||||
Like = Activity
|
||||
|
||||
// Listen inherits all properties from Activity.
|
||||
Listen = Activity
|
||||
|
||||
// Move indicates that the actor has moved object from origin to target.
|
||||
// If the origin or target are not specified, either can be determined by context.
|
||||
Move = Activity
|
||||
|
||||
// Offer indicates that the actor is offering the object.
|
||||
// If specified, the target indicates the entity to which the object is being offered.
|
||||
Offer = Activity
|
||||
|
||||
// Reject indicates that the actor is rejecting the object. The target and origin typically have no defined meaning.
|
||||
Reject = Activity
|
||||
|
||||
// Read indicates that the actor has read the object.
|
||||
Read = Activity
|
||||
|
||||
// Remove indicates that the actor is removing the object. If specified,
|
||||
// the origin indicates the context from which the object is being removed.
|
||||
Remove = Activity
|
||||
|
||||
// TentativeReject is a specialization of Reject in which the rejection is considered tentative.
|
||||
TentativeReject = Reject
|
||||
|
||||
// TentativeAccept is a specialization of Accept indicating that the acceptance is tentative.
|
||||
TentativeAccept = Accept
|
||||
|
||||
// Undo indicates that the actor is undoing the object. In most cases, the object will be an Activity describing
|
||||
// some previously performed action (for instance, a person may have previously "liked" an article but,
|
||||
// for whatever reason, might choose to undo that like at some later point in time).
|
||||
// The target and origin typically have no defined meaning.
|
||||
Undo = Activity
|
||||
|
||||
// Update indicates that the actor has updated the object. Note, however, that this vocabulary does not define a mechanism
|
||||
// for describing the actual set of modifications made to object.
|
||||
// The target and origin typically have no defined meaning.
|
||||
Update = Activity
|
||||
|
||||
// View indicates that the actor has viewed the object.
|
||||
View = Activity
|
||||
)
|
||||
|
||||
// AcceptNew initializes an Accept activity
|
||||
func AcceptNew(id ID, ob Item) *Accept {
|
||||
a := ActivityNew(id, AcceptType, ob)
|
||||
o := Accept(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// AddNew initializes an Add activity
|
||||
func AddNew(id ID, ob, trgt Item) *Add {
|
||||
a := ActivityNew(id, AddType, ob)
|
||||
o := Add(*a)
|
||||
o.Target = trgt
|
||||
return &o
|
||||
}
|
||||
|
||||
// AnnounceNew initializes an Announce activity
|
||||
func AnnounceNew(id ID, ob Item) *Announce {
|
||||
a := ActivityNew(id, AnnounceType, ob)
|
||||
o := Announce(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// BlockNew initializes a Block activity
|
||||
func BlockNew(id ID, ob Item) *Block {
|
||||
a := ActivityNew(id, BlockType, ob)
|
||||
o := Block(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// CreateNew initializes a Create activity
|
||||
func CreateNew(id ID, ob Item) *Create {
|
||||
a := ActivityNew(id, CreateType, ob)
|
||||
o := Create(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// DeleteNew initializes a Delete activity
|
||||
func DeleteNew(id ID, ob Item) *Delete {
|
||||
a := ActivityNew(id, DeleteType, ob)
|
||||
o := Delete(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// DislikeNew initializes a Dislike activity
|
||||
func DislikeNew(id ID, ob Item) *Dislike {
|
||||
a := ActivityNew(id, DislikeType, ob)
|
||||
o := Dislike(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// FlagNew initializes a Flag activity
|
||||
func FlagNew(id ID, ob Item) *Flag {
|
||||
a := ActivityNew(id, FlagType, ob)
|
||||
o := Flag(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// FollowNew initializes a Follow activity
|
||||
func FollowNew(id ID, ob Item) *Follow {
|
||||
a := ActivityNew(id, FollowType, ob)
|
||||
o := Follow(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// IgnoreNew initializes an Ignore activity
|
||||
func IgnoreNew(id ID, ob Item) *Ignore {
|
||||
a := ActivityNew(id, IgnoreType, ob)
|
||||
o := Ignore(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// InviteNew initializes an Invite activity
|
||||
func InviteNew(id ID, ob Item) *Invite {
|
||||
a := ActivityNew(id, InviteType, ob)
|
||||
o := Invite(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// JoinNew initializes a Join activity
|
||||
func JoinNew(id ID, ob Item) *Join {
|
||||
a := ActivityNew(id, JoinType, ob)
|
||||
o := Join(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// LeaveNew initializes a Leave activity
|
||||
func LeaveNew(id ID, ob Item) *Leave {
|
||||
a := ActivityNew(id, LeaveType, ob)
|
||||
o := Leave(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// LikeNew initializes a Like activity
|
||||
func LikeNew(id ID, ob Item) *Like {
|
||||
a := ActivityNew(id, LikeType, ob)
|
||||
o := Like(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// ListenNew initializes a Listen activity
|
||||
func ListenNew(id ID, ob Item) *Listen {
|
||||
a := ActivityNew(id, ListenType, ob)
|
||||
o := Listen(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// MoveNew initializes a Move activity
|
||||
func MoveNew(id ID, ob Item) *Move {
|
||||
a := ActivityNew(id, MoveType, ob)
|
||||
o := Move(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// OfferNew initializes an Offer activity
|
||||
func OfferNew(id ID, ob Item) *Offer {
|
||||
a := ActivityNew(id, OfferType, ob)
|
||||
o := Offer(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// RejectNew initializes a Reject activity
|
||||
func RejectNew(id ID, ob Item) *Reject {
|
||||
a := ActivityNew(id, RejectType, ob)
|
||||
o := Reject(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// ReadNew initializes a Read activity
|
||||
func ReadNew(id ID, ob Item) *Read {
|
||||
a := ActivityNew(id, ReadType, ob)
|
||||
o := Read(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// RemoveNew initializes a Remove activity
|
||||
func RemoveNew(id ID, ob, trgt Item) *Remove {
|
||||
a := ActivityNew(id, RemoveType, ob)
|
||||
o := Remove(*a)
|
||||
o.Target = trgt
|
||||
return &o
|
||||
}
|
||||
|
||||
// TentativeRejectNew initializes a TentativeReject activity
|
||||
func TentativeRejectNew(id ID, ob Item) *TentativeReject {
|
||||
a := ActivityNew(id, TentativeRejectType, ob)
|
||||
o := TentativeReject(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// TentativeAcceptNew initializes a TentativeAccept activity
|
||||
func TentativeAcceptNew(id ID, ob Item) *TentativeAccept {
|
||||
a := ActivityNew(id, TentativeAcceptType, ob)
|
||||
o := TentativeAccept(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// UndoNew initializes an Undo activity
|
||||
func UndoNew(id ID, ob Item) *Undo {
|
||||
a := ActivityNew(id, UndoType, ob)
|
||||
o := Undo(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// UpdateNew initializes an Update activity
|
||||
func UpdateNew(id ID, ob Item) *Update {
|
||||
a := ActivityNew(id, UpdateType, ob)
|
||||
u := Update(*a)
|
||||
return &u
|
||||
}
|
||||
|
||||
// ViewNew initializes a View activity
|
||||
func ViewNew(id ID, ob Item) *View {
|
||||
a := ActivityNew(id, ViewType, ob)
|
||||
o := View(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// ActivityNew initializes a basic activity
|
||||
func ActivityNew(id ID, typ ActivityVocabularyType, ob Item) *Activity {
|
||||
if !ActivityTypes.Contains(typ) {
|
||||
typ = ActivityType
|
||||
}
|
||||
a := Activity{ID: id, Type: typ}
|
||||
a.Name = NaturalLanguageValuesNew()
|
||||
a.Content = NaturalLanguageValuesNew()
|
||||
|
||||
a.Object = ob
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (a *Activity) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadActivity(val, a)
|
||||
}
|
||||
|
||||
func fmtActivityProps(w io.Writer) func(*Activity) error {
|
||||
return func(a *Activity) error {
|
||||
if !IsNil(a.Object) {
|
||||
_, _ = fmt.Fprintf(w, " object: %s", a.Object)
|
||||
}
|
||||
return OnIntransitiveActivity(a, fmtIntransitiveActivityProps(w))
|
||||
}
|
||||
}
|
||||
|
||||
func (a Activity) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
if a.Type != "" && a.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T[%s]( %s )", a, a.Type, a.ID)
|
||||
} else if a.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T( %s )", a, a.ID)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%T[%p]", a, &a)
|
||||
}
|
||||
case 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] {", a, a.Type)
|
||||
fmtActivityProps(s)(&a)
|
||||
_, _ = io.WriteString(s, " }")
|
||||
}
|
||||
}
|
||||
|
||||
// ToActivity
|
||||
func ToActivity(it Item) (*Activity, error) {
|
||||
switch i := it.(type) {
|
||||
case *Activity:
|
||||
return i, nil
|
||||
case Activity:
|
||||
return &i, nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Activity))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Activity); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Activity](it)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (a Activity) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if !JSONWriteActivityValue(&b, a) {
|
||||
return nil, nil
|
||||
}
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (a *Activity) UnmarshalBinary(data []byte) error {
|
||||
return a.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (a Activity) MarshalBinary() ([]byte, error) {
|
||||
return a.GobEncode()
|
||||
}
|
||||
|
||||
func mapIntransitiveActivityProperties(mm map[string][]byte, a *IntransitiveActivity) (hasData bool, err error) {
|
||||
err = OnObject(a, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if a.Actor != nil {
|
||||
if mm["actor"], err = gobEncodeItem(a.Actor); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Target != nil {
|
||||
if mm["target"], err = gobEncodeItem(a.Target); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Result != nil {
|
||||
if mm["result"], err = gobEncodeItem(a.Result); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Instrument != nil {
|
||||
if mm["instrument"], err = gobEncodeItem(a.Instrument); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return hasData, err
|
||||
}
|
||||
|
||||
func mapActivityProperties(mm map[string][]byte, a *Activity) (hasData bool, err error) {
|
||||
err = OnIntransitiveActivity(a, func(a *IntransitiveActivity) error {
|
||||
hasData, err = mapIntransitiveActivityProperties(mm, a)
|
||||
return err
|
||||
})
|
||||
if a.Object != nil {
|
||||
if mm["object"], err = gobEncodeItem(a.Object); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return hasData, err
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (a Activity) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapActivityProperties(mm, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (a *Activity) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapActivityProperties(mm, a)
|
||||
}
|
||||
|
||||
// Equals verifies if our receiver Object is equals with the "with" Object
|
||||
func (a Activity) Equals(with Item) bool {
|
||||
result := true
|
||||
err := OnActivity(with, func(w *Activity) error {
|
||||
_ = OnIntransitiveActivity(a, func(oi *IntransitiveActivity) error {
|
||||
result = oi.Equals(w)
|
||||
return nil
|
||||
})
|
||||
if w.Object != nil {
|
||||
if !ItemsEqual(a.Object, w.Object) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
1711
activity_test.go
Normal file
1711
activity_test.go
Normal file
File diff suppressed because it is too large
Load diff
615
actor.go
Normal file
615
actor.go
Normal file
|
@ -0,0 +1,615 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// CanReceiveActivities Types
|
||||
const (
|
||||
ApplicationType ActivityVocabularyType = "Application"
|
||||
GroupType ActivityVocabularyType = "Group"
|
||||
OrganizationType ActivityVocabularyType = "Organization"
|
||||
PersonType ActivityVocabularyType = "Person"
|
||||
ServiceType ActivityVocabularyType = "Service"
|
||||
)
|
||||
|
||||
// ActorTypes represent the valid Actor types.
|
||||
var ActorTypes = ActivityVocabularyTypes{
|
||||
ApplicationType,
|
||||
GroupType,
|
||||
OrganizationType,
|
||||
PersonType,
|
||||
ServiceType,
|
||||
}
|
||||
|
||||
// CanReceiveActivities is generally one of the ActivityStreams Actor Types, but they don't have to be.
|
||||
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
|
||||
// Actors are retrieved like any other Object in ActivityPub.
|
||||
// Like other ActivityStreams objects, actors have an id, which is a URI.
|
||||
type CanReceiveActivities Item
|
||||
|
||||
type Actors interface {
|
||||
Actor
|
||||
}
|
||||
|
||||
// Actor is generally one of the ActivityStreams actor Types, but they don't have to be.
|
||||
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
|
||||
// Actors are retrieved like any other Object in ActivityPub.
|
||||
// Like other ActivityStreams objects, actors have an id, which is a URI.
|
||||
type Actor struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor;
|
||||
// see 5.2 Inbox.
|
||||
Inbox Item `jsonld:"inbox,omitempty"`
|
||||
// An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor;
|
||||
// see 5.1 outbox.
|
||||
Outbox Item `jsonld:"outbox,omitempty"`
|
||||
// A link to an [ActivityStreams] collection of the actors that this actor is following;
|
||||
// see 5.4 Following Collection
|
||||
Following Item `jsonld:"following,omitempty"`
|
||||
// A link to an [ActivityStreams] collection of the actors that follow this actor;
|
||||
// see 5.3 Followers Collection.
|
||||
Followers Item `jsonld:"followers,omitempty"`
|
||||
// A link to an [ActivityStreams] collection of objects this actor has liked;
|
||||
// see 5.5 Liked Collection.
|
||||
Liked Item `jsonld:"liked,omitempty"`
|
||||
// A short username which may be used to refer to the actor, with no uniqueness guarantees.
|
||||
PreferredUsername NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"`
|
||||
// A json object which maps additional (typically server/domain-wide) endpoints which may be useful either
|
||||
// for this actor or someone referencing this actor.
|
||||
// This mapping may be nested inside the actor document as the value or may be a link
|
||||
// to a JSON-LD document with these properties.
|
||||
Endpoints *Endpoints `jsonld:"endpoints,omitempty"`
|
||||
// A list of supplementary Collections which may be of interest.
|
||||
Streams ItemCollection `jsonld:"streams,omitempty"`
|
||||
PublicKey PublicKey `jsonld:"publicKey,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Actor
|
||||
func (a Actor) GetID() ID {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Actor
|
||||
func (a Actor) GetLink() IRI {
|
||||
return IRI(a.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Actor
|
||||
func (a Actor) GetType() ActivityVocabularyType {
|
||||
return a.Type
|
||||
}
|
||||
|
||||
// IsLink validates if currentActivity Pub Actor is a Link
|
||||
func (a Actor) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject validates if currentActivity Pub Actor is an Object
|
||||
func (a Actor) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Actor Objects
|
||||
func (a Actor) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PublicKey holds the ActivityPub compatible public key data
|
||||
// The document reference can be found at:
|
||||
// https://w3c-ccg.github.io/security-vocab/#publicKey
|
||||
type PublicKey struct {
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
Owner IRI `jsonld:"owner,omitempty"`
|
||||
PublicKeyPem string `jsonld:"publicKeyPem,omitempty"`
|
||||
}
|
||||
|
||||
func (p *PublicKey) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return JSONLoadPublicKey(val, p)
|
||||
}
|
||||
|
||||
func (p PublicKey) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := true
|
||||
JSONWrite(&b, '{')
|
||||
if v, err := p.ID.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = !JSONWriteProp(&b, "id", v)
|
||||
}
|
||||
if len(p.Owner) > 0 {
|
||||
notEmpty = JSONWriteIRIProp(&b, "owner", p.Owner) || notEmpty
|
||||
}
|
||||
if len(p.PublicKeyPem) > 0 {
|
||||
if pem, err := json.Marshal(p.PublicKeyPem); err == nil {
|
||||
notEmpty = JSONWriteProp(&b, "publicKeyPem", pem) || notEmpty
|
||||
}
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (a *Actor) UnmarshalBinary(data []byte) error {
|
||||
return a.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (a Actor) MarshalBinary() ([]byte, error) {
|
||||
return a.GobEncode()
|
||||
}
|
||||
|
||||
func (a Actor) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapActorProperties(mm, &a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (a *Actor) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapActorProperties(mm, a)
|
||||
}
|
||||
|
||||
type (
|
||||
// Application describes a software application.
|
||||
Application = Actor
|
||||
|
||||
// Group represents a formal or informal collective of Actors.
|
||||
Group = Actor
|
||||
|
||||
// Organization represents an organization.
|
||||
Organization = Actor
|
||||
|
||||
// Person represents an individual person.
|
||||
Person = Actor
|
||||
|
||||
// Service represents a service of any kind.
|
||||
Service = Actor
|
||||
)
|
||||
|
||||
// ActorNew initializes an CanReceiveActivities type actor
|
||||
func ActorNew(id ID, typ ActivityVocabularyType) *Actor {
|
||||
if !ActorTypes.Contains(typ) {
|
||||
typ = ActorType
|
||||
}
|
||||
|
||||
a := Actor{ID: id, Type: typ}
|
||||
a.Name = NaturalLanguageValuesNew()
|
||||
a.Content = NaturalLanguageValuesNew()
|
||||
a.Summary = NaturalLanguageValuesNew()
|
||||
a.PreferredUsername = NaturalLanguageValuesNew()
|
||||
|
||||
return &a
|
||||
}
|
||||
|
||||
// ApplicationNew initializes an Application type actor
|
||||
func ApplicationNew(id ID) *Application {
|
||||
a := ActorNew(id, ApplicationType)
|
||||
o := Application(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// GroupNew initializes a Group type actor
|
||||
func GroupNew(id ID) *Group {
|
||||
a := ActorNew(id, GroupType)
|
||||
o := Group(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// OrganizationNew initializes an Organization type actor
|
||||
func OrganizationNew(id ID) *Organization {
|
||||
a := ActorNew(id, OrganizationType)
|
||||
o := Organization(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// PersonNew initializes a Person type actor
|
||||
func PersonNew(id ID) *Person {
|
||||
a := ActorNew(id, PersonType)
|
||||
o := Person(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// ServiceNew initializes a Service type actor
|
||||
func ServiceNew(id ID) *Service {
|
||||
a := ActorNew(id, ServiceType)
|
||||
o := Service(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
func (a *Actor) Recipients() ItemCollection {
|
||||
aud := a.Audience
|
||||
return ItemCollectionDeduplication(&a.To, &a.CC, &a.Bto, &a.BCC, &aud)
|
||||
}
|
||||
|
||||
func (a *Actor) Clean() {
|
||||
_ = OnObject(a, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (a *Actor) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadActor(val, a)
|
||||
}
|
||||
|
||||
func (a Actor) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(a, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if a.Inbox != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "inbox", a.Inbox) || notEmpty
|
||||
}
|
||||
if a.Outbox != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "outbox", a.Outbox) || notEmpty
|
||||
}
|
||||
if a.Following != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "following", a.Following) || notEmpty
|
||||
}
|
||||
if a.Followers != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "followers", a.Followers) || notEmpty
|
||||
}
|
||||
if a.Liked != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "liked", a.Liked) || notEmpty
|
||||
}
|
||||
if a.PreferredUsername != nil {
|
||||
notEmpty = JSONWriteNaturalLanguageProp(&b, "preferredUsername", a.PreferredUsername) || notEmpty
|
||||
}
|
||||
if a.Endpoints != nil {
|
||||
if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(&b, "endpoints", v) || notEmpty
|
||||
}
|
||||
}
|
||||
if len(a.Streams) > 0 {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "streams", a.Streams, false)
|
||||
}
|
||||
if len(a.PublicKey.PublicKeyPem)+len(a.PublicKey.ID) > 0 {
|
||||
if v, err := a.PublicKey.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(&b, "publicKey", v) || notEmpty
|
||||
}
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a Actor) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
if a.Type != "" && a.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T[%s]( %s )", a, a.Type, a.ID)
|
||||
} else if a.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T( %s )", a, a.ID)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%T[%p]", a, &a)
|
||||
}
|
||||
case 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", a, a.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoints a json object which maps additional (typically server/domain-wide)
|
||||
// endpoints which may be useful either for this actor or someone referencing this actor.
|
||||
// This mapping may be nested inside the actor document as the value or may be a link to
|
||||
// a JSON-LD document with these properties.
|
||||
type Endpoints struct {
|
||||
// UploadMedia Upload endpoint URI for this user for binary data.
|
||||
UploadMedia Item `jsonld:"uploadMedia,omitempty"`
|
||||
// OauthAuthorizationEndpoint Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication
|
||||
// to access. To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being
|
||||
// the id of the requested ActivityStreams object.
|
||||
OauthAuthorizationEndpoint Item `jsonld:"oauthAuthorizationEndpoint,omitempty"`
|
||||
// OauthTokenEndpoint If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
|
||||
// this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant.
|
||||
OauthTokenEndpoint Item `jsonld:"oauthTokenEndpoint,omitempty"`
|
||||
// ProvideClientKey If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
|
||||
// this endpoint specifies a URI at which a client may acquire an access token.
|
||||
ProvideClientKey Item `jsonld:"provideClientKey,omitempty"`
|
||||
// SignClientKey If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization,
|
||||
// this endpoint specifies a URI at which browser-authenticated users may authorize a client's public
|
||||
// key for client to server interactions.
|
||||
SignClientKey Item `jsonld:"signClientKey,omitempty"`
|
||||
// SharedInbox An optional endpoint used for wide delivery of publicly addressed activities and activities sent to followers.
|
||||
// SharedInbox endpoints SHOULD also be publicly readable OrderedCollection objects containing objects addressed to the
|
||||
// Public special collection. Reading from the sharedInbox endpoint MUST NOT present objects which are not addressed to the Public endpoint.
|
||||
SharedInbox Item `jsonld:"sharedInbox,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (e *Endpoints) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e.OauthAuthorizationEndpoint = JSONGetItem(val, "oauthAuthorizationEndpoint")
|
||||
e.OauthTokenEndpoint = JSONGetItem(val, "oauthTokenEndpoint")
|
||||
e.UploadMedia = JSONGetItem(val, "uploadMedia")
|
||||
e.ProvideClientKey = JSONGetItem(val, "provideClientKey")
|
||||
e.SignClientKey = JSONGetItem(val, "signClientKey")
|
||||
e.SharedInbox = JSONGetItem(val, "sharedInbox")
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (e Endpoints) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
|
||||
JSONWrite(&b, '{')
|
||||
if e.OauthAuthorizationEndpoint != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty
|
||||
}
|
||||
if e.OauthTokenEndpoint != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty
|
||||
}
|
||||
if e.ProvideClientKey != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty
|
||||
}
|
||||
if e.SignClientKey != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty
|
||||
}
|
||||
if e.SharedInbox != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty
|
||||
}
|
||||
if e.UploadMedia != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ToActor
|
||||
func ToActor(it Item) (*Actor, error) {
|
||||
switch i := it.(type) {
|
||||
case *Actor:
|
||||
return i, nil
|
||||
case Actor:
|
||||
return &i, nil
|
||||
default:
|
||||
return reflectItemToType[Actor](it)
|
||||
}
|
||||
}
|
||||
|
||||
// Equals verifies if our receiver Object is equals with the "with" Object
|
||||
func (a Actor) Equals(with Item) bool {
|
||||
result := true
|
||||
err := OnActor(with, func(w *Actor) error {
|
||||
_ = OnObject(a, func(oa *Object) error {
|
||||
result = oa.Equals(w)
|
||||
return nil
|
||||
})
|
||||
if w.Inbox != nil {
|
||||
if !ItemsEqual(a.Inbox, w.Inbox) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Outbox != nil {
|
||||
if !ItemsEqual(a.Outbox, w.Outbox) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Liked != nil {
|
||||
if !ItemsEqual(a.Liked, w.Liked) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.PreferredUsername != nil {
|
||||
if !a.PreferredUsername.Equals(w.PreferredUsername) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e Endpoints) GobEncode() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (e *Endpoints) GobDecode(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p PublicKey) GobEncode() ([]byte, error) {
|
||||
var (
|
||||
mm = make(map[string][]byte)
|
||||
err error
|
||||
hasData bool
|
||||
)
|
||||
if len(p.ID) > 0 {
|
||||
if mm["id"], err = p.ID.GobEncode(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(p.PublicKeyPem) > 0 {
|
||||
mm["publicKeyPem"] = []byte(p.PublicKeyPem)
|
||||
hasData = true
|
||||
}
|
||||
if len(p.Owner) > 0 {
|
||||
if mm["owner"], err = gobEncodeItem(p.Owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (p *PublicKey) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["id"]; ok {
|
||||
if err = p.ID.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["owner"]; ok {
|
||||
if err = p.Owner.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["publicKeyPem"]; ok {
|
||||
p.PublicKeyPem = string(raw)
|
||||
}
|
||||
return nil
|
||||
}
|
571
actor_test.go
Normal file
571
actor_test.go
Normal file
|
@ -0,0 +1,571 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestActorNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
testType := ApplicationType
|
||||
|
||||
o := ActorNew(testValue, testType)
|
||||
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != testType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, testType)
|
||||
}
|
||||
|
||||
n := ActorNew(testValue, "")
|
||||
if n.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", n.ID, testValue)
|
||||
}
|
||||
if n.Type != ActorType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", n.Type, ActorType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
o := PersonNew(testValue)
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != PersonType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, PersonType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
o := ApplicationNew(testValue)
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != ApplicationType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, ApplicationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
o := GroupNew(testValue)
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != GroupType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, GroupType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrganizationNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
o := OrganizationNew(testValue)
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != OrganizationType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, OrganizationType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
o := ServiceNew(testValue)
|
||||
if o.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
|
||||
}
|
||||
if o.Type != ServiceType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, ServiceType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_IsLink(t *testing.T) {
|
||||
m := ActorNew("test", ActorType)
|
||||
if m.IsLink() {
|
||||
t.Errorf("%#v should not be a valid Link", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_IsObject(t *testing.T) {
|
||||
m := ActorNew("test", ActorType)
|
||||
if !m.IsObject() {
|
||||
t.Errorf("%#v should be a valid object", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_Object(t *testing.T) {
|
||||
m := ActorNew("test", ActorType)
|
||||
if reflect.DeepEqual(ID(""), m.GetID()) {
|
||||
t.Errorf("%#v should not be an empty activity pub object", m.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_Type(t *testing.T) {
|
||||
m := ActorNew("test", ActorType)
|
||||
if m.GetType() != ActorType {
|
||||
t.Errorf("%#v should be an empty Link object", m.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerson_IsLink(t *testing.T) {
|
||||
m := PersonNew("test")
|
||||
if m.IsLink() {
|
||||
t.Errorf("%T should not be a valid Link", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerson_IsObject(t *testing.T) {
|
||||
m := PersonNew("test")
|
||||
if !m.IsObject() {
|
||||
t.Errorf("%T should be a valid object", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestApplication_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestGroup_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrganization_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPerson_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPerson_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPerson_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPerson_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func validateEmptyPerson(p Person, t *testing.T) {
|
||||
if p.ID != "" {
|
||||
t.Errorf("Unmarshaled object %T should have empty ID, received %q", p, p.ID)
|
||||
}
|
||||
if p.Type != "" {
|
||||
t.Errorf("Unmarshaled object %T should have empty Type, received %q", p, p.Type)
|
||||
}
|
||||
if p.AttributedTo != nil {
|
||||
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", p, p.AttributedTo)
|
||||
}
|
||||
if len(p.Name) != 0 {
|
||||
t.Errorf("Unmarshaled object %T should have empty Name, received %q", p, p.Name)
|
||||
}
|
||||
if len(p.Summary) != 0 {
|
||||
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", p, p.Summary)
|
||||
}
|
||||
if len(p.Content) != 0 {
|
||||
t.Errorf("Unmarshaled object %T should have empty Content, received %q", p, p.Content)
|
||||
}
|
||||
if p.URL != nil {
|
||||
t.Errorf("Unmarshaled object %T should have empty URL, received %v", p, p.URL)
|
||||
}
|
||||
if !p.Published.IsZero() {
|
||||
t.Errorf("Unmarshaled object %T should have empty Published, received %q", p, p.Published)
|
||||
}
|
||||
if !p.StartTime.IsZero() {
|
||||
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", p, p.StartTime)
|
||||
}
|
||||
if !p.Updated.IsZero() {
|
||||
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", p, p.Updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerson_UnmarshalJSON(t *testing.T) {
|
||||
p := Person{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
p.UnmarshalJSON(dataEmpty)
|
||||
validateEmptyPerson(p, t)
|
||||
}
|
||||
|
||||
func TestApplication_UnmarshalJSON(t *testing.T) {
|
||||
a := Application{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
a.UnmarshalJSON(dataEmpty)
|
||||
validateEmptyPerson(Person(a), t)
|
||||
}
|
||||
|
||||
func TestGroup_UnmarshalJSON(t *testing.T) {
|
||||
g := Group{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
g.UnmarshalJSON(dataEmpty)
|
||||
validateEmptyPerson(Person(g), t)
|
||||
}
|
||||
|
||||
func TestOrganization_UnmarshalJSON(t *testing.T) {
|
||||
o := Organization{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
o.UnmarshalJSON(dataEmpty)
|
||||
validateEmptyPerson(Person(o), t)
|
||||
}
|
||||
|
||||
func TestService_UnmarshalJSON(t *testing.T) {
|
||||
s := Service{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
s.UnmarshalJSON(dataEmpty)
|
||||
validateEmptyPerson(Person(s), t)
|
||||
}
|
||||
|
||||
func TestService_GetActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestService_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestService_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestService_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestService_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestService_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToPerson(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestEndpoints_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_Recipients(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPublicKey_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestActor_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestEndpoints_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPublicKey_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func assertPersonWithTesting(fn canErrorFunc, expected Item) WithActorFn {
|
||||
return func(p *Person) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnActor(t *testing.T) {
|
||||
testPerson := Actor{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, Item) WithActorFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testPerson, assertPersonWithTesting},
|
||||
expected: &testPerson,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{Person{ID: "https://not-equals"}, assertPersonWithTesting},
|
||||
expected: &testPerson,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfPersons",
|
||||
args: args{ItemCollection{testPerson, testPerson}, assertPersonWithTesting},
|
||||
expected: &testPerson,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfPersons fails",
|
||||
args: args{ItemCollection{testPerson, Person{ID: "https://not-equals"}}, assertPersonWithTesting},
|
||||
expected: &testPerson,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnActor(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnPerson() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActor_Equals(t *testing.T) {
|
||||
type fields struct {
|
||||
ID ID
|
||||
Type ActivityVocabularyType
|
||||
Name NaturalLanguageValues
|
||||
Attachment Item
|
||||
AttributedTo Item
|
||||
Audience ItemCollection
|
||||
Content NaturalLanguageValues
|
||||
Context Item
|
||||
MediaType MimeType
|
||||
EndTime time.Time
|
||||
Generator Item
|
||||
Icon Item
|
||||
Image Item
|
||||
InReplyTo Item
|
||||
Location Item
|
||||
Preview Item
|
||||
Published time.Time
|
||||
Replies Item
|
||||
StartTime time.Time
|
||||
Summary NaturalLanguageValues
|
||||
Tag ItemCollection
|
||||
Updated time.Time
|
||||
URL Item
|
||||
To ItemCollection
|
||||
Bto ItemCollection
|
||||
CC ItemCollection
|
||||
BCC ItemCollection
|
||||
Duration time.Duration
|
||||
Likes Item
|
||||
Shares Item
|
||||
Source Source
|
||||
Inbox Item
|
||||
Outbox Item
|
||||
Following Item
|
||||
Followers Item
|
||||
Liked Item
|
||||
PreferredUsername NaturalLanguageValues
|
||||
Endpoints *Endpoints
|
||||
Streams ItemCollection
|
||||
PublicKey PublicKey
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
arg Item
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal-empty-actor",
|
||||
fields: fields{},
|
||||
arg: Actor{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-actor-just-id",
|
||||
fields: fields{ID: "test"},
|
||||
arg: Actor{ID: "test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-actor-id",
|
||||
fields: fields{ID: "test", URL: IRI("example.com")},
|
||||
arg: Actor{ID: "test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-false-with-id-and-url",
|
||||
fields: fields{ID: "test"},
|
||||
arg: Actor{ID: "test", URL: IRI("example.com")},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not a valid actor",
|
||||
fields: fields{ID: "http://example.com"},
|
||||
arg: Activity{ID: "http://example.com"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := Actor{
|
||||
ID: tt.fields.ID,
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
Attachment: tt.fields.Attachment,
|
||||
AttributedTo: tt.fields.AttributedTo,
|
||||
Audience: tt.fields.Audience,
|
||||
Content: tt.fields.Content,
|
||||
Context: tt.fields.Context,
|
||||
MediaType: tt.fields.MediaType,
|
||||
EndTime: tt.fields.EndTime,
|
||||
Generator: tt.fields.Generator,
|
||||
Icon: tt.fields.Icon,
|
||||
Image: tt.fields.Image,
|
||||
InReplyTo: tt.fields.InReplyTo,
|
||||
Location: tt.fields.Location,
|
||||
Preview: tt.fields.Preview,
|
||||
Published: tt.fields.Published,
|
||||
Replies: tt.fields.Replies,
|
||||
StartTime: tt.fields.StartTime,
|
||||
Summary: tt.fields.Summary,
|
||||
Tag: tt.fields.Tag,
|
||||
Updated: tt.fields.Updated,
|
||||
URL: tt.fields.URL,
|
||||
To: tt.fields.To,
|
||||
Bto: tt.fields.Bto,
|
||||
CC: tt.fields.CC,
|
||||
BCC: tt.fields.BCC,
|
||||
Duration: tt.fields.Duration,
|
||||
Likes: tt.fields.Likes,
|
||||
Shares: tt.fields.Shares,
|
||||
Source: tt.fields.Source,
|
||||
Inbox: tt.fields.Inbox,
|
||||
Outbox: tt.fields.Outbox,
|
||||
Following: tt.fields.Following,
|
||||
Followers: tt.fields.Followers,
|
||||
Liked: tt.fields.Liked,
|
||||
PreferredUsername: tt.fields.PreferredUsername,
|
||||
Endpoints: tt.fields.Endpoints,
|
||||
Streams: tt.fields.Streams,
|
||||
PublicKey: tt.fields.PublicKey,
|
||||
}
|
||||
if got := a.Equals(tt.arg); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
439
collection.go
Normal file
439
collection.go
Normal file
|
@ -0,0 +1,439 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const CollectionOfIRIs ActivityVocabularyType = "IRICollection"
|
||||
const CollectionOfItems ActivityVocabularyType = "ItemCollection"
|
||||
|
||||
var CollectionTypes = ActivityVocabularyTypes{
|
||||
CollectionOfItems,
|
||||
CollectionType,
|
||||
OrderedCollectionType,
|
||||
CollectionPageType,
|
||||
OrderedCollectionPageType,
|
||||
}
|
||||
|
||||
// Collections
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#collections
|
||||
//
|
||||
// [ActivityStreams] defines the collection concept; ActivityPub defines several collections with special behavior.
|
||||
//
|
||||
// Note that ActivityPub makes use of ActivityStreams paging to traverse large sets of objects.
|
||||
//
|
||||
// Note that some of these collections are specified to be of type OrderedCollection specifically,
|
||||
// while others are permitted to be either a Collection or an OrderedCollection.
|
||||
// An OrderedCollection MUST be presented consistently in reverse chronological order.
|
||||
//
|
||||
// NOTE
|
||||
// What property is used to determine the reverse chronological order is intentionally left as an implementation detail.
|
||||
// For example, many SQL-style databases use an incrementing integer as an identifier, which can be reasonably used for
|
||||
// handling insertion order in most cases. In other databases, an insertion time timestamp may be preferred.
|
||||
// What is used isn't important, but the ordering of elements must remain intact, with newer items first.
|
||||
// A property which changes regularly, such a "last updated" timestamp, should not be used.
|
||||
type Collections interface {
|
||||
Collection | CollectionPage | OrderedCollection | OrderedCollectionPage | ItemCollection | IRIs
|
||||
}
|
||||
|
||||
type CollectionInterface interface {
|
||||
ObjectOrLink
|
||||
Collection() ItemCollection
|
||||
Append(ob ...Item) error
|
||||
Count() uint
|
||||
Contains(Item) bool
|
||||
}
|
||||
|
||||
// Collection is a subtype of Activity Pub Object that represents ordered or unordered sets of Activity Pub Object or Link instances.
|
||||
type Collection struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be ordered or unordered.
|
||||
Items ItemCollection `jsonld:"items,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
// FollowersCollection is a collection of followers
|
||||
FollowersCollection = Collection
|
||||
|
||||
// FollowingCollection is a list of everybody that the actor has followed, added as a side effect.
|
||||
// The following collection MUST be either an OrderedCollection or a Collection and MAY
|
||||
// be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
FollowingCollection = Collection
|
||||
)
|
||||
|
||||
// CollectionNew initializes a new Collection
|
||||
func CollectionNew(id ID) *Collection {
|
||||
c := Collection{ID: id, Type: CollectionType}
|
||||
c.Name = NaturalLanguageValuesNew()
|
||||
c.Content = NaturalLanguageValuesNew()
|
||||
c.Summary = NaturalLanguageValuesNew()
|
||||
return &c
|
||||
}
|
||||
|
||||
// OrderedCollectionNew initializes a new OrderedCollection
|
||||
func OrderedCollectionNew(id ID) *OrderedCollection {
|
||||
o := OrderedCollection{ID: id, Type: OrderedCollectionType}
|
||||
o.Name = NaturalLanguageValuesNew()
|
||||
o.Content = NaturalLanguageValuesNew()
|
||||
|
||||
return &o
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the Collection object
|
||||
func (c Collection) GetID() ID {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetType returns the Collection's type
|
||||
func (c Collection) GetType() ActivityVocabularyType {
|
||||
return c.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for a Collection object
|
||||
func (c Collection) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a Collection object
|
||||
func (c Collection) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns true for Collection objects
|
||||
func (c Collection) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the Collection object
|
||||
func (c Collection) GetLink() IRI {
|
||||
return IRI(c.ID)
|
||||
}
|
||||
|
||||
// Collection returns the Collection's items
|
||||
func (c Collection) Collection() ItemCollection {
|
||||
return c.Items
|
||||
}
|
||||
|
||||
// Append adds an element to a Collection
|
||||
func (c *Collection) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if c.Items.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
c.Items = append(c.Items, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in collection and its TotalItems property
|
||||
func (c *Collection) Count() uint {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(c.Items))
|
||||
}
|
||||
|
||||
// Contains verifies if Collection array contains the received one
|
||||
func (c Collection) Contains(r Item) bool {
|
||||
if len(c.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range c.Items {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (c *Collection) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadCollection(val, c)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (c Collection) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(c, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if c.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", c.Current) || notEmpty
|
||||
}
|
||||
if c.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", c.First) || notEmpty
|
||||
}
|
||||
if c.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", c.Last) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty
|
||||
if c.Items != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "items", c.Items, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (c *Collection) UnmarshalBinary(data []byte) error {
|
||||
return c.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (c Collection) MarshalBinary() ([]byte, error) {
|
||||
return c.GobEncode()
|
||||
}
|
||||
|
||||
func (c Collection) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapCollectionProperties(mm, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Collection) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapCollectionProperties(mm, c)
|
||||
}
|
||||
|
||||
func (c Collection) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", c, c.Type, c.TotalItems)
|
||||
}
|
||||
}
|
||||
|
||||
// ToCollection
|
||||
func ToCollection(it Item) (*Collection, error) {
|
||||
switch i := it.(type) {
|
||||
case *Collection:
|
||||
return i, nil
|
||||
case Collection:
|
||||
return &i, nil
|
||||
case *CollectionPage:
|
||||
return (*Collection)(unsafe.Pointer(i)), nil
|
||||
case CollectionPage:
|
||||
return (*Collection)(unsafe.Pointer(&i)), nil
|
||||
// NOTE(marius): let's try again to convert OrderedCollection -> Collection, as they have the same
|
||||
// shape in memory.
|
||||
case *OrderedCollection:
|
||||
return (*Collection)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollection:
|
||||
return (*Collection)(unsafe.Pointer(&i)), nil
|
||||
case *OrderedCollectionPage:
|
||||
return (*Collection)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollectionPage:
|
||||
return (*Collection)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[Collection](it)
|
||||
}
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (c Collection) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := c.Items.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (c Collection) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
_ = OnCollection(with, func(w *Collection) error {
|
||||
_ = OnObject(w, func(wo *Object) error {
|
||||
if !wo.Equals(c) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.TotalItems > 0 {
|
||||
if w.TotalItems != c.TotalItems {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Current != nil {
|
||||
if !ItemsEqual(c.Current, w.Current) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.First != nil {
|
||||
if !ItemsEqual(c.First, w.First) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Last != nil {
|
||||
if !ItemsEqual(c.Last, w.Last) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Items != nil {
|
||||
if !ItemsEqual(c.Items, w.Items) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (c *Collection) Recipients() ItemCollection {
|
||||
aud := c.Audience
|
||||
return ItemCollectionDeduplication(&c.To, &c.CC, &c.Bto, &c.BCC, &aud)
|
||||
}
|
||||
|
||||
func (c *Collection) Clean() {
|
||||
_ = OnObject(c, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
433
collection_page.go
Normal file
433
collection_page.go
Normal file
|
@ -0,0 +1,433 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// CollectionPage is a Collection that contains a large number of items and when it becomes impractical
|
||||
// for an implementation to serialize every item contained by a Collection using the items
|
||||
// property alone. In such cases, the items within a Collection can be divided into distinct subsets or "pages".
|
||||
type CollectionPage struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be unordered.
|
||||
Items ItemCollection `jsonld:"items,omitempty"`
|
||||
// Identifies the Collection to which a CollectionPage objects items belong.
|
||||
PartOf Item `jsonld:"partOf,omitempty"`
|
||||
// In a paged Collection, indicates the next page of items.
|
||||
Next Item `jsonld:"next,omitempty"`
|
||||
// In a paged Collection, identifies the previous page of items.
|
||||
Prev Item `jsonld:"prev,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the CollectionPage object
|
||||
func (c CollectionPage) GetID() ID {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetType returns the CollectionPage's type
|
||||
func (c CollectionPage) GetType() ActivityVocabularyType {
|
||||
return c.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for a CollectionPage object
|
||||
func (c CollectionPage) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a CollectionPage object
|
||||
func (c CollectionPage) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns true for CollectionPage objects
|
||||
func (c CollectionPage) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the CollectionPage object
|
||||
func (c CollectionPage) GetLink() IRI {
|
||||
return IRI(c.ID)
|
||||
}
|
||||
|
||||
// Collection returns the ColleCollectionPagection items
|
||||
func (c CollectionPage) Collection() ItemCollection {
|
||||
return c.Items
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in the collection page and its TotalItems property
|
||||
func (c *CollectionPage) Count() uint {
|
||||
if c == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(c.Items))
|
||||
}
|
||||
|
||||
// Append adds an element to a CollectionPage
|
||||
func (c *CollectionPage) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if c.Items.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
c.Items = append(c.Items, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains verifies if CollectionPage array contains the received one
|
||||
func (c CollectionPage) Contains(r Item) bool {
|
||||
if len(c.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range c.Items {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (c *CollectionPage) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadCollectionPage(val, c)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (c CollectionPage) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(c, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if c.PartOf != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "partOf", c.PartOf) || notEmpty
|
||||
}
|
||||
if c.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", c.Current) || notEmpty
|
||||
}
|
||||
if c.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", c.First) || notEmpty
|
||||
}
|
||||
if c.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", c.Last) || notEmpty
|
||||
}
|
||||
if c.Next != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "next", c.Next) || notEmpty
|
||||
}
|
||||
if c.Prev != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "prev", c.Prev) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty
|
||||
if c.Items != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "items", c.Items, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (c *CollectionPage) UnmarshalBinary(data []byte) error {
|
||||
return c.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (c CollectionPage) MarshalBinary() ([]byte, error) {
|
||||
return c.GobEncode()
|
||||
}
|
||||
|
||||
func (c CollectionPage) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapCollectionPageProperties(mm, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *CollectionPage) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapCollectionPageProperties(mm, c)
|
||||
}
|
||||
|
||||
// CollectionNew initializes a new CollectionPage
|
||||
func CollectionPageNew(parent CollectionInterface) *CollectionPage {
|
||||
p := CollectionPage{
|
||||
PartOf: parent.GetLink(),
|
||||
}
|
||||
if pc, ok := parent.(*Collection); ok {
|
||||
copyCollectionToPage(pc, &p)
|
||||
}
|
||||
p.Type = CollectionPageType
|
||||
return &p
|
||||
}
|
||||
|
||||
func copyCollectionToPage(c *Collection, p *CollectionPage) error {
|
||||
p.Type = CollectionPageType
|
||||
p.Name = c.Name
|
||||
p.Content = c.Content
|
||||
p.Summary = c.Summary
|
||||
p.Context = c.Context
|
||||
p.URL = c.URL
|
||||
p.MediaType = c.MediaType
|
||||
p.Generator = c.Generator
|
||||
p.AttributedTo = c.AttributedTo
|
||||
p.Attachment = c.Attachment
|
||||
p.Location = c.Location
|
||||
p.Published = c.Published
|
||||
p.StartTime = c.StartTime
|
||||
p.EndTime = c.EndTime
|
||||
p.Duration = c.Duration
|
||||
p.Icon = c.Icon
|
||||
p.Preview = c.Preview
|
||||
p.Image = c.Image
|
||||
p.Updated = c.Updated
|
||||
p.InReplyTo = c.InReplyTo
|
||||
p.To = c.To
|
||||
p.Audience = c.Audience
|
||||
p.Bto = c.Bto
|
||||
p.CC = c.CC
|
||||
p.BCC = c.BCC
|
||||
p.Replies = c.Replies
|
||||
p.Tag = c.Tag
|
||||
p.TotalItems = c.TotalItems
|
||||
p.Items = c.Items
|
||||
p.Current = c.Current
|
||||
p.First = c.First
|
||||
p.PartOf = c.GetLink()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToCollectionPage
|
||||
func ToCollectionPage(it Item) (*CollectionPage, error) {
|
||||
switch i := it.(type) {
|
||||
case *CollectionPage:
|
||||
return i, nil
|
||||
case CollectionPage:
|
||||
return &i, nil
|
||||
// NOTE(marius): let's try again to convert OrderedCollectionPage -> CollectionPage, as they have the same
|
||||
// shape in memory.
|
||||
case *OrderedCollectionPage:
|
||||
return (*CollectionPage)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollectionPage:
|
||||
return (*CollectionPage)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[CollectionPage](it)
|
||||
}
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (c CollectionPage) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := c.Items.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (c CollectionPage) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
OnCollectionPage(with, func(w *CollectionPage) error {
|
||||
OnCollection(w, func(wo *Collection) error {
|
||||
if !wo.Equals(c) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.PartOf != nil {
|
||||
if !ItemsEqual(c.PartOf, w.PartOf) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Current != nil {
|
||||
if !ItemsEqual(c.Current, w.Current) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.First != nil {
|
||||
if !ItemsEqual(c.First, w.First) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Last != nil {
|
||||
if !ItemsEqual(c.Last, w.Last) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Next != nil {
|
||||
if !ItemsEqual(c.Next, w.Next) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Prev != nil {
|
||||
if !ItemsEqual(c.Prev, w.Prev) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (c CollectionPage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", c, c.Type, c.TotalItems)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CollectionPage) Recipients() ItemCollection {
|
||||
aud := c.Audience
|
||||
return ItemCollectionDeduplication(&c.To, &c.CC, &c.Bto, &c.BCC, &aud)
|
||||
}
|
||||
|
||||
func (c *CollectionPage) Clean() {
|
||||
_ = OnObject(c, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
226
collection_page_test.go
Normal file
226
collection_page_test.go
Normal file
|
@ -0,0 +1,226 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCollectionPageNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
c := CollectionNew(testValue)
|
||||
p := CollectionPageNew(c)
|
||||
if reflect.DeepEqual(p.Collection, c) {
|
||||
t.Errorf("Invalid collection parent '%v'", p.PartOf)
|
||||
}
|
||||
if p.PartOf != c.GetLink() {
|
||||
t.Errorf("Invalid collection '%v'", p.PartOf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPage_Append(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
val := Object{ID: ID("grrr")}
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
p := CollectionPageNew(c)
|
||||
p.Append(val)
|
||||
|
||||
if p.PartOf != c.GetLink() {
|
||||
t.Errorf("Collection page should point to collection %q", c.GetLink())
|
||||
}
|
||||
if p.Count() != 1 {
|
||||
t.Errorf("Collection page of %q should have exactly one element", p.GetID())
|
||||
}
|
||||
if !reflect.DeepEqual(p.Items[0], val) {
|
||||
t.Errorf("First item in Inbox is does not match %q", val.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPage_UnmarshalJSON(t *testing.T) {
|
||||
p := CollectionPage{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
p.UnmarshalJSON(dataEmpty)
|
||||
if p.ID != "" {
|
||||
t.Errorf("Unmarshaled object should have empty ID, received %q", p.ID)
|
||||
}
|
||||
if p.Type != "" {
|
||||
t.Errorf("Unmarshaled object should have empty Type, received %q", p.Type)
|
||||
}
|
||||
if p.AttributedTo != nil {
|
||||
t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", p.AttributedTo)
|
||||
}
|
||||
if len(p.Name) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Name, received %q", p.Name)
|
||||
}
|
||||
if len(p.Summary) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Summary, received %q", p.Summary)
|
||||
}
|
||||
if len(p.Content) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Content, received %q", p.Content)
|
||||
}
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if len(p.Items) > 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Items, received %v", p.Items)
|
||||
}
|
||||
if p.URL != nil {
|
||||
t.Errorf("Unmarshaled object should have empty URL, received %v", p.URL)
|
||||
}
|
||||
if !p.Published.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Published, received %q", p.Published)
|
||||
}
|
||||
if !p.StartTime.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty StartTime, received %q", p.StartTime)
|
||||
}
|
||||
if !p.Updated.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Updated, received %q", p.Updated)
|
||||
}
|
||||
if p.PartOf != nil {
|
||||
t.Errorf("Unmarshaled object should have empty PartOf, received %q", p.PartOf)
|
||||
}
|
||||
if p.Current != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Current, received %q", p.Current)
|
||||
}
|
||||
if p.First != nil {
|
||||
t.Errorf("Unmarshaled object should have empty First, received %q", p.First)
|
||||
}
|
||||
if p.Last != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Last, received %q", p.Last)
|
||||
}
|
||||
if p.Next != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Next, received %q", p.Next)
|
||||
}
|
||||
if p.Prev != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Prev, received %q", p.Prev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPage_Collection(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
p := CollectionPageNew(c)
|
||||
|
||||
if !reflect.DeepEqual(p.Collection(), p.Items) {
|
||||
t.Errorf("Collection items should be equal %v %v", p.Collection(), p.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPage_Count(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
p := CollectionPageNew(c)
|
||||
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if len(p.Items) > 0 {
|
||||
t.Errorf("Empty object should have empty Items, received %v", p.Items)
|
||||
}
|
||||
if p.Count() != uint(len(p.Items)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.Items))
|
||||
}
|
||||
|
||||
p.Append(IRI("test"))
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if p.Count() != uint(len(p.Items)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestToCollectionPage(t *testing.T) {
|
||||
err := func(it Item) error { return ErrorInvalidType[CollectionPage](it) }
|
||||
tests := map[string]struct {
|
||||
it Item
|
||||
want *CollectionPage
|
||||
wantErr error
|
||||
}{
|
||||
"CollectionPage": {
|
||||
it: new(CollectionPage),
|
||||
want: new(CollectionPage),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollectionPage": {
|
||||
it: new(OrderedCollectionPage),
|
||||
want: new(CollectionPage),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollection": {
|
||||
it: new(OrderedCollection),
|
||||
want: new(CollectionPage),
|
||||
wantErr: err(new(OrderedCollection)),
|
||||
},
|
||||
"Collection": {
|
||||
it: new(Collection),
|
||||
want: new(CollectionPage),
|
||||
wantErr: err(new(Collection)),
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := ToCollectionPage(tt.it)
|
||||
if tt.wantErr != nil && err == nil {
|
||||
t.Errorf("ToCollectionPage() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if tt.wantErr == nil {
|
||||
t.Errorf("ToCollectionPage() returned unexpected error[%T]%s", err, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.wantErr) {
|
||||
t.Errorf("ToCollectionPage() received error %v, wanted error %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToCollectionPage() got %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPage_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionPage_ItemMatches(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
279
collection_test.go
Normal file
279
collection_test.go
Normal file
|
@ -0,0 +1,279 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCollectionNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
c := CollectionNew(testValue)
|
||||
|
||||
if c.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", c.ID, testValue)
|
||||
}
|
||||
if c.Type != CollectionType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", c.Type, CollectionType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_Append(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
val := Object{ID: ID("grrr")}
|
||||
|
||||
c := CollectionNew(id)
|
||||
c.Append(val)
|
||||
|
||||
if c.Count() != 1 {
|
||||
t.Errorf("Inbox collectionPath of %q should have one element", c.GetID())
|
||||
}
|
||||
if !reflect.DeepEqual(c.Items[0], val) {
|
||||
t.Errorf("First item in Inbox is does not match %q", val.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_Collection(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if !reflect.DeepEqual(c.Collection(), c.Items) {
|
||||
t.Errorf("Collection items should be equal %v %v", c.Collection(), c.Items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_GetID(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.GetID() != id {
|
||||
t.Errorf("GetID should return %s, received %s", id, c.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_GetLink(t *testing.T) {
|
||||
id := ID("test")
|
||||
link := IRI(id)
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.GetLink() != link {
|
||||
t.Errorf("GetLink should return %q, received %q", link, c.GetLink())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_GetType(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.GetType() != CollectionType {
|
||||
t.Errorf("Collection Type should be %q, received %q", CollectionType, c.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_IsLink(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.IsLink() != false {
|
||||
t.Errorf("Collection should not be a link, received %t", c.IsLink())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_IsObject(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.IsObject() != true {
|
||||
t.Errorf("Collection should be an object, received %t", c.IsObject())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_UnmarshalJSON(t *testing.T) {
|
||||
c := Collection{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
c.UnmarshalJSON(dataEmpty)
|
||||
if c.ID != "" {
|
||||
t.Errorf("Unmarshaled object should have empty ID, received %q", c.ID)
|
||||
}
|
||||
if c.Type != "" {
|
||||
t.Errorf("Unmarshaled object should have empty Type, received %q", c.Type)
|
||||
}
|
||||
if c.AttributedTo != nil {
|
||||
t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", c.AttributedTo)
|
||||
}
|
||||
if len(c.Name) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Name, received %q", c.Name)
|
||||
}
|
||||
if len(c.Summary) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Summary, received %q", c.Summary)
|
||||
}
|
||||
if len(c.Content) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Content, received %q", c.Content)
|
||||
}
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if len(c.Items) > 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Items, received %v", c.Items)
|
||||
}
|
||||
if c.URL != nil {
|
||||
t.Errorf("Unmarshaled object should have empty URL, received %v", c.URL)
|
||||
}
|
||||
if !c.Published.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Published, received %q", c.Published)
|
||||
}
|
||||
if !c.StartTime.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty StartTime, received %q", c.StartTime)
|
||||
}
|
||||
if !c.Updated.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Updated, received %q", c.Updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_Count(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := CollectionNew(id)
|
||||
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if len(c.Items) > 0 {
|
||||
t.Errorf("Empty object should have empty Items, received %v", c.Items)
|
||||
}
|
||||
if c.Count() != uint(len(c.Items)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.Items))
|
||||
}
|
||||
|
||||
c.Append(IRI("test"))
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if c.Count() != uint(len(c.Items)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.Items))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollection_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFollowersNew(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFollowingNew(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollection_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollection_ItemMatches(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToCollection(t *testing.T) {
|
||||
//err := func(it Item) error { return ErrorInvalidType[Collection](it) }
|
||||
tests := map[string]struct {
|
||||
it Item
|
||||
want *Collection
|
||||
wantErr error
|
||||
}{
|
||||
"Collection": {
|
||||
it: new(Collection),
|
||||
want: new(Collection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"CollectionPage": {
|
||||
it: new(CollectionPage),
|
||||
want: new(Collection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollectionPage": {
|
||||
it: new(OrderedCollectionPage),
|
||||
want: new(Collection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollection": {
|
||||
it: new(OrderedCollection),
|
||||
want: new(Collection),
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := ToCollection(tt.it)
|
||||
if tt.wantErr != nil && err == nil {
|
||||
t.Errorf("ToCollection() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if tt.wantErr == nil {
|
||||
t.Errorf("ToCollection() returned unexpected error[%T]%s", err, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.wantErr) {
|
||||
t.Errorf("ToCollection() received error %v, wanted error %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToCollection() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollection_Equals(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fields Collection
|
||||
item Item
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "collection with two items",
|
||||
fields: Collection{
|
||||
ID: "https://example.com/1",
|
||||
Type: CollectionType,
|
||||
First: IRI("https://example.com/1?first"),
|
||||
Items: ItemCollection{
|
||||
Object{ID: "https://example.com/1/1", Type: NoteType},
|
||||
Object{ID: "https://example.com/1/3", Type: ImageType},
|
||||
},
|
||||
},
|
||||
item: &Collection{
|
||||
ID: "https://example.com/1",
|
||||
Type: CollectionType,
|
||||
First: IRI("https://example.com/1?first"),
|
||||
Items: ItemCollection{
|
||||
Object{ID: "https://example.com/1/1", Type: NoteType},
|
||||
Object{ID: "https://example.com/1/3", Type: ImageType},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.fields.Equals(tt.item); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
234
copy.go
Normal file
234
copy.go
Normal file
|
@ -0,0 +1,234 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func CopyOrderedCollectionPageProperties(to, from *OrderedCollectionPage) (*OrderedCollectionPage, error) {
|
||||
to.PartOf = replaceIfItem(to.PartOf, from.PartOf)
|
||||
to.Next = replaceIfItem(to.Next, from.Next)
|
||||
to.Prev = replaceIfItem(to.Prev, from.Prev)
|
||||
oldCol, _ := ToOrderedCollection(to)
|
||||
newCol, _ := ToOrderedCollection(from)
|
||||
_, err := CopyOrderedCollectionProperties(oldCol, newCol)
|
||||
if err != nil {
|
||||
return to, err
|
||||
}
|
||||
return to, nil
|
||||
}
|
||||
|
||||
func CopyCollectionPageProperties(to, from *CollectionPage) (*CollectionPage, error) {
|
||||
to.PartOf = replaceIfItem(to.PartOf, from.PartOf)
|
||||
to.Next = replaceIfItem(to.Next, from.Next)
|
||||
to.Prev = replaceIfItem(to.Prev, from.Prev)
|
||||
toCol, _ := ToCollection(to)
|
||||
fromCol, _ := ToCollection(from)
|
||||
_, err := CopyCollectionProperties(toCol, fromCol)
|
||||
return to, err
|
||||
}
|
||||
|
||||
func CopyOrderedCollectionProperties(to, from *OrderedCollection) (*OrderedCollection, error) {
|
||||
to.First = replaceIfItem(to.First, from.First)
|
||||
to.Last = replaceIfItem(to.Last, from.Last)
|
||||
to.OrderedItems = replaceIfItemCollection(to.OrderedItems, from.OrderedItems)
|
||||
if to.TotalItems == 0 {
|
||||
to.TotalItems = from.TotalItems
|
||||
}
|
||||
oldOb, _ := ToObject(to)
|
||||
newOb, _ := ToObject(from)
|
||||
_, err := CopyObjectProperties(oldOb, newOb)
|
||||
return to, err
|
||||
}
|
||||
|
||||
func CopyCollectionProperties(to, from *Collection) (*Collection, error) {
|
||||
to.First = replaceIfItem(to.First, from.First)
|
||||
to.Last = replaceIfItem(to.Last, from.Last)
|
||||
to.Items = replaceIfItemCollection(to.Items, from.Items)
|
||||
if to.TotalItems == 0 {
|
||||
to.TotalItems = from.TotalItems
|
||||
}
|
||||
oldOb, _ := ToObject(to)
|
||||
newOb, _ := ToObject(from)
|
||||
_, err := CopyObjectProperties(oldOb, newOb)
|
||||
return to, err
|
||||
}
|
||||
|
||||
// CopyObjectProperties updates the "old" object properties with the "new's"
|
||||
// Including ID and Type
|
||||
func CopyObjectProperties(to, from *Object) (*Object, error) {
|
||||
to.ID = from.ID
|
||||
to.Type = from.Type
|
||||
to.Name = replaceIfNaturalLanguageValues(to.Name, from.Name)
|
||||
to.Attachment = replaceIfItem(to.Attachment, from.Attachment)
|
||||
to.AttributedTo = replaceIfItem(to.AttributedTo, from.AttributedTo)
|
||||
to.Audience = replaceIfItemCollection(to.Audience, from.Audience)
|
||||
to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content)
|
||||
to.Context = replaceIfItem(to.Context, from.Context)
|
||||
if len(from.MediaType) > 0 {
|
||||
to.MediaType = from.MediaType
|
||||
}
|
||||
if !from.EndTime.IsZero() {
|
||||
to.EndTime = from.EndTime
|
||||
}
|
||||
to.Generator = replaceIfItem(to.Generator, from.Generator)
|
||||
to.Icon = replaceIfItem(to.Icon, from.Icon)
|
||||
to.Image = replaceIfItem(to.Image, from.Image)
|
||||
to.InReplyTo = replaceIfItem(to.InReplyTo, from.InReplyTo)
|
||||
to.Location = replaceIfItem(to.Location, from.Location)
|
||||
to.Preview = replaceIfItem(to.Preview, from.Preview)
|
||||
if to.Published.IsZero() && !from.Published.IsZero() {
|
||||
to.Published = from.Published
|
||||
}
|
||||
if to.Updated.IsZero() && !from.Updated.IsZero() {
|
||||
to.Updated = from.Updated
|
||||
}
|
||||
to.Replies = replaceIfItem(to.Replies, from.Replies)
|
||||
if !from.StartTime.IsZero() {
|
||||
to.StartTime = from.StartTime
|
||||
}
|
||||
to.Summary = replaceIfNaturalLanguageValues(to.Summary, from.Summary)
|
||||
to.Tag = replaceIfItemCollection(to.Tag, from.Tag)
|
||||
if from.URL != nil {
|
||||
to.URL = from.URL
|
||||
}
|
||||
to.To = replaceIfItemCollection(to.To, from.To)
|
||||
to.Bto = replaceIfItemCollection(to.Bto, from.Bto)
|
||||
to.CC = replaceIfItemCollection(to.CC, from.CC)
|
||||
to.BCC = replaceIfItemCollection(to.BCC, from.BCC)
|
||||
if from.Duration == 0 {
|
||||
to.Duration = from.Duration
|
||||
}
|
||||
to.Source = replaceIfSource(to.Source, from.Source)
|
||||
return to, nil
|
||||
}
|
||||
|
||||
func copyAllItemProperties(to, from Item) (Item, error) {
|
||||
if CollectionType == to.GetType() {
|
||||
o, err := ToCollection(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToCollection(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return CopyCollectionProperties(o, n)
|
||||
}
|
||||
if CollectionPageType == to.GetType() {
|
||||
o, err := ToCollectionPage(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToCollectionPage(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return CopyCollectionPageProperties(o, n)
|
||||
}
|
||||
if OrderedCollectionType == to.GetType() {
|
||||
o, err := ToOrderedCollection(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToOrderedCollection(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return CopyOrderedCollectionProperties(o, n)
|
||||
}
|
||||
if OrderedCollectionPageType == to.GetType() {
|
||||
o, err := ToOrderedCollectionPage(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToOrderedCollectionPage(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return CopyOrderedCollectionPageProperties(o, n)
|
||||
}
|
||||
if ActorTypes.Contains(to.GetType()) {
|
||||
o, err := ToActor(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToActor(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return UpdatePersonProperties(o, n)
|
||||
}
|
||||
if ObjectTypes.Contains(to.GetType()) || to.GetType() == "" {
|
||||
o, err := ToObject(to)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
n, err := ToObject(from)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
return CopyObjectProperties(o, n)
|
||||
}
|
||||
return to, fmt.Errorf("could not process objects with type %s", to.GetType())
|
||||
}
|
||||
|
||||
// CopyItemProperties delegates to the correct per type functions for copying
|
||||
// properties between matching Activity Objects
|
||||
func CopyItemProperties(to, from Item) (Item, error) {
|
||||
if to == nil {
|
||||
return to, fmt.Errorf("nil object to update")
|
||||
}
|
||||
if from == nil {
|
||||
return to, fmt.Errorf("nil object for update")
|
||||
}
|
||||
if !to.GetLink().Equals(from.GetLink(), false) {
|
||||
return to, fmt.Errorf("object IDs don't match")
|
||||
}
|
||||
if to.GetType() != "" && to.GetType() != from.GetType() {
|
||||
return to, fmt.Errorf("invalid object types for update %s(old) and %s(new)", from.GetType(), to.GetType())
|
||||
}
|
||||
return copyAllItemProperties(to, from)
|
||||
}
|
||||
|
||||
// UpdatePersonProperties
|
||||
func UpdatePersonProperties(to, from *Actor) (*Actor, error) {
|
||||
to.Inbox = replaceIfItem(to.Inbox, from.Inbox)
|
||||
to.Outbox = replaceIfItem(to.Outbox, from.Outbox)
|
||||
to.Following = replaceIfItem(to.Following, from.Following)
|
||||
to.Followers = replaceIfItem(to.Followers, from.Followers)
|
||||
to.Liked = replaceIfItem(to.Liked, from.Liked)
|
||||
to.PreferredUsername = replaceIfNaturalLanguageValues(to.PreferredUsername, from.PreferredUsername)
|
||||
oldOb, _ := ToObject(to)
|
||||
newOb, _ := ToObject(from)
|
||||
_, err := CopyObjectProperties(oldOb, newOb)
|
||||
return to, err
|
||||
}
|
||||
|
||||
func replaceIfItem(old, new Item) Item {
|
||||
if new == nil {
|
||||
return old
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
func replaceIfItemCollection(old, new ItemCollection) ItemCollection {
|
||||
if new == nil {
|
||||
return old
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
func replaceIfNaturalLanguageValues(old, new NaturalLanguageValues) NaturalLanguageValues {
|
||||
if new == nil {
|
||||
return old
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
func replaceIfSource(to, from Source) Source {
|
||||
if from.MediaType != to.MediaType {
|
||||
return from
|
||||
}
|
||||
to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content)
|
||||
return to
|
||||
}
|
722
decoding_gob.go
Normal file
722
decoding_gob.go
Normal file
|
@ -0,0 +1,722 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GobDecode(data []byte) (Item, error) {
|
||||
return gobDecodeItem(data)
|
||||
}
|
||||
|
||||
func gobDecodeUint(i *uint, data []byte) error {
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
return g.Decode(i)
|
||||
}
|
||||
|
||||
func gobDecodeFloat64(f *float64, data []byte) error {
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
return g.Decode(f)
|
||||
}
|
||||
|
||||
func gobDecodeInt64(i *int64, data []byte) error {
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
return g.Decode(i)
|
||||
}
|
||||
|
||||
func gobDecodeBool(b *bool, data []byte) error {
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
return g.Decode(b)
|
||||
}
|
||||
|
||||
func unmapActorProperties(mm map[string][]byte, a *Actor) error {
|
||||
err := OnObject(a, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["inbox"]; ok {
|
||||
if a.Inbox, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["outbox"]; ok {
|
||||
if a.Outbox, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["following"]; ok {
|
||||
if a.Following, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["followers"]; ok {
|
||||
if a.Followers, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["liked"]; ok {
|
||||
if a.Liked, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["preferredUsername"]; ok {
|
||||
if a.PreferredUsername, err = gobDecodeNaturalLanguageValues(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["endpoints"]; ok {
|
||||
if err = a.Endpoints.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["streams"]; ok {
|
||||
if a.Streams, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["publicKey"]; ok {
|
||||
if err = a.PublicKey.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapIntransitiveActivityProperties(mm map[string][]byte, act *IntransitiveActivity) error {
|
||||
err := OnObject(act, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["actor"]; ok {
|
||||
if act.Actor, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["target"]; ok {
|
||||
if act.Target, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["result"]; ok {
|
||||
if act.Result, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["origin"]; ok {
|
||||
if act.Origin, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["instrument"]; ok {
|
||||
if act.Instrument, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapActivityProperties(mm map[string][]byte, act *Activity) error {
|
||||
err := OnIntransitiveActivity(act, func(act *IntransitiveActivity) error {
|
||||
return unmapIntransitiveActivityProperties(mm, act)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["object"]; ok {
|
||||
if act.Object, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapLinkProperties(mm map[string][]byte, l *Link) error {
|
||||
if raw, ok := mm["id"]; ok {
|
||||
if err := l.ID.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["type"]; ok {
|
||||
if err := l.Type.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["mediaType"]; ok {
|
||||
if err := l.MediaType.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["href"]; ok {
|
||||
if err := l.Href.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["hrefLang"]; ok {
|
||||
if err := l.HrefLang.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["name"]; ok {
|
||||
if err := l.Name.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["rel"]; ok {
|
||||
if err := l.Rel.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["width"]; ok {
|
||||
if err := gobDecodeUint(&l.Width, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["height"]; ok {
|
||||
if err := gobDecodeUint(&l.Height, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapObjectProperties(mm map[string][]byte, o *Object) error {
|
||||
var err error
|
||||
if raw, ok := mm["id"]; ok {
|
||||
if err = o.ID.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["type"]; ok {
|
||||
if err = o.Type.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["name"]; ok {
|
||||
if err = o.Name.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["attachment"]; ok {
|
||||
if o.Attachment, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["attributedTo"]; ok {
|
||||
if o.AttributedTo, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["audience"]; ok {
|
||||
if o.Audience, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["content"]; ok {
|
||||
if o.Content, err = gobDecodeNaturalLanguageValues(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["context"]; ok {
|
||||
if o.Context, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["mediaType"]; ok {
|
||||
if err = o.MediaType.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["endTime"]; ok {
|
||||
if err = o.EndTime.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["generator"]; ok {
|
||||
if o.Generator, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["icon"]; ok {
|
||||
if o.Icon, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["image"]; ok {
|
||||
if o.Image, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["inReplyTo"]; ok {
|
||||
if o.InReplyTo, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["location"]; ok {
|
||||
if o.Location, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["preview"]; ok {
|
||||
if o.Preview, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["published"]; ok {
|
||||
if err = o.Published.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["replies"]; ok {
|
||||
if o.Replies, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["startTime"]; ok {
|
||||
if err = o.StartTime.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["summary"]; ok {
|
||||
if o.Summary, err = gobDecodeNaturalLanguageValues(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["tag"]; ok {
|
||||
if o.Tag, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["updated"]; ok {
|
||||
if err = o.Updated.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["url"]; ok {
|
||||
if o.URL, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["to"]; ok {
|
||||
if o.To, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["bto"]; ok {
|
||||
if o.Bto, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["cc"]; ok {
|
||||
if o.CC, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["bcc"]; ok {
|
||||
if o.BCC, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["duration"]; ok {
|
||||
if o.Duration, err = gobDecodeDuration(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["likes"]; ok {
|
||||
if o.Likes, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["shares"]; ok {
|
||||
if o.Shares, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["source"]; ok {
|
||||
if err := o.Source.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryDecodeItems(items *ItemCollection, data []byte) error {
|
||||
tt := make([][]byte, 0)
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := g.Decode(&tt); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, it := range tt {
|
||||
ob, err := gobDecodeItem(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*items = append(*items, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryDecodeIRIs(iris *IRIs, data []byte) error {
|
||||
return iris.GobDecode(data)
|
||||
}
|
||||
|
||||
func tryDecodeIRI(iri *IRI, data []byte) error {
|
||||
return iri.GobDecode(data)
|
||||
}
|
||||
|
||||
func gobDecodeDuration(data []byte) (time.Duration, error) {
|
||||
var d time.Duration
|
||||
err := gob.NewDecoder(bytes.NewReader(data)).Decode(&d)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func gobDecodeNaturalLanguageValues(data []byte) (NaturalLanguageValues, error) {
|
||||
n := make(NaturalLanguageValues, 0)
|
||||
err := n.GobDecode(data)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func gobDecodeItems(data []byte) (ItemCollection, error) {
|
||||
items := make(ItemCollection, 0)
|
||||
if err := tryDecodeItems(&items, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func gobDecodeItem(data []byte) (Item, error) {
|
||||
items := make(ItemCollection, 0)
|
||||
if err := tryDecodeItems(&items, data); err == nil {
|
||||
return items, nil
|
||||
}
|
||||
iris := make(IRIs, 0)
|
||||
if err := tryDecodeIRIs(&iris, data); err == nil {
|
||||
return iris, nil
|
||||
}
|
||||
isObject := false
|
||||
typ := ActivityVocabularyType("")
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err == nil {
|
||||
var sTyp []byte
|
||||
sTyp, isObject = mm["type"]
|
||||
if isObject {
|
||||
typ = ActivityVocabularyType(sTyp)
|
||||
} else {
|
||||
_, isObject = mm["id"]
|
||||
}
|
||||
}
|
||||
if isObject {
|
||||
it, err := ItemTyperFunc(typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch it.GetType() {
|
||||
case IRIType:
|
||||
case "", ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType:
|
||||
err = OnObject(it, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
case LinkType, MentionType:
|
||||
err = OnLink(it, func(l *Link) error {
|
||||
return unmapLinkProperties(mm, l)
|
||||
})
|
||||
case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType,
|
||||
FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType,
|
||||
RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType:
|
||||
err = OnActivity(it, func(act *Activity) error {
|
||||
return unmapActivityProperties(mm, act)
|
||||
})
|
||||
case IntransitiveActivityType, ArriveType, TravelType:
|
||||
err = OnIntransitiveActivity(it, func(act *IntransitiveActivity) error {
|
||||
return unmapIntransitiveActivityProperties(mm, act)
|
||||
})
|
||||
case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType:
|
||||
err = OnActor(it, func(a *Actor) error {
|
||||
return unmapActorProperties(mm, a)
|
||||
})
|
||||
case CollectionType:
|
||||
err = OnCollection(it, func(c *Collection) error {
|
||||
return unmapCollectionProperties(mm, c)
|
||||
})
|
||||
case OrderedCollectionType:
|
||||
err = OnOrderedCollection(it, func(c *OrderedCollection) error {
|
||||
return unmapOrderedCollectionProperties(mm, c)
|
||||
})
|
||||
case CollectionPageType:
|
||||
err = OnCollectionPage(it, func(p *CollectionPage) error {
|
||||
return unmapCollectionPageProperties(mm, p)
|
||||
})
|
||||
case OrderedCollectionPageType:
|
||||
err = OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error {
|
||||
return unmapOrderedCollectionPageProperties(mm, p)
|
||||
})
|
||||
case PlaceType:
|
||||
err = OnPlace(it, func(p *Place) error {
|
||||
return unmapPlaceProperties(mm, p)
|
||||
})
|
||||
case ProfileType:
|
||||
err = OnProfile(it, func(p *Profile) error {
|
||||
return unmapProfileProperties(mm, p)
|
||||
})
|
||||
case RelationshipType:
|
||||
err = OnRelationship(it, func(r *Relationship) error {
|
||||
return unmapRelationshipProperties(mm, r)
|
||||
})
|
||||
case TombstoneType:
|
||||
err = OnTombstone(it, func(t *Tombstone) error {
|
||||
return unmapTombstoneProperties(mm, t)
|
||||
})
|
||||
case QuestionType:
|
||||
err = OnQuestion(it, func(q *Question) error {
|
||||
return unmapQuestionProperties(mm, q)
|
||||
})
|
||||
}
|
||||
return it, err
|
||||
}
|
||||
iri := IRI("")
|
||||
if err := tryDecodeIRI(&iri, data); err == nil {
|
||||
return iri, err
|
||||
}
|
||||
|
||||
return nil, errors.New("unable to gob decode to any known ActivityPub types")
|
||||
}
|
||||
|
||||
func gobDecodeObjectAsMap(data []byte) (map[string][]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := g.Decode(&mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mm, nil
|
||||
}
|
||||
|
||||
func unmapIncompleteCollectionProperties(mm map[string][]byte, c *Collection) error {
|
||||
err := OnObject(c, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["current"]; ok {
|
||||
if c.Current, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["first"]; ok {
|
||||
if c.First, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["last"]; ok {
|
||||
if c.Last, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["totalItems"]; ok {
|
||||
if err = gobDecodeUint(&c.TotalItems, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapCollectionProperties(mm map[string][]byte, c *Collection) error {
|
||||
err := unmapIncompleteCollectionProperties(mm, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["items"]; ok {
|
||||
if c.Items, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmapCollectionPageProperties(mm map[string][]byte, c *CollectionPage) error {
|
||||
err := OnCollection(c, func(c *Collection) error {
|
||||
return unmapCollectionProperties(mm, c)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["partOf"]; ok {
|
||||
if c.PartOf, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["next"]; ok {
|
||||
if c.Next, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["prev"]; ok {
|
||||
if c.Prev, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmapOrderedCollectionProperties(mm map[string][]byte, o *OrderedCollection) error {
|
||||
err := OnCollection(o, func(c *Collection) error {
|
||||
return unmapIncompleteCollectionProperties(mm, c)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["orderedItems"]; ok {
|
||||
if o.OrderedItems, err = gobDecodeItems(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmapOrderedCollectionPageProperties(mm map[string][]byte, c *OrderedCollectionPage) error {
|
||||
err := OnOrderedCollection(c, func(c *OrderedCollection) error {
|
||||
return unmapOrderedCollectionProperties(mm, c)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["partOf"]; ok {
|
||||
if c.PartOf, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["next"]; ok {
|
||||
if c.Next, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["prev"]; ok {
|
||||
if c.Prev, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unmapPlaceProperties(mm map[string][]byte, p *Place) error {
|
||||
err := OnObject(p, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["accuracy"]; ok {
|
||||
if err = gobDecodeFloat64(&p.Accuracy, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["altitude"]; ok {
|
||||
if err = gobDecodeFloat64(&p.Altitude, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["latitude"]; ok {
|
||||
if err = gobDecodeFloat64(&p.Latitude, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["radius"]; ok {
|
||||
if err = gobDecodeInt64(&p.Radius, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["units"]; ok {
|
||||
p.Units = string(raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapProfileProperties(mm map[string][]byte, p *Profile) error {
|
||||
err := OnObject(p, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["Describes"]; ok {
|
||||
if p.Describes, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapRelationshipProperties(mm map[string][]byte, r *Relationship) error {
|
||||
err := OnObject(r, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["subject"]; ok {
|
||||
if r.Subject, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["object"]; ok {
|
||||
if r.Object, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["relationship"]; ok {
|
||||
if r.Relationship, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapTombstoneProperties(mm map[string][]byte, t *Tombstone) error {
|
||||
err := OnObject(t, func(ob *Object) error {
|
||||
return unmapObjectProperties(mm, ob)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["formerType"]; ok {
|
||||
if err = t.FormerType.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["deleted"]; ok {
|
||||
if err = t.Deleted.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmapQuestionProperties(mm map[string][]byte, q *Question) error {
|
||||
err := OnIntransitiveActivity(q, func(act *IntransitiveActivity) error {
|
||||
return unmapIntransitiveActivityProperties(mm, act)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["oneOf"]; ok {
|
||||
if q.OneOf, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["anyOf"]; ok {
|
||||
if q.AnyOf, err = gobDecodeItem(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["closed"]; ok {
|
||||
if err = gobDecodeBool(&q.Closed, raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
60
decoding_gob_test.go
Normal file
60
decoding_gob_test.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package activitypub
|
||||
|
||||
/*
|
||||
func TestGobEncode(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GobEncode(tt.args.it)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalGob(t *testing.T) {
|
||||
type args struct {
|
||||
data []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := UnmarshalGob(tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalGob() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("UnmarshalGob() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
662
decoding_json.go
Normal file
662
decoding_json.go
Normal file
|
@ -0,0 +1,662 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
var (
|
||||
apUnmarshalerType = reflect.TypeOf(new(Item)).Elem()
|
||||
unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem()
|
||||
textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
|
||||
)
|
||||
|
||||
// ItemTyperFunc will return an instance of a struct that implements activitypub.Item
|
||||
// The default for this package is GetItemByType but can be overwritten
|
||||
var ItemTyperFunc TyperFn = GetItemByType
|
||||
|
||||
// JSONItemUnmarshal can be set externally to populate a custom object based on its type
|
||||
var JSONItemUnmarshal JSONUnmarshalerFn = nil
|
||||
|
||||
// IsNotEmpty checks if an object is empty
|
||||
var IsNotEmpty NotEmptyCheckerFn = NotEmpty
|
||||
|
||||
// TyperFn is the type of the function which returns an Item struct instance
|
||||
// for a specific ActivityVocabularyType
|
||||
type TyperFn func(ActivityVocabularyType) (Item, error)
|
||||
|
||||
// JSONUnmarshalerFn is the type of the function that will load the data from a fastjson.Value into an Item
|
||||
// that the current package doesn't know about.
|
||||
type JSONUnmarshalerFn func(ActivityVocabularyType, *fastjson.Value, Item) error
|
||||
|
||||
// NotEmptyCheckerFn is the type of the function that checks if an object is empty
|
||||
type NotEmptyCheckerFn func(Item) bool
|
||||
|
||||
func JSONGetID(val *fastjson.Value) ID {
|
||||
i := val.Get("id").GetStringBytes()
|
||||
return ID(i)
|
||||
}
|
||||
|
||||
func JSONGetType(val *fastjson.Value) ActivityVocabularyType {
|
||||
t := val.Get("type").GetStringBytes()
|
||||
return ActivityVocabularyType(t)
|
||||
}
|
||||
|
||||
func JSONGetMimeType(val *fastjson.Value, prop string) MimeType {
|
||||
if !val.Exists(prop) {
|
||||
return ""
|
||||
}
|
||||
t := val.GetStringBytes(prop)
|
||||
return MimeType(t)
|
||||
}
|
||||
|
||||
func JSONGetInt(val *fastjson.Value, prop string) int64 {
|
||||
if !val.Exists(prop) {
|
||||
return 0
|
||||
}
|
||||
i := val.Get(prop).GetInt64()
|
||||
return i
|
||||
}
|
||||
|
||||
func JSONGetFloat(val *fastjson.Value, prop string) float64 {
|
||||
if !val.Exists(prop) {
|
||||
return 0.0
|
||||
}
|
||||
f := val.Get(prop).GetFloat64()
|
||||
return f
|
||||
}
|
||||
|
||||
func JSONGetString(val *fastjson.Value, prop string) string {
|
||||
if !val.Exists(prop) {
|
||||
return ""
|
||||
}
|
||||
s := val.Get(prop).GetStringBytes()
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func JSONGetBytes(val *fastjson.Value, prop string) []byte {
|
||||
if !val.Exists(prop) {
|
||||
return nil
|
||||
}
|
||||
s := val.Get(prop).GetStringBytes()
|
||||
return s
|
||||
}
|
||||
|
||||
func JSONGetBoolean(val *fastjson.Value, prop string) bool {
|
||||
if !val.Exists(prop) {
|
||||
return false
|
||||
}
|
||||
t, _ := val.Get(prop).Bool()
|
||||
return t
|
||||
}
|
||||
|
||||
func JSONGetNaturalLanguageField(val *fastjson.Value, prop string) NaturalLanguageValues {
|
||||
n := NaturalLanguageValues{}
|
||||
if val == nil {
|
||||
return n
|
||||
}
|
||||
v := val.Get(prop)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
switch v.Type() {
|
||||
case fastjson.TypeObject:
|
||||
ob, _ := v.Object()
|
||||
ob.Visit(func(key []byte, v *fastjson.Value) {
|
||||
l := LangRefValue{}
|
||||
l.Ref = LangRef(key)
|
||||
if err := l.Value.UnmarshalJSON(v.GetStringBytes()); err == nil {
|
||||
if l.Ref != NilLangRef || len(l.Value) > 0 {
|
||||
n = append(n, l)
|
||||
}
|
||||
}
|
||||
})
|
||||
case fastjson.TypeString:
|
||||
l := LangRefValue{}
|
||||
if err := l.UnmarshalJSON(v.GetStringBytes()); err == nil {
|
||||
n = append(n, l)
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func JSONGetTime(val *fastjson.Value, prop string) time.Time {
|
||||
t := time.Time{}
|
||||
if val == nil {
|
||||
return t
|
||||
}
|
||||
|
||||
if str := val.Get(prop).GetStringBytes(); len(str) > 0 {
|
||||
t.UnmarshalText(str)
|
||||
return t.UTC()
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func JSONGetDuration(val *fastjson.Value, prop string) time.Duration {
|
||||
if str := val.Get(prop).GetStringBytes(); len(str) > 0 {
|
||||
// TODO(marius): this needs to be replaced to be compatible with xsd:duration
|
||||
d, _ := time.ParseDuration(string(str))
|
||||
return d
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func JSONGetPublicKey(val *fastjson.Value, prop string) PublicKey {
|
||||
key := PublicKey{}
|
||||
if val == nil {
|
||||
return key
|
||||
}
|
||||
val = val.Get(prop)
|
||||
if val == nil {
|
||||
return key
|
||||
}
|
||||
JSONLoadPublicKey(val, &key)
|
||||
return key
|
||||
}
|
||||
|
||||
func JSONItemsFn(val *fastjson.Value) (Item, error) {
|
||||
if val.Type() == fastjson.TypeArray {
|
||||
it := val.GetArray()
|
||||
items := make(ItemCollection, 0)
|
||||
for _, v := range it {
|
||||
if it, _ := JSONLoadItem(v); it != nil {
|
||||
items.Append(it)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
return JSONLoadItem(val)
|
||||
}
|
||||
|
||||
func JSONLoadItem(val *fastjson.Value) (Item, error) {
|
||||
typ := JSONGetType(val)
|
||||
if typ == "" && val.Type() == fastjson.TypeString {
|
||||
// try to see if it's an IRI
|
||||
if i, ok := asIRI(val); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
i, err := ItemTyperFunc(typ)
|
||||
if err != nil || IsNil(i) {
|
||||
return nil, nil
|
||||
}
|
||||
var empty = func(i Item) bool { return !IsNotEmpty(i) }
|
||||
|
||||
switch typ {
|
||||
case "":
|
||||
// NOTE(marius): this handles Tags which usually don't have types
|
||||
fallthrough
|
||||
case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType:
|
||||
err = OnObject(i, func(ob *Object) error {
|
||||
return JSONLoadObject(val, ob)
|
||||
})
|
||||
case LinkType, MentionType:
|
||||
err = OnLink(i, func(l *Link) error {
|
||||
return JSONLoadLink(val, l)
|
||||
})
|
||||
case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType,
|
||||
FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType,
|
||||
RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType:
|
||||
err = OnActivity(i, func(act *Activity) error {
|
||||
return JSONLoadActivity(val, act)
|
||||
})
|
||||
case IntransitiveActivityType, ArriveType, TravelType:
|
||||
err = OnIntransitiveActivity(i, func(act *IntransitiveActivity) error {
|
||||
return JSONLoadIntransitiveActivity(val, act)
|
||||
})
|
||||
case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType:
|
||||
err = OnActor(i, func(a *Actor) error {
|
||||
return JSONLoadActor(val, a)
|
||||
})
|
||||
case CollectionType:
|
||||
err = OnCollection(i, func(c *Collection) error {
|
||||
return JSONLoadCollection(val, c)
|
||||
})
|
||||
case OrderedCollectionType:
|
||||
err = OnOrderedCollection(i, func(c *OrderedCollection) error {
|
||||
return JSONLoadOrderedCollection(val, c)
|
||||
})
|
||||
case CollectionPageType:
|
||||
err = OnCollectionPage(i, func(p *CollectionPage) error {
|
||||
return JSONLoadCollectionPage(val, p)
|
||||
})
|
||||
case OrderedCollectionPageType:
|
||||
err = OnOrderedCollectionPage(i, func(p *OrderedCollectionPage) error {
|
||||
return JSONLoadOrderedCollectionPage(val, p)
|
||||
})
|
||||
case PlaceType:
|
||||
err = OnPlace(i, func(p *Place) error {
|
||||
return JSONLoadPlace(val, p)
|
||||
})
|
||||
case ProfileType:
|
||||
err = OnProfile(i, func(p *Profile) error {
|
||||
return JSONLoadProfile(val, p)
|
||||
})
|
||||
case RelationshipType:
|
||||
err = OnRelationship(i, func(r *Relationship) error {
|
||||
return JSONLoadRelationship(val, r)
|
||||
})
|
||||
case TombstoneType:
|
||||
err = OnTombstone(i, func(t *Tombstone) error {
|
||||
return JSONLoadTombstone(val, t)
|
||||
})
|
||||
case QuestionType:
|
||||
err = OnQuestion(i, func(q *Question) error {
|
||||
return JSONLoadQuestion(val, q)
|
||||
})
|
||||
default:
|
||||
if JSONItemUnmarshal == nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal custom type %s, you need to set a correct function for JSONItemUnmarshal", typ)
|
||||
}
|
||||
err = JSONItemUnmarshal(typ, val, i)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if empty(i) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func JSONUnmarshalToItem(val *fastjson.Value) Item {
|
||||
var (
|
||||
i Item
|
||||
err error
|
||||
)
|
||||
switch val.Type() {
|
||||
case fastjson.TypeArray:
|
||||
i, err = JSONItemsFn(val)
|
||||
case fastjson.TypeObject:
|
||||
i, err = JSONLoadItem(val)
|
||||
case fastjson.TypeString:
|
||||
if iri, ok := asIRI(val); ok {
|
||||
// try to see if it's an IRI
|
||||
i = iri
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func asIRI(val *fastjson.Value) (IRI, bool) {
|
||||
if val == nil {
|
||||
return NilIRI, true
|
||||
}
|
||||
s := strings.Trim(val.String(), `"`)
|
||||
u, err := url.ParseRequestURI(s)
|
||||
if err == nil && len(u.Scheme) > 0 && len(u.Host) > 0 {
|
||||
// try to see if it's an IRI
|
||||
return IRI(s), true
|
||||
}
|
||||
return EmptyIRI, false
|
||||
}
|
||||
|
||||
func JSONGetItem(val *fastjson.Value, prop string) Item {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
if val = val.Get(prop); val == nil {
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeString:
|
||||
if i, ok := asIRI(val); ok {
|
||||
// try to see if it's an IRI
|
||||
return i
|
||||
}
|
||||
case fastjson.TypeArray:
|
||||
it, _ := JSONItemsFn(val)
|
||||
return it
|
||||
case fastjson.TypeObject:
|
||||
it, _ := JSONLoadItem(val)
|
||||
return it
|
||||
case fastjson.TypeNumber:
|
||||
fallthrough
|
||||
case fastjson.TypeNull:
|
||||
fallthrough
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func JSONGetURIItem(val *fastjson.Value, prop string) Item {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
if val = val.Get(prop); val == nil {
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeObject:
|
||||
if it, _ := JSONLoadItem(val); it != nil {
|
||||
return it
|
||||
}
|
||||
case fastjson.TypeArray:
|
||||
if it, _ := JSONItemsFn(val); it != nil {
|
||||
return it
|
||||
}
|
||||
case fastjson.TypeString:
|
||||
return IRI(val.GetStringBytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func JSONGetItems(val *fastjson.Value, prop string) ItemCollection {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
if val = val.Get(prop); val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
it := make(ItemCollection, 0)
|
||||
switch val.Type() {
|
||||
case fastjson.TypeArray:
|
||||
for _, v := range val.GetArray() {
|
||||
if i, _ := JSONLoadItem(v); i != nil {
|
||||
it.Append(i)
|
||||
}
|
||||
}
|
||||
case fastjson.TypeObject:
|
||||
if i := JSONGetItem(val, prop); i != nil {
|
||||
it.Append(i)
|
||||
}
|
||||
case fastjson.TypeString:
|
||||
if iri := val.GetStringBytes(); len(iri) > 0 {
|
||||
it.Append(IRI(iri))
|
||||
}
|
||||
}
|
||||
if len(it) == 0 {
|
||||
return nil
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func JSONGetLangRefField(val *fastjson.Value, prop string) LangRef {
|
||||
s := val.Get(prop).GetStringBytes()
|
||||
return LangRef(s)
|
||||
}
|
||||
|
||||
func JSONGetIRI(val *fastjson.Value, prop string) IRI {
|
||||
s := val.Get(prop).GetStringBytes()
|
||||
return IRI(s)
|
||||
}
|
||||
|
||||
// UnmarshalJSON tries to detect the type of the object in the json data and then outputs a matching
|
||||
// ActivityStreams object, if possible
|
||||
func UnmarshalJSON(data []byte) (Item, error) {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return JSONUnmarshalToItem(val), nil
|
||||
}
|
||||
|
||||
func GetItemByType(typ ActivityVocabularyType) (Item, error) {
|
||||
switch typ {
|
||||
case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType:
|
||||
return ObjectNew(typ), nil
|
||||
case LinkType, MentionType:
|
||||
return &Link{Type: typ}, nil
|
||||
case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType,
|
||||
FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType,
|
||||
RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType:
|
||||
return &Activity{Type: typ}, nil
|
||||
case IntransitiveActivityType, ArriveType, TravelType:
|
||||
return &IntransitiveActivity{Type: typ}, nil
|
||||
case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType:
|
||||
return &Actor{Type: typ}, nil
|
||||
case CollectionType:
|
||||
return &Collection{Type: typ}, nil
|
||||
case OrderedCollectionType:
|
||||
return &OrderedCollection{Type: typ}, nil
|
||||
case CollectionPageType:
|
||||
return &CollectionPage{Type: typ}, nil
|
||||
case OrderedCollectionPageType:
|
||||
return &OrderedCollectionPage{Type: typ}, nil
|
||||
case PlaceType:
|
||||
return &Place{Type: typ}, nil
|
||||
case ProfileType:
|
||||
return &Profile{Type: typ}, nil
|
||||
case RelationshipType:
|
||||
return &Relationship{Type: typ}, nil
|
||||
case TombstoneType:
|
||||
return &Tombstone{Type: typ}, nil
|
||||
case QuestionType:
|
||||
return &Question{Type: typ}, nil
|
||||
case "":
|
||||
fallthrough
|
||||
default:
|
||||
// when no type is available use a plain Object
|
||||
return &Object{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("empty ActivityStreams type")
|
||||
}
|
||||
|
||||
func JSONGetActorEndpoints(val *fastjson.Value, prop string) *Endpoints {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
if val = val.Get(prop); val == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
e := Endpoints{}
|
||||
e.UploadMedia = JSONGetURIItem(val, "uploadMedia")
|
||||
e.OauthAuthorizationEndpoint = JSONGetURIItem(val, "oauthAuthorizationEndpoint")
|
||||
e.OauthTokenEndpoint = JSONGetURIItem(val, "oauthTokenEndpoint")
|
||||
e.SharedInbox = JSONGetURIItem(val, "sharedInbox")
|
||||
e.ProvideClientKey = JSONGetURIItem(val, "provideClientKey")
|
||||
e.SignClientKey = JSONGetURIItem(val, "signClientKey")
|
||||
|
||||
return &e
|
||||
}
|
||||
|
||||
func JSONLoadObject(val *fastjson.Value, o *Object) error {
|
||||
o.ID = JSONGetID(val)
|
||||
o.Type = JSONGetType(val)
|
||||
o.Name = JSONGetNaturalLanguageField(val, "name")
|
||||
o.Content = JSONGetNaturalLanguageField(val, "content")
|
||||
o.Summary = JSONGetNaturalLanguageField(val, "summary")
|
||||
o.Context = JSONGetItem(val, "context")
|
||||
o.URL = JSONGetURIItem(val, "url")
|
||||
o.MediaType = JSONGetMimeType(val, "mediaType")
|
||||
o.Generator = JSONGetItem(val, "generator")
|
||||
o.AttributedTo = JSONGetItem(val, "attributedTo")
|
||||
o.Attachment = JSONGetItem(val, "attachment")
|
||||
o.Location = JSONGetItem(val, "location")
|
||||
o.Published = JSONGetTime(val, "published")
|
||||
o.StartTime = JSONGetTime(val, "startTime")
|
||||
o.EndTime = JSONGetTime(val, "endTime")
|
||||
o.Duration = JSONGetDuration(val, "duration")
|
||||
o.Icon = JSONGetItem(val, "icon")
|
||||
o.Preview = JSONGetItem(val, "preview")
|
||||
o.Image = JSONGetItem(val, "image")
|
||||
o.Updated = JSONGetTime(val, "updated")
|
||||
o.InReplyTo = JSONGetItem(val, "inReplyTo")
|
||||
o.To = JSONGetItems(val, "to")
|
||||
o.Audience = JSONGetItems(val, "audience")
|
||||
o.Bto = JSONGetItems(val, "bto")
|
||||
o.CC = JSONGetItems(val, "cc")
|
||||
o.BCC = JSONGetItems(val, "bcc")
|
||||
o.Replies = JSONGetItem(val, "replies")
|
||||
o.Tag = JSONGetItems(val, "tag")
|
||||
o.Likes = JSONGetItem(val, "likes")
|
||||
o.Shares = JSONGetItem(val, "shares")
|
||||
o.Source = GetAPSource(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
func JSONLoadIntransitiveActivity(val *fastjson.Value, i *IntransitiveActivity) error {
|
||||
i.Actor = JSONGetItem(val, "actor")
|
||||
i.Target = JSONGetItem(val, "target")
|
||||
i.Result = JSONGetItem(val, "result")
|
||||
i.Origin = JSONGetItem(val, "origin")
|
||||
i.Instrument = JSONGetItem(val, "instrument")
|
||||
return OnObject(i, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadActivity(val *fastjson.Value, a *Activity) error {
|
||||
a.Object = JSONGetItem(val, "object")
|
||||
return OnIntransitiveActivity(a, func(i *IntransitiveActivity) error {
|
||||
return JSONLoadIntransitiveActivity(val, i)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadQuestion(val *fastjson.Value, q *Question) error {
|
||||
q.OneOf = JSONGetItem(val, "oneOf")
|
||||
q.AnyOf = JSONGetItem(val, "anyOf")
|
||||
q.Closed = JSONGetBoolean(val, "closed")
|
||||
return OnIntransitiveActivity(q, func(i *IntransitiveActivity) error {
|
||||
return JSONLoadIntransitiveActivity(val, i)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadActor(val *fastjson.Value, a *Actor) error {
|
||||
a.PreferredUsername = JSONGetNaturalLanguageField(val, "preferredUsername")
|
||||
a.Followers = JSONGetItem(val, "followers")
|
||||
a.Following = JSONGetItem(val, "following")
|
||||
a.Inbox = JSONGetItem(val, "inbox")
|
||||
a.Outbox = JSONGetItem(val, "outbox")
|
||||
a.Liked = JSONGetItem(val, "liked")
|
||||
a.Endpoints = JSONGetActorEndpoints(val, "endpoints")
|
||||
a.Streams = JSONGetItems(val, "streams")
|
||||
a.PublicKey = JSONGetPublicKey(val, "publicKey")
|
||||
return OnObject(a, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadCollection(val *fastjson.Value, c *Collection) error {
|
||||
c.Current = JSONGetItem(val, "current")
|
||||
c.First = JSONGetItem(val, "first")
|
||||
c.Last = JSONGetItem(val, "last")
|
||||
c.TotalItems = uint(JSONGetInt(val, "totalItems"))
|
||||
c.Items = JSONGetItems(val, "items")
|
||||
return OnObject(c, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadCollectionPage(val *fastjson.Value, c *CollectionPage) error {
|
||||
c.Next = JSONGetItem(val, "next")
|
||||
c.Prev = JSONGetItem(val, "prev")
|
||||
c.PartOf = JSONGetItem(val, "partOf")
|
||||
return OnCollection(c, func(c *Collection) error {
|
||||
return JSONLoadCollection(val, c)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadOrderedCollection(val *fastjson.Value, c *OrderedCollection) error {
|
||||
c.Current = JSONGetItem(val, "current")
|
||||
c.First = JSONGetItem(val, "first")
|
||||
c.Last = JSONGetItem(val, "last")
|
||||
c.TotalItems = uint(JSONGetInt(val, "totalItems"))
|
||||
c.OrderedItems = JSONGetItems(val, "orderedItems")
|
||||
return OnObject(c, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadOrderedCollectionPage(val *fastjson.Value, c *OrderedCollectionPage) error {
|
||||
c.Next = JSONGetItem(val, "next")
|
||||
c.Prev = JSONGetItem(val, "prev")
|
||||
c.PartOf = JSONGetItem(val, "partOf")
|
||||
c.StartIndex = uint(JSONGetInt(val, "startIndex"))
|
||||
return OnOrderedCollection(c, func(c *OrderedCollection) error {
|
||||
return JSONLoadOrderedCollection(val, c)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadPlace(val *fastjson.Value, p *Place) error {
|
||||
p.Accuracy = JSONGetFloat(val, "accuracy")
|
||||
p.Altitude = JSONGetFloat(val, "altitude")
|
||||
p.Latitude = JSONGetFloat(val, "latitude")
|
||||
p.Longitude = JSONGetFloat(val, "longitude")
|
||||
p.Radius = JSONGetInt(val, "radius")
|
||||
p.Units = JSONGetString(val, "units")
|
||||
return OnObject(p, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadProfile(val *fastjson.Value, p *Profile) error {
|
||||
p.Describes = JSONGetItem(val, "describes")
|
||||
return OnObject(p, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadRelationship(val *fastjson.Value, r *Relationship) error {
|
||||
r.Subject = JSONGetItem(val, "subject")
|
||||
r.Object = JSONGetItem(val, "object")
|
||||
r.Relationship = JSONGetItem(val, "relationship")
|
||||
return OnObject(r, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadTombstone(val *fastjson.Value, t *Tombstone) error {
|
||||
t.FormerType = ActivityVocabularyType(JSONGetString(val, "formerType"))
|
||||
t.Deleted = JSONGetTime(val, "deleted")
|
||||
return OnObject(t, func(o *Object) error {
|
||||
return JSONLoadObject(val, o)
|
||||
})
|
||||
}
|
||||
|
||||
func JSONLoadLink(val *fastjson.Value, l *Link) error {
|
||||
l.ID = JSONGetID(val)
|
||||
l.Type = JSONGetType(val)
|
||||
l.MediaType = JSONGetMimeType(val, "mediaType")
|
||||
l.Preview = JSONGetItem(val, "preview")
|
||||
if h := JSONGetInt(val, "height"); h != 0 {
|
||||
l.Height = uint(h)
|
||||
}
|
||||
if w := JSONGetInt(val, "width"); w != 0 {
|
||||
l.Width = uint(w)
|
||||
}
|
||||
l.Name = JSONGetNaturalLanguageField(val, "name")
|
||||
if hrefLang := JSONGetLangRefField(val, "hrefLang"); len(hrefLang) > 0 {
|
||||
l.HrefLang = hrefLang
|
||||
}
|
||||
if href := JSONGetURIItem(val, "href"); href != nil {
|
||||
ll := href.GetLink()
|
||||
if len(ll) > 0 {
|
||||
l.Href = ll
|
||||
}
|
||||
}
|
||||
if rel := JSONGetURIItem(val, "rel"); rel != nil {
|
||||
rr := rel.GetLink()
|
||||
if len(rr) > 0 {
|
||||
l.Rel = rr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func JSONLoadPublicKey(val *fastjson.Value, p *PublicKey) error {
|
||||
p.ID = JSONGetID(val)
|
||||
p.Owner = JSONGetIRI(val, "owner")
|
||||
if pub := val.GetStringBytes("publicKeyPem"); len(pub) > 0 {
|
||||
p.PublicKeyPem = string(pub)
|
||||
}
|
||||
return nil
|
||||
}
|
487
decoding_json_test.go
Normal file
487
decoding_json_test.go
Normal file
|
@ -0,0 +1,487 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type visit struct {
|
||||
a1 unsafe.Pointer
|
||||
a2 unsafe.Pointer
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
type canErrorFunc func(format string, args ...interface{})
|
||||
|
||||
// See reflect.DeepEqual
|
||||
func assertDeepEquals(t canErrorFunc, x, y interface{}) bool {
|
||||
if x == nil || y == nil {
|
||||
return x == y
|
||||
}
|
||||
v1 := reflect.ValueOf(x)
|
||||
v2 := reflect.ValueOf(y)
|
||||
if v1.Type() != v2.Type() {
|
||||
t("%T != %T", x, y)
|
||||
return false
|
||||
}
|
||||
return deepValueEqual(t, v1, v2, make(map[visit]bool), 0)
|
||||
}
|
||||
|
||||
// See reflect.deepValueEqual
|
||||
func deepValueEqual(t canErrorFunc, v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
|
||||
if !v1.IsValid() || !v2.IsValid() {
|
||||
return v1.IsValid() == v2.IsValid()
|
||||
}
|
||||
if v1.Type() != v2.Type() {
|
||||
t("types differ %s != %s", v1.Type().Name(), v2.Type().Name())
|
||||
return false
|
||||
}
|
||||
|
||||
hard := func(v1, v2 reflect.Value) bool {
|
||||
switch v1.Kind() {
|
||||
case reflect.Ptr:
|
||||
return false
|
||||
case reflect.Map, reflect.Slice, reflect.Interface:
|
||||
// Nil pointers cannot be cyclic. Avoid putting them in the visited map.
|
||||
return !v1.IsNil() && !v2.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if hard(v1, v2) {
|
||||
var addr1, addr2 unsafe.Pointer
|
||||
if v1.CanAddr() {
|
||||
addr1 = unsafe.Pointer(v1.UnsafeAddr())
|
||||
} else {
|
||||
addr1 = unsafe.Pointer(v1.Pointer())
|
||||
}
|
||||
if v2.CanAddr() {
|
||||
addr2 = unsafe.Pointer(v2.UnsafeAddr())
|
||||
} else {
|
||||
addr2 = unsafe.Pointer(v2.Pointer())
|
||||
}
|
||||
if uintptr(addr1) > uintptr(addr2) {
|
||||
// Canonicalize order to reduce number of entries in visited.
|
||||
// Assumes non-moving garbage collector.
|
||||
addr1, addr2 = addr2, addr1
|
||||
}
|
||||
// Short circuit if references are already seen.
|
||||
typ := v1.Type()
|
||||
v := visit{addr1, addr2, typ}
|
||||
if visited[v] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Remember for later.
|
||||
visited[v] = true
|
||||
}
|
||||
|
||||
switch v1.Kind() {
|
||||
case reflect.Array:
|
||||
for i := 0; i < v1.Len(); i++ {
|
||||
if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) {
|
||||
t("Arrays not equal at index %d %s %s", i, v1.Index(i), v2.Index(i))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Slice:
|
||||
if v1.IsNil() != v2.IsNil() {
|
||||
t("One of the slices is not nil %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Len() != v2.Len() {
|
||||
t("Slices lengths are different %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < v1.Len(); i++ {
|
||||
if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) {
|
||||
t("Slices elements at pos %d are not equal %#v vs %#v", i, v1.Index(i), v2.Index(i))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Interface:
|
||||
if v1.IsNil() || v2.IsNil() {
|
||||
if v1.IsNil() == v2.IsNil() {
|
||||
return true
|
||||
}
|
||||
var isNil1, isNil2 string
|
||||
if v1.IsNil() {
|
||||
isNil1 = "is"
|
||||
} else {
|
||||
isNil1 = "is not"
|
||||
}
|
||||
if v2.IsNil() {
|
||||
isNil2 = "is"
|
||||
} else {
|
||||
isNil2 = "is not"
|
||||
}
|
||||
t("Interface '%s' %s nil and '%s' %s nil", v1.Type().Name(), isNil1, v2.Type().Name(), isNil2)
|
||||
return false
|
||||
}
|
||||
return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
case reflect.Ptr:
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
case reflect.Struct:
|
||||
for i, n := 0, v1.NumField(); i < n; i++ {
|
||||
var (
|
||||
f1 = v1.Field(i)
|
||||
f2 = v2.Field(i)
|
||||
n1 = v1.Type().Field(i).Name
|
||||
n2 = v2.Type().Field(i).Name
|
||||
t1 = f1.Type().Name()
|
||||
t2 = f2.Type().Name()
|
||||
)
|
||||
if !deepValueEqual(t, v1.Field(i), v2.Field(i), visited, depth+1) {
|
||||
t("Struct fields at pos %d %s[%s] and %s[%s] are not deeply equal", i, n1, t1, n2, t2)
|
||||
if f1.CanInterface() && f2.CanInterface() {
|
||||
t(" Values: %#v - %#v", v1.Field(i).Interface(), v2.Field(i).Interface())
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Map:
|
||||
if v1.IsNil() != v2.IsNil() {
|
||||
t("Maps are not nil", v1.Type().Name(), v2.Type().Name())
|
||||
return false
|
||||
}
|
||||
if v1.Len() != v2.Len() {
|
||||
t("Maps don't have the same length %d vs %d", v1.Len(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
for _, k := range v1.MapKeys() {
|
||||
val1 := v1.MapIndex(k)
|
||||
val2 := v2.MapIndex(k)
|
||||
if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(t, v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
|
||||
t("Maps values at index %s are not equal", k.String())
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Func:
|
||||
if v1.IsNil() && v2.IsNil() {
|
||||
return true
|
||||
}
|
||||
// Can't do better than this:
|
||||
return false
|
||||
case reflect.String:
|
||||
return v1.String() == v2.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v1.Int() == v2.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return v1.Uint() == v2.Uint()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v1.Float() == v2.Float()
|
||||
case reflect.Bool:
|
||||
return v1.Bool() == v2.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return v1.Complex() == v2.Complex()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type testPairs map[ActivityVocabularyType]reflect.Type
|
||||
|
||||
var (
|
||||
objectPtrType = reflect.TypeOf(new(*Object)).Elem()
|
||||
tombstoneType = reflect.TypeOf(new(*Tombstone)).Elem()
|
||||
profileType = reflect.TypeOf(new(*Profile)).Elem()
|
||||
placeType = reflect.TypeOf(new(*Place)).Elem()
|
||||
relationshipType = reflect.TypeOf(new(*Relationship)).Elem()
|
||||
linkPtrType = reflect.TypeOf(new(*Link)).Elem()
|
||||
mentionPtrType = reflect.TypeOf(new(*Mention)).Elem()
|
||||
activityPtrType = reflect.TypeOf(new(*Activity)).Elem()
|
||||
intransitiveActivityPtrType = reflect.TypeOf(new(*IntransitiveActivity)).Elem()
|
||||
collectionPtrType = reflect.TypeOf(new(*Collection)).Elem()
|
||||
collectionPagePtrType = reflect.TypeOf(new(*CollectionPage)).Elem()
|
||||
orderedCollectionPtrType = reflect.TypeOf(new(*OrderedCollection)).Elem()
|
||||
orderedCollectionPagePtrType = reflect.TypeOf(new(*OrderedCollectionPage)).Elem()
|
||||
actorPtrType = reflect.TypeOf(new(*Actor)).Elem()
|
||||
applicationPtrType = reflect.TypeOf(new(*Application)).Elem()
|
||||
servicePtrType = reflect.TypeOf(new(*Service)).Elem()
|
||||
personPtrType = reflect.TypeOf(new(*Person)).Elem()
|
||||
groupPtrType = reflect.TypeOf(new(*Group)).Elem()
|
||||
organizationPtrType = reflect.TypeOf(new(*Organization)).Elem()
|
||||
acceptPtrType = reflect.TypeOf(new(*Accept)).Elem()
|
||||
addPtrType = reflect.TypeOf(new(*Add)).Elem()
|
||||
announcePtrType = reflect.TypeOf(new(*Announce)).Elem()
|
||||
arrivePtrType = reflect.TypeOf(new(*Arrive)).Elem()
|
||||
blockPtrType = reflect.TypeOf(new(*Block)).Elem()
|
||||
createPtrType = reflect.TypeOf(new(*Create)).Elem()
|
||||
deletePtrType = reflect.TypeOf(new(*Delete)).Elem()
|
||||
dislikePtrType = reflect.TypeOf(new(*Dislike)).Elem()
|
||||
flagPtrType = reflect.TypeOf(new(*Flag)).Elem()
|
||||
followPtrType = reflect.TypeOf(new(*Follow)).Elem()
|
||||
ignorePtrType = reflect.TypeOf(new(*Ignore)).Elem()
|
||||
invitePtrType = reflect.TypeOf(new(*Invite)).Elem()
|
||||
joinPtrType = reflect.TypeOf(new(*Join)).Elem()
|
||||
leavePtrType = reflect.TypeOf(new(*Leave)).Elem()
|
||||
likePtrType = reflect.TypeOf(new(*Like)).Elem()
|
||||
listenPtrType = reflect.TypeOf(new(*Listen)).Elem()
|
||||
movePtrType = reflect.TypeOf(new(*Move)).Elem()
|
||||
offerPtrType = reflect.TypeOf(new(*Offer)).Elem()
|
||||
questionPtrType = reflect.TypeOf(new(*Question)).Elem()
|
||||
rejectPtrType = reflect.TypeOf(new(*Reject)).Elem()
|
||||
readPtrType = reflect.TypeOf(new(*Read)).Elem()
|
||||
removePtrType = reflect.TypeOf(new(*Remove)).Elem()
|
||||
tentativeRejectPtrType = reflect.TypeOf(new(*TentativeReject)).Elem()
|
||||
tentativeAcceptPtrType = reflect.TypeOf(new(*TentativeAccept)).Elem()
|
||||
travelPtrType = reflect.TypeOf(new(*Travel)).Elem()
|
||||
undoPtrType = reflect.TypeOf(new(*Undo)).Elem()
|
||||
updatePtrType = reflect.TypeOf(new(*Update)).Elem()
|
||||
viewPtrType = reflect.TypeOf(new(*View)).Elem()
|
||||
)
|
||||
|
||||
var tests = testPairs{
|
||||
ObjectType: objectPtrType,
|
||||
ArticleType: objectPtrType,
|
||||
AudioType: objectPtrType,
|
||||
DocumentType: objectPtrType,
|
||||
ImageType: objectPtrType,
|
||||
NoteType: objectPtrType,
|
||||
PageType: objectPtrType,
|
||||
PlaceType: placeType,
|
||||
ProfileType: profileType,
|
||||
RelationshipType: relationshipType,
|
||||
TombstoneType: tombstoneType,
|
||||
VideoType: objectPtrType,
|
||||
LinkType: linkPtrType,
|
||||
MentionType: mentionPtrType,
|
||||
CollectionType: collectionPtrType,
|
||||
CollectionPageType: collectionPagePtrType,
|
||||
OrderedCollectionType: orderedCollectionPtrType,
|
||||
OrderedCollectionPageType: orderedCollectionPagePtrType,
|
||||
ActorType: actorPtrType,
|
||||
ApplicationType: applicationPtrType,
|
||||
ServiceType: servicePtrType,
|
||||
PersonType: personPtrType,
|
||||
GroupType: groupPtrType,
|
||||
OrganizationType: organizationPtrType,
|
||||
ActivityType: activityPtrType,
|
||||
IntransitiveActivityType: intransitiveActivityPtrType,
|
||||
AcceptType: acceptPtrType,
|
||||
AddType: addPtrType,
|
||||
AnnounceType: announcePtrType,
|
||||
ArriveType: arrivePtrType,
|
||||
BlockType: blockPtrType,
|
||||
CreateType: createPtrType,
|
||||
DeleteType: deletePtrType,
|
||||
DislikeType: dislikePtrType,
|
||||
FlagType: flagPtrType,
|
||||
FollowType: followPtrType,
|
||||
IgnoreType: ignorePtrType,
|
||||
InviteType: invitePtrType,
|
||||
JoinType: joinPtrType,
|
||||
LeaveType: leavePtrType,
|
||||
LikeType: likePtrType,
|
||||
ListenType: listenPtrType,
|
||||
MoveType: movePtrType,
|
||||
OfferType: offerPtrType,
|
||||
QuestionType: questionPtrType,
|
||||
RejectType: rejectPtrType,
|
||||
ReadType: readPtrType,
|
||||
RemoveType: removePtrType,
|
||||
TentativeRejectType: tentativeRejectPtrType,
|
||||
TentativeAcceptType: tentativeAcceptPtrType,
|
||||
TravelType: travelPtrType,
|
||||
UndoType: undoPtrType,
|
||||
UpdateType: updatePtrType,
|
||||
ViewType: viewPtrType,
|
||||
}
|
||||
|
||||
func TestJSONGetItemByType(t *testing.T) {
|
||||
for typ, test := range tests {
|
||||
t.Run(string(typ), func(t *testing.T) {
|
||||
v, err := GetItemByType(typ)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if reflect.TypeOf(v) != test {
|
||||
t.Errorf("Invalid type returned %T, expected %s", v, test.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want Item
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
data: []byte{'{', '}'},
|
||||
want: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "IRI",
|
||||
data: []byte(`"http://example.com"`),
|
||||
want: IRI("http://example.com"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "IRIs",
|
||||
data: []byte(fmt.Sprintf("[%q, %q]", "http://example.com", "http://example.net")),
|
||||
want: ItemCollection{
|
||||
IRI("http://example.com"),
|
||||
IRI("http://example.net"),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "object",
|
||||
data: []byte(`{"type":"Note"}`),
|
||||
want: &Object{Type: NoteType},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "activity",
|
||||
data: []byte(`{"type":"Like"}`),
|
||||
want: &Activity{Type: LikeType},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "collection-2-items",
|
||||
data: []byte(`{ "@context": "https://www.w3.org/ns/activitystreams", "id": "https://federated.git/inbox", "type": "OrderedCollection", "updated": "2021-08-08T16:09:05Z", "first": "https://federated.git/inbox?maxItems=100", "totalItems": 2, "orderedItems": [ { "id": "https://federated.git/activities/07440c39-64b2-4492-89cf-f5c2872cf4ff", "type": "Create", "attributedTo": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "to": [ "https://www.w3.org/ns/activitystreams#Public" ], "cc": [ "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91/followers" ], "published": "2021-08-08T16:09:05Z", "actor": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "object": "https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4" }, { "id": "https://federated.git/activities/ab9a5511-cdb5-4585-8a48-775d1bf20121", "type": "Like", "attributedTo": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "to": [ "https://www.w3.org/ns/activitystreams#Public", "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91" ], "published": "2021-08-08T16:09:05Z", "actor": "https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91", "object": "https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4" }]}`),
|
||||
want: &OrderedCollection{
|
||||
ID: "https://federated.git/inbox",
|
||||
Type: OrderedCollectionType,
|
||||
Updated: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC),
|
||||
First: IRI("https://federated.git/inbox?maxItems=100"),
|
||||
OrderedItems: ItemCollection{
|
||||
&Activity{
|
||||
ID: "https://federated.git/activities/07440c39-64b2-4492-89cf-f5c2872cf4ff",
|
||||
Type: CreateType,
|
||||
AttributedTo: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"),
|
||||
To: ItemCollection{PublicNS},
|
||||
CC: ItemCollection{IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91/followers")},
|
||||
Published: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC),
|
||||
Actor: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"),
|
||||
Object: IRI("https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4"),
|
||||
},
|
||||
&Activity{
|
||||
ID: "https://federated.git/activities/ab9a5511-cdb5-4585-8a48-775d1bf20121",
|
||||
Type: LikeType,
|
||||
AttributedTo: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"),
|
||||
To: ItemCollection{PublicNS, IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91")},
|
||||
Published: time.Date(2021, 8, 8, 16, 9, 5, 0, time.UTC),
|
||||
Actor: IRI("https://federated.git/actors/b1757243-080a-49dc-b832-42905d554b91"),
|
||||
Object: IRI("https://federated.git/objects/3eb69f77-3b08-4bf1-8760-c7333e2900c4"),
|
||||
},
|
||||
},
|
||||
TotalItems: 2,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := UnmarshalJSON(tt.data)
|
||||
if (err != nil && tt.err == nil) || (err == nil && tt.err != nil) {
|
||||
if !errors.Is(err, tt.err) {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !assertDeepEquals(t.Errorf, got, tt.want) {
|
||||
t.Errorf("UnmarshalJSON() got = %#v, want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONGetDuration(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetInt(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJSONGetIRI(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetItem(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetItems(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestJSONGetLangRefField(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetMimeType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetNaturalLanguageField(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetString(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetTime(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetURIItem(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONUnmarshalToItem(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetActorEndpoints(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetBoolean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetBytes(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetFloat(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetPublicKey(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestJSONGetStreams(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
792
encoding_gob.go
Normal file
792
encoding_gob.go
Normal file
|
@ -0,0 +1,792 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
func GobEncode(it Item) ([]byte, error) {
|
||||
return gobEncodeItem(it)
|
||||
}
|
||||
|
||||
// TODO(marius): when migrating to go1.18, use a numeric constraint for this
|
||||
func gobEncodeInt64(i int64) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
if err := gg.Encode(i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// TODO(marius): when migrating to go1.18, use a numeric constraint for this
|
||||
func gobEncodeUint(i uint) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
if err := gg.Encode(i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func gobEncodeFloat64(f float64) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
if err := gg.Encode(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func gobEncodeBool(t bool) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
if err := gg.Encode(t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func gobEncodeStringLikeType(g *gob.Encoder, s []byte) error {
|
||||
if err := g.Encode(s); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func gobEncodeItems(col ItemCollection) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
tt := make([][]byte, 0)
|
||||
for _, it := range col.Collection() {
|
||||
single, err := gobEncodeItem(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tt = append(tt, single)
|
||||
}
|
||||
err := gob.NewEncoder(&b).Encode(tt)
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func gobEncodeIRIs(col IRIs) ([]byte, error) {
|
||||
b := bytes.Buffer{}
|
||||
err := gob.NewEncoder(&b).Encode(col)
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func gobEncodeItemOrLink(it LinkOrIRI) ([]byte, error) {
|
||||
if ob, ok := it.(Item); ok {
|
||||
return gobEncodeItem(ob)
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
err := OnLink(it, func(l *Link) error {
|
||||
bytes, err := l.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func gobEncodeItem(it Item) ([]byte, error) {
|
||||
if IsIRI(it) {
|
||||
if i, ok := it.(IRI); ok {
|
||||
return []byte(i), nil
|
||||
}
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
var err error
|
||||
if IsIRIs(it) {
|
||||
err = OnIRIs(it, func(iris *IRIs) error {
|
||||
bytes, err := gobEncodeIRIs(*iris)
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
err = OnItemCollection(it, func(col *ItemCollection) error {
|
||||
bytes, err := gobEncodeItems(*col)
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if IsObject(it) {
|
||||
switch it.GetType() {
|
||||
case IRIType:
|
||||
var bytes []byte
|
||||
bytes, err = it.(IRI).GobEncode()
|
||||
b.Write(bytes)
|
||||
case "", ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType:
|
||||
err = OnObject(it, func(ob *Object) error {
|
||||
bytes, err := ob.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case LinkType, MentionType:
|
||||
// TODO(marius): this shouldn't work, as Link does not implement Item? (or rather, should not)
|
||||
err = OnLink(it, func(l *Link) error {
|
||||
bytes, err := l.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType,
|
||||
FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType,
|
||||
RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType:
|
||||
err = OnActivity(it, func(act *Activity) error {
|
||||
bytes, err := act.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case IntransitiveActivityType, ArriveType, TravelType:
|
||||
err = OnIntransitiveActivity(it, func(act *IntransitiveActivity) error {
|
||||
bytes, err := act.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType:
|
||||
err = OnActor(it, func(a *Actor) error {
|
||||
bytes, err := a.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case CollectionType:
|
||||
err = OnCollection(it, func(c *Collection) error {
|
||||
bytes, err := c.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case OrderedCollectionType:
|
||||
err = OnOrderedCollection(it, func(c *OrderedCollection) error {
|
||||
bytes, err := c.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case CollectionPageType:
|
||||
err = OnCollectionPage(it, func(p *CollectionPage) error {
|
||||
bytes, err := p.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case OrderedCollectionPageType:
|
||||
err = OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error {
|
||||
bytes, err := p.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case PlaceType:
|
||||
err = OnPlace(it, func(p *Place) error {
|
||||
bytes, err := p.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case ProfileType:
|
||||
err = OnProfile(it, func(p *Profile) error {
|
||||
bytes, err := p.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case RelationshipType:
|
||||
err = OnRelationship(it, func(r *Relationship) error {
|
||||
bytes, err := r.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case TombstoneType:
|
||||
err = OnTombstone(it, func(t *Tombstone) error {
|
||||
bytes, err := t.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
case QuestionType:
|
||||
err = OnQuestion(it, func(q *Question) error {
|
||||
bytes, err := q.GobEncode()
|
||||
b.Write(bytes)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
return b.Bytes(), err
|
||||
}
|
||||
|
||||
func mapObjectProperties(mm map[string][]byte, o *Object) (hasData bool, err error) {
|
||||
if len(o.ID) > 0 {
|
||||
if mm["id"], err = o.ID.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.Type) > 0 {
|
||||
if mm["type"], err = o.Type.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.MediaType) > 0 {
|
||||
if mm["mediaType"], err = o.MediaType.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.Name) > 0 {
|
||||
if mm["name"], err = o.Name.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Attachment != nil {
|
||||
if mm["attachment"], err = gobEncodeItem(o.Attachment); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.AttributedTo != nil {
|
||||
if mm["attributedTo"], err = gobEncodeItem(o.AttributedTo); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Audience != nil {
|
||||
if mm["audience"], err = gobEncodeItem(o.Audience); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Content != nil {
|
||||
if mm["content"], err = o.Content.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Context != nil {
|
||||
if mm["context"], err = gobEncodeItem(o.Context); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.MediaType) > 0 {
|
||||
if mm["mediaType"], err = o.MediaType.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !o.EndTime.IsZero() {
|
||||
if mm["endTime"], err = o.EndTime.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Generator != nil {
|
||||
if mm["generator"], err = gobEncodeItem(o.Generator); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Icon != nil {
|
||||
if mm["icon"], err = gobEncodeItem(o.Icon); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Image != nil {
|
||||
if mm["image"], err = gobEncodeItem(o.Image); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.InReplyTo != nil {
|
||||
if mm["inReplyTo"], err = gobEncodeItem(o.InReplyTo); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Location != nil {
|
||||
if mm["location"], err = gobEncodeItem(o.Location); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Preview != nil {
|
||||
if mm["preview"], err = gobEncodeItem(o.Preview); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !o.Published.IsZero() {
|
||||
if mm["published"], err = o.Published.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Replies != nil {
|
||||
if mm["replies"], err = gobEncodeItem(o.Replies); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !o.StartTime.IsZero() {
|
||||
if mm["startTime"], err = o.StartTime.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.Summary) > 0 {
|
||||
if mm["summary"], err = o.Summary.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Tag != nil {
|
||||
if mm["tag"], err = gobEncodeItem(o.Tag); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !o.Updated.IsZero() {
|
||||
if mm["updated"], err = o.Updated.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Tag != nil {
|
||||
if mm["tag"], err = gobEncodeItem(o.Tag); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !o.Updated.IsZero() {
|
||||
if mm["updated"], err = o.Updated.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.URL != nil {
|
||||
if mm["url"], err = gobEncodeItemOrLink(o.URL); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.To != nil {
|
||||
if mm["to"], err = gobEncodeItem(o.To); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Bto != nil {
|
||||
if mm["bto"], err = gobEncodeItem(o.Bto); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.CC != nil {
|
||||
if mm["cc"], err = gobEncodeItem(o.CC); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.BCC != nil {
|
||||
if mm["bcc"], err = gobEncodeItem(o.BCC); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Duration > 0 {
|
||||
if mm["duration"], err = gobEncodeInt64(int64(o.Duration)); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Likes != nil {
|
||||
if mm["likes"], err = gobEncodeItem(o.Likes); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Shares != nil {
|
||||
if mm["shares"], err = gobEncodeItem(o.Shares); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if o.Shares != nil {
|
||||
if mm["shares"], err = gobEncodeItem(o.Shares); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(o.Source.MediaType)+len(o.Source.Content) > 0 {
|
||||
if mm["source"], err = o.Source.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
|
||||
return hasData, nil
|
||||
}
|
||||
|
||||
func mapActorProperties(mm map[string][]byte, a *Actor) (hasData bool, err error) {
|
||||
err = OnObject(a, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if a.Inbox != nil {
|
||||
if mm["inbox"], err = gobEncodeItem(a.Inbox); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Inbox != nil {
|
||||
if mm["inbox"], err = gobEncodeItem(a.Inbox); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Outbox != nil {
|
||||
if mm["outbox"], err = gobEncodeItem(a.Outbox); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Following != nil {
|
||||
if mm["following"], err = gobEncodeItem(a.Following); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Followers != nil {
|
||||
if mm["followers"], err = gobEncodeItem(a.Followers); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Liked != nil {
|
||||
if mm["liked"], err = gobEncodeItem(a.Liked); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(a.PreferredUsername) > 0 {
|
||||
if mm["preferredUsername"], err = a.PreferredUsername.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if a.Endpoints != nil {
|
||||
if mm["endpoints"], err = a.Endpoints.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(a.Streams) > 0 {
|
||||
if mm["streams"], err = gobEncodeItems(a.Streams); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(a.PublicKey.PublicKeyPem)+len(a.PublicKey.ID) > 0 {
|
||||
if mm["publicKey"], err = a.PublicKey.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return hasData, err
|
||||
}
|
||||
|
||||
func mapIncompleteCollectionProperties(mm map[string][]byte, c Collection) (hasData bool, err error) {
|
||||
err = OnObject(c, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if c.Current != nil {
|
||||
if mm["current"], err = gobEncodeItem(c.Current); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.First != nil {
|
||||
if mm["first"], err = gobEncodeItem(c.First); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.Last != nil {
|
||||
if mm["last"], err = gobEncodeItem(c.Last); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.TotalItems > 0 {
|
||||
hasData = true
|
||||
}
|
||||
if mm["totalItems"], err = gobEncodeUint(c.TotalItems); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapCollectionProperties(mm map[string][]byte, c Collection) (hasData bool, err error) {
|
||||
hasData, err = mapIncompleteCollectionProperties(mm, c)
|
||||
if err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
if c.Items != nil {
|
||||
if mm["items"], err = gobEncodeItems(c.Items); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapOrderedCollectionProperties(mm map[string][]byte, c OrderedCollection) (hasData bool, err error) {
|
||||
err = OnCollection(c, func(c *Collection) error {
|
||||
hasData, err = mapIncompleteCollectionProperties(mm, *c)
|
||||
return err
|
||||
})
|
||||
if c.OrderedItems != nil {
|
||||
if mm["orderedItems"], err = gobEncodeItems(c.OrderedItems); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapCollectionPageProperties(mm map[string][]byte, c CollectionPage) (hasData bool, err error) {
|
||||
err = OnCollection(c, func(c *Collection) error {
|
||||
hasData, err = mapCollectionProperties(mm, *c)
|
||||
return err
|
||||
})
|
||||
if c.PartOf != nil {
|
||||
if mm["partOf"], err = gobEncodeItem(c.PartOf); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.Next != nil {
|
||||
if mm["next"], err = gobEncodeItem(c.Next); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.Prev != nil {
|
||||
if mm["prev"], err = gobEncodeItem(c.Prev); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapOrderedCollectionPageProperties(mm map[string][]byte, c OrderedCollectionPage) (hasData bool, err error) {
|
||||
err = OnOrderedCollection(c, func(c *OrderedCollection) error {
|
||||
hasData, err = mapOrderedCollectionProperties(mm, *c)
|
||||
return err
|
||||
})
|
||||
if c.PartOf != nil {
|
||||
if mm["partOf"], err = gobEncodeItem(c.PartOf); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.Next != nil {
|
||||
if mm["next"], err = gobEncodeItem(c.Next); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if c.Prev != nil {
|
||||
if mm["prev"], err = gobEncodeItem(c.Prev); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapLinkProperties(mm map[string][]byte, l Link) (hasData bool, err error) {
|
||||
if len(l.ID) > 0 {
|
||||
if mm["id"], err = l.ID.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.Type) > 0 {
|
||||
if mm["type"], err = l.Type.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.MediaType) > 0 {
|
||||
if mm["mediaType"], err = l.MediaType.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.Href) > 0 {
|
||||
if mm["href"], err = l.Href.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.HrefLang) > 0 {
|
||||
if mm["hrefLang"], err = l.HrefLang.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.Name) > 0 {
|
||||
if mm["name"], err = l.Name.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(l.Rel) > 0 {
|
||||
if mm["rel"], err = l.Rel.GobEncode(); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if l.Width > 0 {
|
||||
if mm["width"], err = gobEncodeUint(l.Width); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if l.Height > 0 {
|
||||
if mm["height"], err = gobEncodeUint(l.Height); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapPlaceProperties(mm map[string][]byte, p Place) (hasData bool, err error) {
|
||||
err = OnObject(p, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if p.Accuracy > 0 {
|
||||
if mm["accuracy"], err = gobEncodeFloat64(p.Accuracy); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if p.Altitude > 0 {
|
||||
if mm["altitude"], err = gobEncodeFloat64(p.Altitude); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if p.Latitude > 0 {
|
||||
if mm["latitude"], err = gobEncodeFloat64(p.Latitude); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if p.Longitude > 0 {
|
||||
if mm["longitude"], err = gobEncodeFloat64(p.Longitude); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if p.Radius > 0 {
|
||||
if mm["radius"], err = gobEncodeInt64(p.Radius); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(p.Units) > 0 {
|
||||
mm["units"] = []byte(p.Units)
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapProfileProperties(mm map[string][]byte, p Profile) (hasData bool, err error) {
|
||||
err = OnObject(p, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if p.Describes != nil {
|
||||
if mm["describes"], err = gobEncodeItem(p.Describes); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapRelationshipProperties(mm map[string][]byte, r Relationship) (hasData bool, err error) {
|
||||
err = OnObject(r, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if r.Subject != nil {
|
||||
if mm["subject"], err = gobEncodeItem(r.Subject); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if r.Object != nil {
|
||||
if mm["object"], err = gobEncodeItem(r.Object); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if r.Relationship != nil {
|
||||
if mm["relationship"], err = gobEncodeItem(r.Relationship); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapTombstoneProperties(mm map[string][]byte, t Tombstone) (hasData bool, err error) {
|
||||
err = OnObject(t, func(o *Object) error {
|
||||
hasData, err = mapObjectProperties(mm, o)
|
||||
return err
|
||||
})
|
||||
if len(t.FormerType) > 0 {
|
||||
if mm["formerType"], err = t.FormerType.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if !t.Deleted.IsZero() {
|
||||
if mm["deleted"], err = t.Deleted.GobEncode(); err != nil {
|
||||
return hasData, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func mapQuestionProperties(mm map[string][]byte, q Question) (hasData bool, err error) {
|
||||
err = OnIntransitiveActivity(q, func(i *IntransitiveActivity) error {
|
||||
hasData, err = mapIntransitiveActivityProperties(mm, i)
|
||||
return err
|
||||
})
|
||||
if q.OneOf != nil {
|
||||
if mm["oneOf"], err = gobEncodeItem(q.OneOf); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if q.AnyOf != nil {
|
||||
if mm["anyOf"], err = gobEncodeItem(q.AnyOf); err != nil {
|
||||
return
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if q.Closed {
|
||||
hasData = true
|
||||
}
|
||||
if hasData {
|
||||
if mm["closed"], err = gobEncodeBool(q.Closed); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
43
encoding_gob_test.go
Normal file
43
encoding_gob_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package activitypub
|
||||
|
||||
/*
|
||||
func TestMarshalGob(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
it Item
|
||||
want []byte
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "empty object",
|
||||
it: &Object{
|
||||
ID: "test",
|
||||
},
|
||||
want: []byte{},
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0))
|
||||
err := gob.NewEncoder(buf).Encode(tt.it)
|
||||
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Errorf("MarshalGob() error = %s, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
it := new(Object)
|
||||
got := buf.Bytes()
|
||||
if err := gob.NewDecoder(bytes.NewReader(got)).Decode(it); err != nil {
|
||||
t.Errorf("Gob Decoding failed for previously generated output %v", err)
|
||||
}
|
||||
if tt.wantErr == nil {
|
||||
if !assertDeepEquals(t.Errorf, it, tt.it) {
|
||||
t.Errorf("Gob Decoded value is different got = %#v, want %#v", it, tt.it)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
405
encoding_json.go
Normal file
405
encoding_json.go
Normal file
|
@ -0,0 +1,405 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.sr.ht/~mariusor/go-xsd-duration"
|
||||
"github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
func JSONWriteComma(b *[]byte) {
|
||||
if len(*b) > 1 && (*b)[len(*b)-1] != ',' {
|
||||
*b = append(*b, ',')
|
||||
}
|
||||
}
|
||||
|
||||
func JSONWriteProp(b *[]byte, name string, val []byte) (notEmpty bool) {
|
||||
if len(val) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWriteComma(b)
|
||||
success := JSONWritePropName(b, name) && JSONWriteValue(b, val)
|
||||
if !success {
|
||||
*b = (*b)[:len(*b)-1]
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
func JSONWrite(b *[]byte, c ...byte) {
|
||||
*b = append(*b, c...)
|
||||
}
|
||||
|
||||
func JSONWriteS(b *[]byte, s string) {
|
||||
*b = append(*b, s...)
|
||||
}
|
||||
|
||||
func JSONWritePropName(b *[]byte, s string) (notEmpty bool) {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWrite(b, '"')
|
||||
JSONWriteS(b, s)
|
||||
JSONWrite(b, '"', ':')
|
||||
return true
|
||||
}
|
||||
|
||||
func JSONWriteValue(b *[]byte, s []byte) (notEmpty bool) {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWrite(b, s...)
|
||||
return true
|
||||
}
|
||||
|
||||
func JSONWriteNaturalLanguageProp(b *[]byte, n string, nl NaturalLanguageValues) (notEmpty bool) {
|
||||
l := nl.Count()
|
||||
if l > 1 {
|
||||
n += "Map"
|
||||
}
|
||||
if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
return JSONWriteProp(b, n, v)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func JSONWriteStringProp(b *[]byte, n string, s string) (notEmpty bool) {
|
||||
return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%s"`, s)))
|
||||
}
|
||||
|
||||
func JSONWriteBoolProp(b *[]byte, n string, t bool) (notEmpty bool) {
|
||||
return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%t"`, t)))
|
||||
}
|
||||
|
||||
func JSONWriteIntProp(b *[]byte, n string, d int64) (notEmpty bool) {
|
||||
return JSONWriteProp(b, n, []byte(fmt.Sprintf("%d", d)))
|
||||
}
|
||||
|
||||
func JSONWriteFloatProp(b *[]byte, n string, f float64) (notEmpty bool) {
|
||||
return JSONWriteProp(b, n, []byte(fmt.Sprintf("%f", f)))
|
||||
}
|
||||
|
||||
func JSONWriteTimeProp(b *[]byte, n string, t time.Time) (notEmpty bool) {
|
||||
var tb []byte
|
||||
JSONWrite(&tb, '"')
|
||||
JSONWriteS(&tb, t.UTC().Format(time.RFC3339))
|
||||
JSONWrite(&tb, '"')
|
||||
return JSONWriteProp(b, n, tb)
|
||||
}
|
||||
|
||||
func JSONWriteDurationProp(b *[]byte, n string, d time.Duration) (notEmpty bool) {
|
||||
var tb []byte
|
||||
if v, err := xsd.Marshal(d); err == nil {
|
||||
JSONWrite(&tb, '"')
|
||||
JSONWrite(&tb, v...)
|
||||
JSONWrite(&tb, '"')
|
||||
}
|
||||
return JSONWriteProp(b, n, tb)
|
||||
}
|
||||
|
||||
func JSONWriteIRIProp(b *[]byte, n string, i LinkOrIRI) (notEmpty bool) {
|
||||
url := i.GetLink().String()
|
||||
if len(url) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWriteStringProp(b, n, url)
|
||||
return true
|
||||
}
|
||||
|
||||
func JSONWriteItemProp(b *[]byte, n string, i Item) (notEmpty bool) {
|
||||
if i == nil {
|
||||
return notEmpty
|
||||
}
|
||||
if im, ok := i.(json.Marshaler); ok {
|
||||
v, err := im.MarshalJSON()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return JSONWriteProp(b, n, v)
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func byteInsertAt(raw []byte, b byte, p int) []byte {
|
||||
return append(raw[:p], append([]byte{b}, raw[p:]...)...)
|
||||
}
|
||||
|
||||
func escapeQuote(s string) string {
|
||||
raw := []byte(s)
|
||||
end := len(s)
|
||||
for i := 0; i < end; i++ {
|
||||
c := raw[i]
|
||||
if c == '"' && (i > 0 && s[i-1] != '\\') {
|
||||
raw = byteInsertAt(raw, '\\', i)
|
||||
i++
|
||||
end++
|
||||
}
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func JSONWriteStringValue(b *[]byte, s string) (notEmpty bool) {
|
||||
if len(s) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWrite(b, '"')
|
||||
JSONWriteS(b, escapeQuote(s))
|
||||
JSONWrite(b, '"')
|
||||
return true
|
||||
}
|
||||
|
||||
func JSONWriteItemCollectionValue(b *[]byte, col ItemCollection, compact bool) (notEmpty bool) {
|
||||
if len(col) == 0 {
|
||||
return notEmpty
|
||||
}
|
||||
if len(col) == 1 && compact {
|
||||
it := col[0]
|
||||
im, ok := it.(json.Marshaler)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
v, err := im.MarshalJSON()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(v) == 0 {
|
||||
return false
|
||||
}
|
||||
JSONWrite(b, v...)
|
||||
return true
|
||||
}
|
||||
writeCommaIfNotEmpty := func(notEmpty bool) {
|
||||
if notEmpty {
|
||||
JSONWrite(b, ',')
|
||||
}
|
||||
}
|
||||
JSONWrite(b, '[')
|
||||
skipComma := true
|
||||
for _, it := range col {
|
||||
im, ok := it.(json.Marshaler)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
v, err := im.MarshalJSON()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
writeCommaIfNotEmpty(!skipComma)
|
||||
JSONWrite(b, v...)
|
||||
skipComma = false
|
||||
}
|
||||
JSONWrite(b, ']')
|
||||
return true
|
||||
}
|
||||
|
||||
func JSONWriteItemCollectionProp(b *[]byte, n string, col ItemCollection, compact bool) (notEmpty bool) {
|
||||
if len(col) == 0 {
|
||||
return notEmpty
|
||||
}
|
||||
JSONWriteComma(b)
|
||||
success := JSONWritePropName(b, n) && JSONWriteItemCollectionValue(b, col, compact)
|
||||
if !success {
|
||||
*b = (*b)[:len(*b)-1]
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
func JSONWriteObjectValue(b *[]byte, o Object) (notEmpty bool) {
|
||||
if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "id", v)
|
||||
}
|
||||
if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "type", v) || notEmpty
|
||||
}
|
||||
if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty
|
||||
}
|
||||
if len(o.Name) > 0 {
|
||||
notEmpty = JSONWriteNaturalLanguageProp(b, "name", o.Name) || notEmpty
|
||||
}
|
||||
if len(o.Summary) > 0 {
|
||||
notEmpty = JSONWriteNaturalLanguageProp(b, "summary", o.Summary) || notEmpty
|
||||
}
|
||||
if len(o.Content) > 0 {
|
||||
notEmpty = JSONWriteNaturalLanguageProp(b, "content", o.Content) || notEmpty
|
||||
}
|
||||
if o.Attachment != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "attachment", o.Attachment) || notEmpty
|
||||
}
|
||||
if o.AttributedTo != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "attributedTo", o.AttributedTo) || notEmpty
|
||||
}
|
||||
if o.Audience != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "audience", o.Audience) || notEmpty
|
||||
}
|
||||
if o.Context != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "context", o.Context) || notEmpty
|
||||
}
|
||||
if o.Generator != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "generator", o.Generator) || notEmpty
|
||||
}
|
||||
if o.Icon != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "icon", o.Icon) || notEmpty
|
||||
}
|
||||
if o.Image != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "image", o.Image) || notEmpty
|
||||
}
|
||||
if o.InReplyTo != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty
|
||||
}
|
||||
if o.Location != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "location", o.Location) || notEmpty
|
||||
}
|
||||
if o.Preview != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "preview", o.Preview) || notEmpty
|
||||
}
|
||||
if o.Replies != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "replies", o.Replies) || notEmpty
|
||||
}
|
||||
if o.Tag != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(b, "tag", o.Tag, false) || notEmpty
|
||||
}
|
||||
if o.URL != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "url", o.URL) || notEmpty
|
||||
}
|
||||
if o.To != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(b, "to", o.To, false) || notEmpty
|
||||
}
|
||||
if o.Bto != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(b, "bto", o.Bto, false) || notEmpty
|
||||
}
|
||||
if o.CC != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(b, "cc", o.CC, false) || notEmpty
|
||||
}
|
||||
if o.BCC != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(b, "bcc", o.BCC, false) || notEmpty
|
||||
}
|
||||
if !o.Published.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(b, "published", o.Published) || notEmpty
|
||||
}
|
||||
if !o.Updated.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(b, "updated", o.Updated) || notEmpty
|
||||
}
|
||||
if !o.StartTime.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(b, "startTime", o.StartTime) || notEmpty
|
||||
}
|
||||
if !o.EndTime.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(b, "endTime", o.EndTime) || notEmpty
|
||||
}
|
||||
if o.Duration != 0 {
|
||||
// TODO(marius): maybe don't use 0 as a nil value for Object types
|
||||
// which can have a valid duration of 0 - (Video, Audio, etc)
|
||||
notEmpty = JSONWriteDurationProp(b, "duration", o.Duration) || notEmpty
|
||||
}
|
||||
if o.Likes != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "likes", o.Likes) || notEmpty
|
||||
}
|
||||
if o.Shares != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "shares", o.Shares) || notEmpty
|
||||
}
|
||||
if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "source", v) || notEmpty
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func JSONWriteActivityValue(b *[]byte, a Activity) (notEmpty bool) {
|
||||
_ = OnIntransitiveActivity(a, func(i *IntransitiveActivity) error {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty
|
||||
return nil
|
||||
})
|
||||
if a.Object != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "object", a.Object) || notEmpty
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func JSONWriteIntransitiveActivityValue(b *[]byte, i IntransitiveActivity) (notEmpty bool) {
|
||||
_ = OnObject(i, func(o *Object) error {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
notEmpty = JSONWriteObjectValue(b, *o) || notEmpty
|
||||
return nil
|
||||
})
|
||||
if i.Actor != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "actor", i.Actor) || notEmpty
|
||||
}
|
||||
if i.Target != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "target", i.Target) || notEmpty
|
||||
}
|
||||
if i.Result != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "result", i.Result) || notEmpty
|
||||
}
|
||||
if i.Origin != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "origin", i.Origin) || notEmpty
|
||||
}
|
||||
if i.Instrument != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "instrument", i.Instrument) || notEmpty
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func JSONWriteQuestionValue(b *[]byte, q Question) (notEmpty bool) {
|
||||
_ = OnIntransitiveActivity(q, func(i *IntransitiveActivity) error {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty
|
||||
return nil
|
||||
})
|
||||
if q.OneOf != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "oneOf", q.OneOf) || notEmpty
|
||||
}
|
||||
if q.AnyOf != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "anyOf", q.AnyOf) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteBoolProp(b, "closed", q.Closed) || notEmpty
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func JSONWriteLinkValue(b *[]byte, l Link) (notEmpty bool) {
|
||||
if v, err := l.ID.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "id", v)
|
||||
}
|
||||
if v, err := l.Type.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "type", v) || notEmpty
|
||||
}
|
||||
if v, err := l.MediaType.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty
|
||||
}
|
||||
if len(l.Name) > 0 {
|
||||
notEmpty = JSONWriteNaturalLanguageProp(b, "name", l.Name) || notEmpty
|
||||
}
|
||||
if v, err := l.Rel.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "rel", v) || notEmpty
|
||||
}
|
||||
if l.Height > 0 {
|
||||
notEmpty = JSONWriteIntProp(b, "height", int64(l.Height))
|
||||
}
|
||||
if l.Width > 0 {
|
||||
notEmpty = JSONWriteIntProp(b, "width", int64(l.Width))
|
||||
}
|
||||
if l.Preview != nil {
|
||||
notEmpty = JSONWriteItemProp(b, "rel", l.Preview) || notEmpty
|
||||
}
|
||||
if v, err := l.Href.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(b, "href", v) || notEmpty
|
||||
}
|
||||
if len(l.HrefLang) > 0 {
|
||||
notEmpty = JSONWriteStringProp(b, "hrefLang", string(l.HrefLang)) || notEmpty
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
// MarshalJSON represents just a wrapper for the jsonld.Marshal function
|
||||
func MarshalJSON(it LinkOrIRI) ([]byte, error) {
|
||||
return jsonld.Marshal(it)
|
||||
}
|
620
encoding_json_test.go
Normal file
620
encoding_json_test.go
Normal file
|
@ -0,0 +1,620 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_JSONWrite(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
c []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteActivity(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
a Activity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteActivityValue(tt.args.b, tt.args.a); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteActivityValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteBoolProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
t bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteBoolProp(tt.args.b, tt.args.n, tt.args.t); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteBoolProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteComma(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteDurationProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
d time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteDurationProp(tt.args.b, tt.args.n, tt.args.d); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteDurationProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteFloatProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
f float64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteFloatProp(tt.args.b, tt.args.n, tt.args.f); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteFloatProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteIRIProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
i LinkOrIRI
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteIRIProp(tt.args.b, tt.args.n, tt.args.i); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteIRIProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteIntProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
d int64
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteIntProp(tt.args.b, tt.args.n, tt.args.d); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteIntProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteIntransitiveActivity(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
i IntransitiveActivity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteIntransitiveActivityValue(tt.args.b, tt.args.i); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteIntransitiveActivityValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteItemCollection(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
col ItemCollection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteItemCollectionValue(tt.args.b, tt.args.col, true); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteItemCollectionValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteItemCollectionProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
col ItemCollection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteItemCollectionProp(tt.args.b, tt.args.n, tt.args.col, true); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteItemCollectionProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteItemProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
i Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteItemProp(tt.args.b, tt.args.n, tt.args.i); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteItemProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteNaturalLanguageProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
nl NaturalLanguageValues
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteNaturalLanguageProp(tt.args.b, tt.args.n, tt.args.nl); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteNaturalLanguageProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteObjectValue(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
o Object
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteObjectValue(tt.args.b, tt.args.o); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteObjectValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
name string
|
||||
val []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteProp(tt.args.b, tt.args.name, tt.args.val); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWritePropName(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWritePropName(tt.args.b, tt.args.s); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWritePropName() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteQuestionValue(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
q Question
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteQuestionValue(tt.args.b, tt.args.q); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteQuestionValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteStringValue(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteStringValue(tt.args.b, tt.args.s); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteStringValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteStringProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteStringProp(tt.args.b, tt.args.n, tt.args.s); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteStringProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteTimeProp(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
n string
|
||||
t time.Time
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteTimeProp(tt.args.b, tt.args.n, tt.args.t); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteTimeProp() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_JSONWriteValue(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
s []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if gotNotEmpty := JSONWriteValue(tt.args.b, tt.args.s); gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mockOb(id IRI, typ ActivityVocabularyType) LinkOrIRI {
|
||||
ob := ObjectNew(typ)
|
||||
ob.ID = id
|
||||
return ob
|
||||
}
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg LinkOrIRI
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
arg: nil,
|
||||
want: []byte("null"),
|
||||
},
|
||||
{
|
||||
name: "Link to example.com",
|
||||
arg: LinkNew("https://example.com", MentionType),
|
||||
want: []byte(`{"id":"https://example.com","type":"Mention"}`),
|
||||
},
|
||||
{
|
||||
name: "Note",
|
||||
arg: mockOb("https://example.com", NoteType),
|
||||
want: []byte(`{"id":"https://example.com","type":"Note"}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := MarshalJSON(tt.arg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MarshalJSON() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONWriteValue(t *testing.T) {
|
||||
buff := func(s int) *[]byte {
|
||||
b := make([]byte, 0, s)
|
||||
return &b
|
||||
}
|
||||
type args struct {
|
||||
b *[]byte
|
||||
s []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
b: buff(0),
|
||||
},
|
||||
wantNotEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "do not escape quotes at start-end",
|
||||
args: args{
|
||||
b: buff(20),
|
||||
s: []byte(`"https://example.com"`),
|
||||
},
|
||||
want: []byte(`"https://example.com"`),
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "escape quotes inside string",
|
||||
args: args{
|
||||
b: buff(80),
|
||||
s: []byte(`"application/ld+json; profile="https://www.w3.org/ns/activitystreams""`),
|
||||
},
|
||||
want: []byte(`"application/ld+json; profile="https://www.w3.org/ns/activitystreams""`),
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotNotEmpty := JSONWriteValue(tt.args.b, tt.args.s)
|
||||
if gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteStringValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
if tt.wantNotEmpty && !bytes.Equal(*tt.args.b, tt.want) {
|
||||
t.Errorf("JSONWriteValue() = %s, want %s", *tt.args.b, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buff(l int) *[]byte {
|
||||
b := make([]byte, 0, l)
|
||||
return &b
|
||||
}
|
||||
|
||||
func TestJSONWriteStringValue(t *testing.T) {
|
||||
type args struct {
|
||||
b *[]byte
|
||||
s string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantNotEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{},
|
||||
want: "",
|
||||
wantNotEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "escaped quote",
|
||||
args: args{
|
||||
b: buff(10),
|
||||
s: `ana"are`,
|
||||
},
|
||||
want: `"ana\"are"`,
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "already escaped quote",
|
||||
args: args{
|
||||
b: buff(10),
|
||||
s: `ana\"are`,
|
||||
},
|
||||
want: `"ana\"are"`,
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "already escaped quote and multiple other quotes",
|
||||
args: args{
|
||||
b: buff(10),
|
||||
s: `ana\"""are`,
|
||||
},
|
||||
want: `"ana\"\"\"are"`,
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "quote at the end",
|
||||
args: args{
|
||||
b: buff(10),
|
||||
s: `anaare"`,
|
||||
},
|
||||
want: `"anaare\""`,
|
||||
wantNotEmpty: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotNotEmpty := JSONWriteStringValue(tt.args.b, tt.args.s)
|
||||
if gotNotEmpty != tt.wantNotEmpty {
|
||||
t.Errorf("JSONWriteStringValue() = %v, want %v", gotNotEmpty, tt.wantNotEmpty)
|
||||
}
|
||||
if tt.wantNotEmpty && tt.want != string(*tt.args.b) {
|
||||
t.Errorf("JSONWriteStringValue() = %s, want %s", *tt.args.b, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
60
extractors.go
Normal file
60
extractors.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package activitypub
|
||||
|
||||
func PreferredNameOf(it Item) string {
|
||||
var cont string
|
||||
if IsObject(it) {
|
||||
_ = OnActor(it, func(act *Actor) error {
|
||||
if act.PreferredUsername != nil {
|
||||
cont = act.PreferredUsername.First().String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return cont
|
||||
}
|
||||
|
||||
func ContentOf(it Item) string {
|
||||
var cont string
|
||||
if IsObject(it) {
|
||||
_ = OnObject(it, func(ob *Object) error {
|
||||
if ob.Content != nil {
|
||||
cont = ob.Content.First().String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return cont
|
||||
}
|
||||
|
||||
func SummaryOf(it Item) string {
|
||||
var cont string
|
||||
if IsObject(it) {
|
||||
_ = OnObject(it, func(ob *Object) error {
|
||||
if ob.Summary != nil {
|
||||
cont = ob.Summary.First().String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return cont
|
||||
}
|
||||
|
||||
func NameOf(it Item) string {
|
||||
var name string
|
||||
if IsLink(it) {
|
||||
_ = OnLink(it, func(lnk *Link) error {
|
||||
if lnk.Name != nil {
|
||||
name = lnk.Name.First().String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
_ = OnObject(it, func(ob *Object) error {
|
||||
if ob.Name != nil {
|
||||
name = ob.Name.First().String()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return name
|
||||
}
|
87
extractors_test.go
Normal file
87
extractors_test.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
package activitypub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestContentOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg Item
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
arg: nil,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ContentOf(tt.arg); got != tt.want {
|
||||
t.Errorf("ContentOf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNameOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg Item
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
arg: nil,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := NameOf(tt.arg); got != tt.want {
|
||||
t.Errorf("NameOf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreferredNameOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg Item
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
arg: nil,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := PreferredNameOf(tt.arg); got != tt.want {
|
||||
t.Errorf("PreferredNameOf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummaryOf(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg Item
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
arg: nil,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := SummaryOf(tt.arg); got != tt.want {
|
||||
t.Errorf("SummaryOf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
142
flatten.go
Normal file
142
flatten.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
package activitypub
|
||||
|
||||
// FlattenActivityProperties flattens the Activity's properties from Object type to IRI
|
||||
func FlattenActivityProperties(act *Activity) *Activity {
|
||||
if act == nil {
|
||||
return nil
|
||||
}
|
||||
_ = OnIntransitiveActivity(act, func(in *IntransitiveActivity) error {
|
||||
FlattenIntransitiveActivityProperties(in)
|
||||
return nil
|
||||
})
|
||||
act.Object = FlattenToIRI(act.Object)
|
||||
return act
|
||||
}
|
||||
|
||||
// FlattenIntransitiveActivityProperties flattens the Activity's properties from Object type to IRI
|
||||
func FlattenIntransitiveActivityProperties(act *IntransitiveActivity) *IntransitiveActivity {
|
||||
if act == nil {
|
||||
return nil
|
||||
}
|
||||
act.Actor = FlattenToIRI(act.Actor)
|
||||
act.Target = FlattenToIRI(act.Target)
|
||||
act.Result = FlattenToIRI(act.Result)
|
||||
act.Origin = FlattenToIRI(act.Origin)
|
||||
act.Result = FlattenToIRI(act.Result)
|
||||
act.Instrument = FlattenToIRI(act.Instrument)
|
||||
_ = OnObject(act, func(o *Object) error {
|
||||
FlattenObjectProperties(o)
|
||||
return nil
|
||||
})
|
||||
return act
|
||||
}
|
||||
|
||||
// FlattenItemCollection flattens an Item Collection to their respective IRIs
|
||||
func FlattenItemCollection(col ItemCollection) ItemCollection {
|
||||
if col == nil {
|
||||
return col
|
||||
}
|
||||
for k, it := range ItemCollectionDeduplication(&col) {
|
||||
if iri := it.GetLink(); iri != "" {
|
||||
col[k] = iri
|
||||
}
|
||||
}
|
||||
return col
|
||||
}
|
||||
|
||||
// FlattenCollection flattens a Collection's objects to their respective IRIs
|
||||
func FlattenCollection(col *Collection) *Collection {
|
||||
if col == nil {
|
||||
return col
|
||||
}
|
||||
col.Items = FlattenItemCollection(col.Items)
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
// FlattenOrderedCollection flattens an OrderedCollection's objects to their respective IRIs
|
||||
func FlattenOrderedCollection(col *OrderedCollection) *OrderedCollection {
|
||||
if col == nil {
|
||||
return col
|
||||
}
|
||||
col.OrderedItems = FlattenItemCollection(col.OrderedItems)
|
||||
|
||||
return col
|
||||
}
|
||||
|
||||
// FlattenActorProperties flattens the Actor's properties from Object types to IRI
|
||||
func FlattenActorProperties(a *Actor) *Actor {
|
||||
if a == nil {
|
||||
return nil
|
||||
}
|
||||
OnObject(a, func(o *Object) error {
|
||||
FlattenObjectProperties(o)
|
||||
return nil
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// FlattenObjectProperties flattens the Object's properties from Object types to IRI
|
||||
func FlattenObjectProperties(o *Object) *Object {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
o.Replies = Flatten(o.Replies)
|
||||
o.Shares = Flatten(o.Shares)
|
||||
o.Likes = Flatten(o.Likes)
|
||||
o.AttributedTo = Flatten(o.AttributedTo)
|
||||
o.To = FlattenItemCollection(o.To)
|
||||
o.Bto = FlattenItemCollection(o.Bto)
|
||||
o.CC = FlattenItemCollection(o.CC)
|
||||
o.BCC = FlattenItemCollection(o.BCC)
|
||||
o.Audience = FlattenItemCollection(o.Audience)
|
||||
// o.Tag = FlattenItemCollection(o.Tag)
|
||||
return o
|
||||
}
|
||||
|
||||
// FlattenProperties flattens the Item's properties from Object types to IRI
|
||||
func FlattenProperties(it Item) Item {
|
||||
if IsNil(it) {
|
||||
return nil
|
||||
}
|
||||
typ := it.GetType()
|
||||
if IntransitiveActivityTypes.Contains(typ) {
|
||||
_ = OnIntransitiveActivity(it, func(a *IntransitiveActivity) error {
|
||||
FlattenIntransitiveActivityProperties(a)
|
||||
return nil
|
||||
})
|
||||
} else if ActivityTypes.Contains(typ) {
|
||||
_ = OnActivity(it, func(a *Activity) error {
|
||||
FlattenActivityProperties(a)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if ActorTypes.Contains(typ) {
|
||||
OnActor(it, func(a *Actor) error {
|
||||
FlattenActorProperties(a)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if ObjectTypes.Contains(typ) {
|
||||
OnObject(it, func(o *Object) error {
|
||||
FlattenObjectProperties(o)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
// Flatten checks if Item can be flattened to an IRI or array of IRIs and returns it if so
|
||||
func Flatten(it Item) Item {
|
||||
if IsNil(it) {
|
||||
return nil
|
||||
}
|
||||
if it.IsCollection() {
|
||||
OnCollectionIntf(it, func(c CollectionInterface) error {
|
||||
it = FlattenItemCollection(c.Collection()).Normalize()
|
||||
return nil
|
||||
})
|
||||
return it
|
||||
}
|
||||
return it.GetLink()
|
||||
}
|
89
flatten_test.go
Normal file
89
flatten_test.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlattenPersonProperties(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFlattenProperties(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFlattenItemCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFlattenCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFlattenOrderedCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestFlattenIntransitiveActivityProperties(t *testing.T) {
|
||||
type args struct {
|
||||
act *IntransitiveActivity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *IntransitiveActivity
|
||||
}{
|
||||
{
|
||||
name: "blank",
|
||||
args: args{&IntransitiveActivity{}},
|
||||
want: &IntransitiveActivity{},
|
||||
},
|
||||
{
|
||||
name: "flatten-actor",
|
||||
args: args{&IntransitiveActivity{Actor: &Actor{ID: "example-actor-iri"}}},
|
||||
want: &IntransitiveActivity{Actor: IRI("example-actor-iri")},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := FlattenIntransitiveActivityProperties(tt.args.act); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FlattenIntransitiveActivityProperties() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenActivityProperties(t *testing.T) {
|
||||
type args struct {
|
||||
act *Activity
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Activity
|
||||
}{
|
||||
{
|
||||
name: "blank",
|
||||
args: args{&Activity{}},
|
||||
want: &Activity{},
|
||||
},
|
||||
{
|
||||
name: "flatten-actor",
|
||||
args: args{&Activity{Actor: &Actor{ID: "example-actor-iri"}}},
|
||||
want: &Activity{Actor: IRI("example-actor-iri")},
|
||||
},
|
||||
{
|
||||
name: "flatten-object",
|
||||
args: args{&Activity{Object: &Object{ID: "example-actor-iri"}}},
|
||||
want: &Activity{Object: IRI("example-actor-iri")},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := FlattenActivityProperties(tt.args.act); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("FlattenActivityProperties() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
10
go.mod
Normal file
10
go.mod
Normal file
|
@ -0,0 +1,10 @@
|
|||
module github.com/go-ap/activitypub
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||
github.com/go-ap/errors v0.0.0-20250501090840-cd50c6a0a4e6
|
||||
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
github.com/valyala/fastjson v1.6.4
|
||||
)
|
571
helpers.go
Normal file
571
helpers.go
Normal file
|
@ -0,0 +1,571 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WithLinkFn represents a function type that can be used as a parameter for OnLink helper function
|
||||
type WithLinkFn func(*Link) error
|
||||
|
||||
// WithObjectFn represents a function type that can be used as a parameter for OnObject helper function
|
||||
type WithObjectFn func(*Object) error
|
||||
|
||||
// WithActivityFn represents a function type that can be used as a parameter for OnActivity helper function
|
||||
type WithActivityFn func(*Activity) error
|
||||
|
||||
// WithIntransitiveActivityFn represents a function type that can be used as a parameter for OnIntransitiveActivity helper function
|
||||
type WithIntransitiveActivityFn func(*IntransitiveActivity) error
|
||||
|
||||
// WithQuestionFn represents a function type that can be used as a parameter for OnQuestion helper function
|
||||
type WithQuestionFn func(*Question) error
|
||||
|
||||
// WithActorFn represents a function type that can be used as a parameter for OnActor helper function
|
||||
type WithActorFn func(*Actor) error
|
||||
|
||||
// WithCollectionInterfaceFn represents a function type that can be used as a parameter for OnCollectionIntf helper function
|
||||
type WithCollectionInterfaceFn func(CollectionInterface) error
|
||||
|
||||
// WithCollectionFn represents a function type that can be used as a parameter for OnCollection helper function
|
||||
type WithCollectionFn func(*Collection) error
|
||||
|
||||
// WithCollectionPageFn represents a function type that can be used as a parameter for OnCollectionPage helper function
|
||||
type WithCollectionPageFn func(*CollectionPage) error
|
||||
|
||||
// WithOrderedCollectionFn represents a function type that can be used as a parameter for OnOrderedCollection helper function
|
||||
type WithOrderedCollectionFn func(*OrderedCollection) error
|
||||
|
||||
// WithOrderedCollectionPageFn represents a function type that can be used as a parameter for OnOrderedCollectionPage helper function
|
||||
type WithOrderedCollectionPageFn func(*OrderedCollectionPage) error
|
||||
|
||||
// WithItemCollectionFn represents a function type that can be used as a parameter for OnItemCollection helper function
|
||||
type WithItemCollectionFn func(*ItemCollection) error
|
||||
|
||||
// WithIRIsFn represents a function type that can be used as a parameter for OnIRIs helper function
|
||||
type WithIRIsFn func(*IRIs) error
|
||||
|
||||
// OnLink calls function fn on it Item if it can be asserted to type *Link
|
||||
//
|
||||
// This function should be safe to use for all types with a structure compatible
|
||||
// with the Link type
|
||||
func OnLink(it LinkOrIRI, fn WithLinkFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToLink(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
||||
|
||||
func To[T Item](it Item) (*T, error) {
|
||||
if ob, ok := it.(T); ok {
|
||||
return &ob, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid cast for object %T", it)
|
||||
}
|
||||
|
||||
// On handles in a generic way the call to fn(*T) if the "it" Item can be asserted to one of the Objects type.
|
||||
// It also covers the case where "it" is a collection of items that match the assertion.
|
||||
func On[T Item](it Item, fn func(*T) error) error {
|
||||
if !IsItemCollection(it) {
|
||||
ob, err := To[T](it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := On(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// OnObject calls function fn on it Item if it can be asserted to type *Object
|
||||
//
|
||||
// This function should be safe to be called for all types with a structure compatible
|
||||
// to the Object type.
|
||||
func OnObject(it Item, fn WithObjectFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnObject(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToObject(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
||||
|
||||
// OnActivity calls function fn on it Item if it can be asserted to type *Activity
|
||||
//
|
||||
// This function should be called if trying to access the Activity specific properties
|
||||
// like "object", for the other properties OnObject, or OnIntransitiveActivity
|
||||
// should be used instead.
|
||||
func OnActivity(it Item, fn WithActivityFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnActivity(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
act, err := ToActivity(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(act)
|
||||
}
|
||||
|
||||
// OnIntransitiveActivity calls function fn on it Item if it can be asserted
|
||||
// to type *IntransitiveActivity
|
||||
//
|
||||
// This function should be called if trying to access the IntransitiveActivity
|
||||
// specific properties like "actor", for the other properties OnObject
|
||||
// should be used instead.
|
||||
func OnIntransitiveActivity(it Item, fn WithIntransitiveActivityFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := OnIntransitiveActivity(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
act, err := ToIntransitiveActivity(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(act)
|
||||
}
|
||||
|
||||
// OnQuestion calls function fn on it Item if it can be asserted to type Question
|
||||
//
|
||||
// This function should be called if trying to access the Questions specific
|
||||
// properties like "anyOf", "oneOf", "closed", etc. For the other properties
|
||||
// OnObject or OnIntransitiveActivity should be used instead.
|
||||
func OnQuestion(it Item, fn WithQuestionFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := OnQuestion(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
act, err := ToQuestion(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(act)
|
||||
}
|
||||
|
||||
// OnActor calls function fn on it Item if it can be asserted to type *Actor
|
||||
//
|
||||
// This function should be called if trying to access the Actor specific
|
||||
// properties like "preferredName", "publicKey", etc. For the other properties
|
||||
// OnObject should be used instead.
|
||||
func OnActor(it Item, fn WithActorFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnActor(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
act, err := ToActor(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(act)
|
||||
}
|
||||
|
||||
// OnItemCollection calls function fn on it Item if it can be asserted to type ItemCollection
|
||||
//
|
||||
// It should be used when Item represents an Item collection and it's usually used as a way
|
||||
// to wrap functionality for other functions that will be called on each item in the collection.
|
||||
func OnItemCollection(it Item, fn WithItemCollectionFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToItemCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// OnIRIs calls function fn on it Item if it can be asserted to type IRIs
|
||||
//
|
||||
// It should be used when Item represents an IRI slice.
|
||||
func OnIRIs(it Item, fn WithIRIsFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToIRIs(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// OnCollectionIntf calls function fn on it Item if it can be asserted to a type
|
||||
// that implements the CollectionInterface
|
||||
//
|
||||
// This function should be called if Item represents a collection of ActivityPub
|
||||
// objects. It basically wraps functionality for the different collection types
|
||||
// supported by the package.
|
||||
func OnCollectionIntf(it Item, fn WithCollectionInterfaceFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
switch it.GetType() {
|
||||
case CollectionOfItems:
|
||||
col, err := ToItemCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
case CollectionOfIRIs:
|
||||
col, err := ToIRIs(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itCol := col.Collection()
|
||||
return fn(&itCol)
|
||||
case CollectionType:
|
||||
col, err := ToCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
case CollectionPageType:
|
||||
return OnCollectionPage(it, func(p *CollectionPage) error {
|
||||
col, err := ToCollectionPage(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
})
|
||||
case OrderedCollectionType:
|
||||
col, err := ToOrderedCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
case OrderedCollectionPageType:
|
||||
return OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error {
|
||||
col, err := ToOrderedCollectionPage(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("%T[%s] can't be converted to a Collection type", it, it.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
// OnCollection calls function fn on it Item if it can be asserted to type *Collection
|
||||
//
|
||||
// This function should be called if trying to access the Collection specific
|
||||
// properties like "totalItems", "items", etc. For the other properties
|
||||
// OnObject should be used instead.
|
||||
func OnCollection(it Item, fn WithCollectionFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// OnCollectionPage calls function fn on it Item if it can be asserted to
|
||||
// type *CollectionPage
|
||||
//
|
||||
// This function should be called if trying to access the CollectionPage specific
|
||||
// properties like "partOf", "next", "perv". For the other properties
|
||||
// OnObject or OnCollection should be used instead.
|
||||
func OnCollectionPage(it Item, fn WithCollectionPageFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToCollectionPage(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// OnOrderedCollection calls function fn on it Item if it can be asserted
|
||||
// to type *OrderedCollection
|
||||
//
|
||||
// This function should be called if trying to access the Collection specific
|
||||
// properties like "totalItems", "orderedItems", etc. For the other properties
|
||||
// OnObject should be used instead.
|
||||
func OnOrderedCollection(it Item, fn WithOrderedCollectionFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToOrderedCollection(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// OnOrderedCollectionPage calls function fn on it Item if it can be asserted
|
||||
// to type *OrderedCollectionPage
|
||||
//
|
||||
// This function should be called if trying to access the OrderedCollectionPage specific
|
||||
// properties like "partOf", "next", "perv". For the other properties
|
||||
// OnObject or OnOrderedCollection should be used instead.
|
||||
func OnOrderedCollectionPage(it Item, fn WithOrderedCollectionPageFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
col, err := ToOrderedCollectionPage(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(col)
|
||||
}
|
||||
|
||||
// ItemOrderTimestamp is used for ordering a ItemCollection slice using the slice.Sort function
|
||||
// It orders i1 and i2 based on their Published and Updated timestamps, whichever is later.
|
||||
func ItemOrderTimestamp(i1, i2 LinkOrIRI) bool {
|
||||
if IsNil(i1) {
|
||||
return !IsNil(i2)
|
||||
} else if IsNil(i2) {
|
||||
return false
|
||||
}
|
||||
|
||||
var t1 time.Time
|
||||
var t2 time.Time
|
||||
if IsObject(i1) {
|
||||
o1, e1 := ToObject(i1)
|
||||
if e1 != nil {
|
||||
return false
|
||||
}
|
||||
t1 = o1.Published
|
||||
if o1.Updated.After(t1) {
|
||||
t1 = o1.Updated
|
||||
}
|
||||
}
|
||||
if IsObject(i2) {
|
||||
o2, e2 := ToObject(i2)
|
||||
if e2 != nil {
|
||||
return false
|
||||
}
|
||||
t2 = o2.Published
|
||||
if o2.Updated.After(t2) {
|
||||
t2 = o2.Updated
|
||||
}
|
||||
}
|
||||
return t1.After(t2)
|
||||
}
|
||||
|
||||
func notEmptyLink(l *Link) bool {
|
||||
return len(l.ID) > 0 ||
|
||||
LinkTypes.Contains(l.Type) ||
|
||||
len(l.MediaType) > 0 ||
|
||||
l.Preview != nil ||
|
||||
l.Name != nil ||
|
||||
len(l.Href) > 0 ||
|
||||
len(l.Rel) > 0 ||
|
||||
len(l.HrefLang) > 0 ||
|
||||
l.Height > 0 ||
|
||||
l.Width > 0
|
||||
}
|
||||
|
||||
func notEmptyObject(o *Object) bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
return len(o.ID) > 0 ||
|
||||
len(o.Type) > 0 ||
|
||||
ActivityTypes.Contains(o.Type) ||
|
||||
o.Content != nil ||
|
||||
o.Attachment != nil ||
|
||||
o.AttributedTo != nil ||
|
||||
o.Audience != nil ||
|
||||
o.BCC != nil ||
|
||||
o.Bto != nil ||
|
||||
o.CC != nil ||
|
||||
o.Context != nil ||
|
||||
o.Duration > 0 ||
|
||||
!o.EndTime.IsZero() ||
|
||||
o.Generator != nil ||
|
||||
o.Icon != nil ||
|
||||
o.Image != nil ||
|
||||
o.InReplyTo != nil ||
|
||||
o.Likes != nil ||
|
||||
o.Location != nil ||
|
||||
len(o.MediaType) > 0 ||
|
||||
o.Name != nil ||
|
||||
o.Preview != nil ||
|
||||
!o.Published.IsZero() ||
|
||||
o.Replies != nil ||
|
||||
o.Shares != nil ||
|
||||
o.Source.MediaType != "" ||
|
||||
o.Source.Content != nil ||
|
||||
!o.StartTime.IsZero() ||
|
||||
o.Summary != nil ||
|
||||
o.Tag != nil ||
|
||||
o.To != nil ||
|
||||
!o.Updated.IsZero() ||
|
||||
o.URL != nil
|
||||
}
|
||||
|
||||
func notEmptyInstransitiveActivity(i *IntransitiveActivity) bool {
|
||||
notEmpty := i.Actor != nil ||
|
||||
i.Target != nil ||
|
||||
i.Result != nil ||
|
||||
i.Origin != nil ||
|
||||
i.Instrument != nil
|
||||
if notEmpty {
|
||||
return true
|
||||
}
|
||||
OnObject(i, func(ob *Object) error {
|
||||
notEmpty = notEmptyObject(ob)
|
||||
return nil
|
||||
})
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
func notEmptyActivity(a *Activity) bool {
|
||||
var notEmpty bool
|
||||
OnIntransitiveActivity(a, func(i *IntransitiveActivity) error {
|
||||
notEmpty = notEmptyInstransitiveActivity(i)
|
||||
return nil
|
||||
})
|
||||
return notEmpty || a.Object != nil
|
||||
}
|
||||
|
||||
func notEmptyActor(a *Actor) bool {
|
||||
var notEmpty bool
|
||||
OnObject(a, func(o *Object) error {
|
||||
notEmpty = notEmptyObject(o)
|
||||
return nil
|
||||
})
|
||||
return notEmpty ||
|
||||
a.Inbox != nil ||
|
||||
a.Outbox != nil ||
|
||||
a.Following != nil ||
|
||||
a.Followers != nil ||
|
||||
a.Liked != nil ||
|
||||
a.PreferredUsername != nil ||
|
||||
a.Endpoints != nil ||
|
||||
a.Streams != nil ||
|
||||
len(a.PublicKey.ID)+len(a.PublicKey.Owner)+len(a.PublicKey.PublicKeyPem) > 0
|
||||
}
|
||||
|
||||
// NotEmpty tells us if a Item interface value has a non nil value for various types
|
||||
// that implement
|
||||
func NotEmpty(i Item) bool {
|
||||
if IsNil(i) {
|
||||
return false
|
||||
}
|
||||
var notEmpty bool
|
||||
if IsIRI(i) {
|
||||
notEmpty = len(i.GetLink()) > 0
|
||||
}
|
||||
if i.IsCollection() {
|
||||
OnCollectionIntf(i, func(c CollectionInterface) error {
|
||||
notEmpty = c != nil || len(c.Collection()) > 0
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if ActivityTypes.Contains(i.GetType()) {
|
||||
OnActivity(i, func(a *Activity) error {
|
||||
notEmpty = notEmptyActivity(a)
|
||||
return nil
|
||||
})
|
||||
} else if ActorTypes.Contains(i.GetType()) {
|
||||
OnActor(i, func(a *Actor) error {
|
||||
notEmpty = notEmptyActor(a)
|
||||
return nil
|
||||
})
|
||||
} else if i.IsLink() {
|
||||
OnLink(i, func(l *Link) error {
|
||||
notEmpty = notEmptyLink(l)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
OnObject(i, func(o *Object) error {
|
||||
notEmpty = notEmptyObject(o)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return notEmpty
|
||||
}
|
||||
|
||||
// DerefItem dereferences
|
||||
func DerefItem(it Item) ItemCollection {
|
||||
if IsNil(it) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var items ItemCollection
|
||||
if IsIRIs(it) {
|
||||
_ = OnIRIs(it, func(col *IRIs) error {
|
||||
items = col.Collection()
|
||||
return nil
|
||||
})
|
||||
} else if IsItemCollection(it) {
|
||||
_ = OnItemCollection(it, func(col *ItemCollection) error {
|
||||
items = col.Collection()
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
items = ItemCollection{it}
|
||||
}
|
||||
return items
|
||||
}
|
679
helpers_test.go
Normal file
679
helpers_test.go
Normal file
|
@ -0,0 +1,679 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func assertObjectWithTesting(fn canErrorFunc, expected Item) WithObjectFn {
|
||||
return func(p *Object) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnObject(t *testing.T) {
|
||||
testObject := Object{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, Item) WithObjectFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testObject, assertObjectWithTesting},
|
||||
expected: &testObject,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{Object{ID: "https://not-equals"}, assertObjectWithTesting},
|
||||
expected: &testObject,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfObjects",
|
||||
args: args{ItemCollection{testObject, testObject}, assertObjectWithTesting},
|
||||
expected: &testObject,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfObjects fails",
|
||||
args: args{ItemCollection{testObject, Object{ID: "https://not-equals"}}, assertObjectWithTesting},
|
||||
expected: &testObject,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnObject(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnObject() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertActivityWithTesting(fn canErrorFunc, expected Item) WithActivityFn {
|
||||
return func(p *Activity) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnActivity(t *testing.T) {
|
||||
testActivity := Activity{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, Item) WithActivityFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testActivity, assertActivityWithTesting},
|
||||
expected: &testActivity,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{Activity{ID: "https://not-equals"}, assertActivityWithTesting},
|
||||
expected: &testActivity,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfActivitys",
|
||||
args: args{ItemCollection{testActivity, testActivity}, assertActivityWithTesting},
|
||||
expected: &testActivity,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfActivitys fails",
|
||||
args: args{ItemCollection{testActivity, Activity{ID: "https://not-equals"}}, assertActivityWithTesting},
|
||||
expected: &testActivity,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnActivity(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnActivity() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertIntransitiveActivityWithTesting(fn canErrorFunc, expected Item) WithIntransitiveActivityFn {
|
||||
return func(p *IntransitiveActivity) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnIntransitiveActivity(t *testing.T) {
|
||||
testIntransitiveActivity := IntransitiveActivity{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, Item) WithIntransitiveActivityFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testIntransitiveActivity, assertIntransitiveActivityWithTesting},
|
||||
expected: &testIntransitiveActivity,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{IntransitiveActivity{ID: "https://not-equals"}, assertIntransitiveActivityWithTesting},
|
||||
expected: &testIntransitiveActivity,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfIntransitiveActivitys",
|
||||
args: args{ItemCollection{testIntransitiveActivity, testIntransitiveActivity}, assertIntransitiveActivityWithTesting},
|
||||
expected: &testIntransitiveActivity,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfIntransitiveActivitys fails",
|
||||
args: args{ItemCollection{testIntransitiveActivity, IntransitiveActivity{ID: "https://not-equals"}}, assertIntransitiveActivityWithTesting},
|
||||
expected: &testIntransitiveActivity,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnIntransitiveActivity(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnIntransitiveActivity() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertQuestionWithTesting(fn canErrorFunc, expected Item) WithQuestionFn {
|
||||
return func(p *Question) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnQuestion(t *testing.T) {
|
||||
testQuestion := Question{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, Item) WithQuestionFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expected Item
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testQuestion, assertQuestionWithTesting},
|
||||
expected: &testQuestion,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{Question{ID: "https://not-equals"}, assertQuestionWithTesting},
|
||||
expected: &testQuestion,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfQuestions",
|
||||
args: args{ItemCollection{testQuestion, testQuestion}, assertQuestionWithTesting},
|
||||
expected: &testQuestion,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfQuestions fails",
|
||||
args: args{ItemCollection{testQuestion, Question{ID: "https://not-equals"}}, assertQuestionWithTesting},
|
||||
expected: &testQuestion,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnQuestion(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnQuestion() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOnCollectionPage(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOnOrderedCollectionPage(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
type args[T Objects] struct {
|
||||
it T
|
||||
fn func(fn canErrorFunc, expected T) func(*T) error
|
||||
}
|
||||
|
||||
type testPair[T Objects] struct {
|
||||
name string
|
||||
args args[T]
|
||||
expected T
|
||||
wantErr bool
|
||||
}
|
||||
|
||||
func assert[T Objects](fn canErrorFunc, expected T) func(*T) error {
|
||||
return func(p *T) error {
|
||||
if !assertDeepEquals(fn, *p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOn(t *testing.T) {
|
||||
var tests = []testPair[Object]{
|
||||
{
|
||||
name: "single object",
|
||||
args: args[Object]{Object{ID: "https://example.com"}, assert[Object]},
|
||||
expected: Object{ID: "https://example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single image",
|
||||
args: args[Image]{Image{ID: "http://example.com"}, assert[Image]},
|
||||
expected: Image{ID: "http://example.com"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := On(tt.args.it, tt.args.fn(logFn, tt.expected)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("On[%T]() error = %v, wantErr %v", tt.args.it, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
emptyPrintFn = func(string, ...any) {}
|
||||
|
||||
fnPrintObj = func(printFn func(string, ...any)) func(_ *Object) error {
|
||||
return func(o *Object) error {
|
||||
printFn("%v", o)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fnObj = func(_ *Object) error { return nil }
|
||||
fnAct = func(_ *Actor) error { return nil }
|
||||
fnA = func(_ *Activity) error { return nil }
|
||||
fnIA = func(_ *IntransitiveActivity) error { return nil }
|
||||
|
||||
maybeObject Item = new(Object)
|
||||
notObject Item = new(Activity)
|
||||
maybeActor Item = new(Actor)
|
||||
maybeActivity Item = new(Activity)
|
||||
notIntransitiveActivity Item = new(Activity)
|
||||
maybeIntransitiveActivity Item = new(IntransitiveActivity)
|
||||
colOfObjects Item = ItemCollection{Object{ID: "unum"}, Object{ID: "duo"}}
|
||||
colOfNotObjects Item = ItemCollection{Activity{ID: "unum"}, Activity{ID: "duo"}}
|
||||
)
|
||||
|
||||
func Benchmark_ToObject(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToObject(maybeObject)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_To_T_Object(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
To[Object](maybeObject)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_ToActor(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToActor(maybeActor)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_To_T_Actor(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
To[Actor](maybeActor)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_ToActivity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToActivity(maybeActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_To_T_Activity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
To[Activity](maybeActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_ToIntransitiveActivityHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToIntransitiveActivity(maybeIntransitiveActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_To_T_IntransitiveActivityHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
To[IntransitiveActivity](maybeIntransitiveActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_ToIntransitiveActivityNotHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ToIntransitiveActivity(notIntransitiveActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_To_T_IntransitiveActivityNotHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
To[IntransitiveActivity](notIntransitiveActivity)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_OnObject(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnObject(maybeObject, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_Object(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Object](maybeObject, fnObj)
|
||||
}
|
||||
}
|
||||
func Benchmark_OnActor(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnActor(maybeObject, fnAct)
|
||||
}
|
||||
}
|
||||
func Benchmark_On_T_Actor(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Actor](maybeObject, fnAct)
|
||||
}
|
||||
}
|
||||
func Benchmark_OnActivity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnActivity(maybeObject, fnA)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_Activity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Activity](maybeObject, fnA)
|
||||
}
|
||||
}
|
||||
func Benchmark_OnIntransitiveActivity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnIntransitiveActivity(maybeObject, fnIA)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_IntransitiveActivity(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[IntransitiveActivity](maybeObject, fnIA)
|
||||
}
|
||||
}
|
||||
func Benchmark_OnObjectNotHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnObject(notObject, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_ObjectNotHappy(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Object](notObject, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_OnObjectHappyCol(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnObject(colOfObjects, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_ObjectHappyCol(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Object](colOfObjects, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_OnObjectNotHappyCol(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
OnObject(colOfNotObjects, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_On_T_ObjectNotHappyCol(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
On[Object](colOfNotObjects, fnObj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDerefItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
arg Item
|
||||
want ItemCollection
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "simple object",
|
||||
arg: &Object{ID: "https://example.com"},
|
||||
want: ItemCollection{&Object{ID: "https://example.com"}},
|
||||
},
|
||||
{
|
||||
name: "simple IRI",
|
||||
arg: IRI("https://example.com"),
|
||||
want: ItemCollection{IRI("https://example.com")},
|
||||
},
|
||||
{
|
||||
name: "IRI collection",
|
||||
arg: IRIs{IRI("https://example.com"), IRI("https://example.com/~jdoe")},
|
||||
want: ItemCollection{IRI("https://example.com"), IRI("https://example.com/~jdoe")},
|
||||
},
|
||||
{
|
||||
name: "Item collection",
|
||||
arg: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
want: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mixed item collection",
|
||||
arg: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
IRI("https://example.com/666"),
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
want: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
IRI("https://example.com/666"),
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := DerefItem(tt.arg); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("DerefItem() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemOrderTimestamp(t *testing.T) {
|
||||
early1 := time.Date(2001, 6, 6, 6, 0, 0, 0, time.UTC)
|
||||
early2 := time.Date(2001, 6, 6, 6, 0, 1, 0, time.UTC)
|
||||
late1 := time.Date(2001, 6, 6, 7, 0, 0, 0, time.UTC)
|
||||
late2 := time.Date(2001, 6, 6, 7, 0, 1, 0, time.UTC)
|
||||
type args struct {
|
||||
i1 Item
|
||||
i2 Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "first empty",
|
||||
args: args{
|
||||
i1: nil,
|
||||
i2: &Object{},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "second empty",
|
||||
args: args{
|
||||
i1: &Object{},
|
||||
i2: nil,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty published/updated",
|
||||
args: args{
|
||||
i1: &Object{},
|
||||
i2: &Object{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "first has empty published/updated",
|
||||
args: args{
|
||||
i1: &Object{},
|
||||
i2: &Object{Published: early1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check published equals",
|
||||
args: args{
|
||||
i1: &Object{Published: early1},
|
||||
i2: &Object{Published: early1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check published/updated equals",
|
||||
args: args{
|
||||
i1: &Object{Published: early2},
|
||||
i2: &Object{Updated: early2},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check updated/published equals",
|
||||
args: args{
|
||||
i1: &Object{Updated: late1},
|
||||
i2: &Object{Published: late1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check first published earlier",
|
||||
args: args{
|
||||
i1: &Object{Published: early1},
|
||||
i2: &Object{Published: late1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check second published earlier",
|
||||
args: args{
|
||||
i1: &Object{Published: late1},
|
||||
i2: &Object{Published: early1},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "check first updated earlier",
|
||||
args: args{
|
||||
i1: &Object{Updated: early1},
|
||||
i2: &Object{Updated: late1},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check second updated earlier",
|
||||
args: args{
|
||||
i1: &Object{Updated: late1},
|
||||
i2: &Object{Updated: early1},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "check first earlier",
|
||||
args: args{
|
||||
i1: &Object{Published: early1, Updated: late1},
|
||||
i2: &Object{Published: early1, Updated: late2},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "check second earlier",
|
||||
args: args{
|
||||
i1: &Object{Published: early1, Updated: late2},
|
||||
i2: &Object{Published: early1, Updated: late1},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ItemOrderTimestamp(tt.args.i1, tt.args.i2); got != tt.want {
|
||||
t.Errorf("ItemOrderTimestamp() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
377
intransitive_activity.go
Normal file
377
intransitive_activity.go
Normal file
|
@ -0,0 +1,377 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
type IntransitiveActivities interface {
|
||||
IntransitiveActivity | Question
|
||||
}
|
||||
|
||||
// IntransitiveActivity Instances of IntransitiveActivity are a subtype of Activity representing intransitive actions.
|
||||
// The object property is therefore inappropriate for these activities.
|
||||
type IntransitiveActivity struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
|
||||
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
|
||||
Actor CanReceiveActivities `jsonld:"actor,omitempty"`
|
||||
// Target describes the indirect object, or target, of the activity.
|
||||
// The precise meaning of the target is largely dependent on the type of action being described
|
||||
// but will often be the object of the English preposition "to".
|
||||
// For instance, in the activity "John added a movie to his wishlist",
|
||||
// the target of the activity is John's wishlist. An activity can have more than one target.
|
||||
Target Item `jsonld:"target,omitempty"`
|
||||
// Result describes the result of the activity. For instance, if a particular action results in the creation
|
||||
// of a new resource, the result property can be used to describe that new resource.
|
||||
Result Item `jsonld:"result,omitempty"`
|
||||
// Origin describes an indirect object of the activity from which the activity is directed.
|
||||
// The precise meaning of the origin is the object of the English preposition "from".
|
||||
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
|
||||
Origin Item `jsonld:"origin,omitempty"`
|
||||
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
|
||||
Instrument Item `jsonld:"instrument,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
// Arrive is an IntransitiveActivity that indicates that the actor has arrived at the location.
|
||||
// The origin can be used to identify the context from which the actor originated.
|
||||
// The target typically has no defined meaning.
|
||||
Arrive = IntransitiveActivity
|
||||
|
||||
// Travel indicates that the actor is traveling to target from origin.
|
||||
// Travel is an IntransitiveObject whose actor specifies the direct object.
|
||||
// If the target or origin are not specified, either can be determined by context.
|
||||
Travel = IntransitiveActivity
|
||||
)
|
||||
|
||||
// Recipients performs recipient de-duplication on the IntransitiveActivity's To, Bto, CC and BCC properties
|
||||
func (i *IntransitiveActivity) Recipients() ItemCollection {
|
||||
aud := i.Audience
|
||||
return ItemCollectionDeduplication(&i.To, &i.CC, &i.Bto, &i.BCC, &ItemCollection{i.Actor}, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (i *IntransitiveActivity) Clean() {
|
||||
_ = OnObject(i, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetType returns the ActivityVocabulary type of the current Intransitive Activity
|
||||
func (i IntransitiveActivity) GetType() ActivityVocabularyType {
|
||||
return i.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for Activity objects
|
||||
func (i IntransitiveActivity) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the IntransitiveActivity object
|
||||
func (i IntransitiveActivity) GetID() ID {
|
||||
return i.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the IntransitiveActivity object
|
||||
func (i IntransitiveActivity) GetLink() IRI {
|
||||
return IRI(i.ID)
|
||||
}
|
||||
|
||||
// IsObject returns true for IntransitiveActivity objects
|
||||
func (i IntransitiveActivity) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for IntransitiveActivity objects
|
||||
func (i IntransitiveActivity) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (i *IntransitiveActivity) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadIntransitiveActivity(val, i)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (i IntransitiveActivity) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if !JSONWriteIntransitiveActivityValue(&b, i) {
|
||||
return nil, nil
|
||||
}
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (i *IntransitiveActivity) UnmarshalBinary(data []byte) error {
|
||||
return i.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (i IntransitiveActivity) MarshalBinary() ([]byte, error) {
|
||||
return i.GobEncode()
|
||||
}
|
||||
|
||||
func (i IntransitiveActivity) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapIntransitiveActivityProperties(mm, &i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (i *IntransitiveActivity) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapIntransitiveActivityProperties(mm, i)
|
||||
}
|
||||
|
||||
// IntransitiveActivityNew initializes a intransitive activity
|
||||
func IntransitiveActivityNew(id ID, typ ActivityVocabularyType) *IntransitiveActivity {
|
||||
if !IntransitiveActivityTypes.Contains(typ) {
|
||||
typ = IntransitiveActivityType
|
||||
}
|
||||
i := IntransitiveActivity{ID: id, Type: typ}
|
||||
i.Name = NaturalLanguageValuesNew()
|
||||
i.Content = NaturalLanguageValuesNew()
|
||||
|
||||
return &i
|
||||
}
|
||||
|
||||
// ToIntransitiveActivity tries to convert it Item to an IntransitiveActivity object
|
||||
func ToIntransitiveActivity(it Item) (*IntransitiveActivity, error) {
|
||||
switch i := it.(type) {
|
||||
case *IntransitiveActivity:
|
||||
return i, nil
|
||||
case IntransitiveActivity:
|
||||
return &i, nil
|
||||
case *Question:
|
||||
return (*IntransitiveActivity)(unsafe.Pointer(i)), nil
|
||||
case Question:
|
||||
return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil
|
||||
case *Activity:
|
||||
return (*IntransitiveActivity)(unsafe.Pointer(i)), nil
|
||||
case Activity:
|
||||
return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[IntransitiveActivity](it)
|
||||
}
|
||||
}
|
||||
|
||||
// ArriveNew initializes an Arrive activity
|
||||
func ArriveNew(id ID) *Arrive {
|
||||
a := IntransitiveActivityNew(id, ArriveType)
|
||||
o := Arrive(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// TravelNew initializes a Travel activity
|
||||
func TravelNew(id ID) *Travel {
|
||||
a := IntransitiveActivityNew(id, TravelType)
|
||||
o := Travel(*a)
|
||||
return &o
|
||||
}
|
||||
|
||||
// Equals verifies if our receiver Object is equals with the "with" Object
|
||||
func (i IntransitiveActivity) Equals(with Item) bool {
|
||||
result := true
|
||||
err := OnIntransitiveActivity(with, func(w *IntransitiveActivity) error {
|
||||
_ = OnObject(i, func(oa *Object) error {
|
||||
result = oa.Equals(w)
|
||||
return nil
|
||||
})
|
||||
if w.Actor != nil {
|
||||
if !ItemsEqual(i.Actor, w.Actor) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Target != nil {
|
||||
if !ItemsEqual(i.Target, w.Target) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Result != nil {
|
||||
if !ItemsEqual(i.Result, w.Result) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Origin != nil {
|
||||
if !ItemsEqual(i.Origin, w.Origin) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Instrument != nil {
|
||||
if !ItemsEqual(i.Instrument, w.Instrument) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (i IntransitiveActivity) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
if i.Type != "" && i.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T[%s]( %s )", i, i.Type, i.ID)
|
||||
} else if i.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T( %s )", i, i.ID)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%T[%p]", i, &i)
|
||||
}
|
||||
case 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] {", i, i.Type)
|
||||
_ = fmtIntransitiveActivityProps(s)(&i)
|
||||
_, _ = io.WriteString(s, " }")
|
||||
}
|
||||
}
|
||||
|
||||
func fmtIntransitiveActivityProps(w io.Writer) func(*IntransitiveActivity) error {
|
||||
return func(ia *IntransitiveActivity) error {
|
||||
if !IsNil(ia.Actor) {
|
||||
_, _ = fmt.Fprintf(w, " actor: %s", ia.Actor)
|
||||
}
|
||||
if !IsNil(ia.Target) {
|
||||
_, _ = fmt.Fprintf(w, " target: %s", ia.Target)
|
||||
}
|
||||
if !IsNil(ia.Result) {
|
||||
_, _ = fmt.Fprintf(w, " result: %s", ia.Result)
|
||||
}
|
||||
if !IsNil(ia.Origin) {
|
||||
_, _ = fmt.Fprintf(w, " origin: %s", ia.Origin)
|
||||
}
|
||||
if !IsNil(ia.Instrument) {
|
||||
_, _ = fmt.Fprintf(w, " instrument: %s", ia.Instrument)
|
||||
}
|
||||
return OnObject(ia, fmtObjectProps(w))
|
||||
}
|
||||
}
|
391
intransitive_activity_test.go
Normal file
391
intransitive_activity_test.go
Normal file
|
@ -0,0 +1,391 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIntransitiveActivityNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
var testType ActivityVocabularyType = "Arrive"
|
||||
|
||||
a := IntransitiveActivityNew(testValue, testType)
|
||||
|
||||
if a.ID != testValue {
|
||||
t.Errorf("IntransitiveActivity Id '%v' different than expected '%v'", a.ID, testValue)
|
||||
}
|
||||
if a.Type != testType {
|
||||
t.Errorf("IntransitiveActivity Type '%v' different than expected '%v'", a.Type, testType)
|
||||
}
|
||||
|
||||
g := IntransitiveActivityNew(testValue, "")
|
||||
|
||||
if g.ID != testValue {
|
||||
t.Errorf("IntransitiveActivity Id '%v' different than expected '%v'", g.ID, testValue)
|
||||
}
|
||||
if g.Type != IntransitiveActivityType {
|
||||
t.Errorf("IntransitiveActivity Type '%v' different than expected '%v'", g.Type, IntransitiveActivityType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivityRecipients(t *testing.T) {
|
||||
bob := PersonNew("bob")
|
||||
alice := PersonNew("alice")
|
||||
foo := OrganizationNew("foo")
|
||||
bar := GroupNew("bar")
|
||||
|
||||
a := IntransitiveActivityNew("test", "t")
|
||||
|
||||
a.To.Append(bob)
|
||||
a.To.Append(alice)
|
||||
a.To.Append(foo)
|
||||
a.To.Append(bar)
|
||||
if len(a.To) != 4 {
|
||||
t.Errorf("%T.To should have exactly 4(four) elements, not %d", a, len(a.To))
|
||||
}
|
||||
|
||||
a.To.Append(bar)
|
||||
a.To.Append(alice)
|
||||
a.To.Append(foo)
|
||||
a.To.Append(bob)
|
||||
if len(a.To) != 4 {
|
||||
t.Errorf("%T.To should have exactly 4(eight) elements, not %d", a, len(a.To))
|
||||
}
|
||||
|
||||
a.Recipients()
|
||||
if len(a.To) != 4 {
|
||||
t.Errorf("%T.To should have exactly 4(four) elements, not %d", a, len(a.To))
|
||||
}
|
||||
|
||||
b := ActivityNew("t", "test", nil)
|
||||
|
||||
b.To.Append(bar)
|
||||
b.To.Append(alice)
|
||||
b.To.Append(foo)
|
||||
b.To.Append(bob)
|
||||
b.Bto.Append(bar)
|
||||
b.Bto.Append(alice)
|
||||
b.Bto.Append(foo)
|
||||
b.Bto.Append(bob)
|
||||
b.CC.Append(bar)
|
||||
b.CC.Append(alice)
|
||||
b.CC.Append(foo)
|
||||
b.CC.Append(bob)
|
||||
b.BCC.Append(bar)
|
||||
b.BCC.Append(alice)
|
||||
b.BCC.Append(foo)
|
||||
b.BCC.Append(bob)
|
||||
|
||||
b.Recipients()
|
||||
if len(b.To) != 4 {
|
||||
t.Errorf("%T.To should have exactly 4(four) elements, not %d", b, len(b.To))
|
||||
}
|
||||
if len(b.Bto) != 0 {
|
||||
t.Errorf("%T.Bto should have exactly 0(zero) elements, not %d", b, len(b.Bto))
|
||||
}
|
||||
if len(b.CC) != 0 {
|
||||
t.Errorf("%T.CC should have exactly 0(zero) elements, not %d", b, len(b.CC))
|
||||
}
|
||||
if len(b.BCC) != 0 {
|
||||
t.Errorf("%T.BCC should have exactly 0(zero) elements, not %d", b, len(b.BCC))
|
||||
}
|
||||
var err error
|
||||
recIds := make([]ID, 0)
|
||||
err = checkDedup(b.To, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(b.Bto, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(b.CC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(b.BCC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_GetLink(t *testing.T) {
|
||||
i := IntransitiveActivityNew("test", QuestionType)
|
||||
|
||||
if i.GetID() != "test" {
|
||||
t.Errorf("%T should return an empty %T object. Received %#v", i, i, i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_GetObject(t *testing.T) {
|
||||
i := IntransitiveActivityNew("test", QuestionType)
|
||||
|
||||
if i.GetID() != "test" || i.GetType() != QuestionType {
|
||||
t.Errorf("%T should not return an empty %T object. Received %#v", i, i, i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_IsLink(t *testing.T) {
|
||||
i := IntransitiveActivityNew("test", QuestionType)
|
||||
|
||||
if i.IsLink() {
|
||||
t.Errorf("%T should not respond true to IsLink", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_IsObject(t *testing.T) {
|
||||
i := IntransitiveActivityNew("test", ActivityType)
|
||||
|
||||
if !i.IsObject() {
|
||||
t.Errorf("%T should respond true to IsObject", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_Recipients(t *testing.T) {
|
||||
to := PersonNew("bob")
|
||||
o := ObjectNew(ArticleType)
|
||||
cc := PersonNew("alice")
|
||||
|
||||
o.ID = "something"
|
||||
|
||||
c := IntransitiveActivityNew("act", IntransitiveActivityType)
|
||||
c.To.Append(to)
|
||||
c.CC.Append(cc)
|
||||
c.BCC.Append(cc)
|
||||
|
||||
c.Recipients()
|
||||
|
||||
var err error
|
||||
recIds := make([]ID, 0)
|
||||
err = checkDedup(c.To, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.Bto, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.CC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.BCC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_GetID(t *testing.T) {
|
||||
a := IntransitiveActivityNew("test", IntransitiveActivityType)
|
||||
|
||||
if a.GetID() != "test" {
|
||||
t.Errorf("%T should return an empty %T object. Received %#v", a, a.GetID(), a.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_GetType(t *testing.T) {
|
||||
{
|
||||
a := IntransitiveActivityNew("test", IntransitiveActivityType)
|
||||
if a.GetType() != IntransitiveActivityType {
|
||||
t.Errorf("GetType should return %q for %T, received %q", IntransitiveActivityType, a, a.GetType())
|
||||
}
|
||||
}
|
||||
{
|
||||
a := IntransitiveActivityNew("test", ArriveType)
|
||||
if a.GetType() != ArriveType {
|
||||
t.Errorf("GetType should return %q for %T, received %q", ArriveType, a, a.GetType())
|
||||
}
|
||||
}
|
||||
{
|
||||
a := IntransitiveActivityNew("test", QuestionType)
|
||||
if a.GetType() != QuestionType {
|
||||
t.Errorf("GetType should return %q for %T, received %q", QuestionType, a, a.GetType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToIntransitiveActivity(t *testing.T) {
|
||||
var it Item
|
||||
act := IntransitiveActivityNew("test", TravelType)
|
||||
it = act
|
||||
|
||||
a, err := ToIntransitiveActivity(it)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if a != act {
|
||||
t.Errorf("Invalid activity returned by ToActivity #%v", a)
|
||||
}
|
||||
|
||||
ob := ObjectNew(ArticleType)
|
||||
it = ob
|
||||
|
||||
o, err := ToIntransitiveActivity(it)
|
||||
if err == nil {
|
||||
t.Errorf("Error returned when calling ToActivity with object should not be nil")
|
||||
}
|
||||
if o != nil {
|
||||
t.Errorf("Invalid return by ToActivity #%v, should have been nil", o)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestArriveNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
a := ArriveNew(testValue)
|
||||
|
||||
if a.ID != testValue {
|
||||
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
|
||||
}
|
||||
if a.Type != ArriveType {
|
||||
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, ArriveType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTravelNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
a := TravelNew(testValue)
|
||||
|
||||
if a.ID != testValue {
|
||||
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
|
||||
}
|
||||
if a.Type != TravelType {
|
||||
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, TravelType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntransitiveActivity_Equals(t *testing.T) {
|
||||
type fields struct {
|
||||
ID ID
|
||||
Type ActivityVocabularyType
|
||||
Name NaturalLanguageValues
|
||||
Attachment Item
|
||||
AttributedTo Item
|
||||
Audience ItemCollection
|
||||
Content NaturalLanguageValues
|
||||
Context Item
|
||||
MediaType MimeType
|
||||
EndTime time.Time
|
||||
Generator Item
|
||||
Icon Item
|
||||
Image Item
|
||||
InReplyTo Item
|
||||
Location Item
|
||||
Preview Item
|
||||
Published time.Time
|
||||
Replies Item
|
||||
StartTime time.Time
|
||||
Summary NaturalLanguageValues
|
||||
Tag ItemCollection
|
||||
Updated time.Time
|
||||
URL Item
|
||||
To ItemCollection
|
||||
Bto ItemCollection
|
||||
CC ItemCollection
|
||||
BCC ItemCollection
|
||||
Duration time.Duration
|
||||
Likes Item
|
||||
Shares Item
|
||||
Source Source
|
||||
Actor Item
|
||||
Target Item
|
||||
Result Item
|
||||
Origin Item
|
||||
Instrument Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
arg Item
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal-empty-intransitive-activity",
|
||||
fields: fields{},
|
||||
arg: IntransitiveActivity{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-intransitive-activity-just-id",
|
||||
fields: fields{ID: "test"},
|
||||
arg: IntransitiveActivity{ID: "test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-intransitive-activity-id",
|
||||
fields: fields{ID: "test", URL: IRI("example.com")},
|
||||
arg: IntransitiveActivity{ID: "test"},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal-false-with-id-and-url",
|
||||
fields: fields{ID: "test"},
|
||||
arg: IntransitiveActivity{ID: "test", URL: IRI("example.com")},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not a valid intransitive-activity",
|
||||
fields: fields{ID: "http://example.com"},
|
||||
arg: Link{ID: "http://example.com"},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := IntransitiveActivity{
|
||||
ID: tt.fields.ID,
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
Attachment: tt.fields.Attachment,
|
||||
AttributedTo: tt.fields.AttributedTo,
|
||||
Audience: tt.fields.Audience,
|
||||
Content: tt.fields.Content,
|
||||
Context: tt.fields.Context,
|
||||
MediaType: tt.fields.MediaType,
|
||||
EndTime: tt.fields.EndTime,
|
||||
Generator: tt.fields.Generator,
|
||||
Icon: tt.fields.Icon,
|
||||
Image: tt.fields.Image,
|
||||
InReplyTo: tt.fields.InReplyTo,
|
||||
Location: tt.fields.Location,
|
||||
Preview: tt.fields.Preview,
|
||||
Published: tt.fields.Published,
|
||||
Replies: tt.fields.Replies,
|
||||
StartTime: tt.fields.StartTime,
|
||||
Summary: tt.fields.Summary,
|
||||
Tag: tt.fields.Tag,
|
||||
Updated: tt.fields.Updated,
|
||||
URL: tt.fields.URL,
|
||||
To: tt.fields.To,
|
||||
Bto: tt.fields.Bto,
|
||||
CC: tt.fields.CC,
|
||||
BCC: tt.fields.BCC,
|
||||
Duration: tt.fields.Duration,
|
||||
Likes: tt.fields.Likes,
|
||||
Shares: tt.fields.Shares,
|
||||
Source: tt.fields.Source,
|
||||
Actor: tt.fields.Actor,
|
||||
Target: tt.fields.Target,
|
||||
Result: tt.fields.Result,
|
||||
Origin: tt.fields.Origin,
|
||||
Instrument: tt.fields.Instrument,
|
||||
}
|
||||
if got := a.Equals(tt.arg); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
452
iri.go
Normal file
452
iri.go
Normal file
|
@ -0,0 +1,452 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
// ActivityBaseURI the URI for the ActivityStreams namespace
|
||||
ActivityBaseURI = IRI("https://www.w3.org/ns/activitystreams")
|
||||
// SecurityContextURI the URI for the security namespace (for an Actor's PublicKey)
|
||||
SecurityContextURI = IRI("https://w3id.org/security/v1")
|
||||
// PublicNS is the reference to the Public entity in the ActivityStreams namespace.
|
||||
//
|
||||
// Public Addressing
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#public-addressing
|
||||
//
|
||||
// In addition to [ActivityStreams] collections and objects, Activities may additionally be addressed to the
|
||||
// special "public" collection, with the identifier https://www.w3.org/ns/activitystreams#Public. For example:
|
||||
//
|
||||
// {
|
||||
// "@context": "https://www.w3.org/ns/activitystreams",
|
||||
// "id": "https://www.w3.org/ns/activitystreams#Public",
|
||||
// "type": "Collection"
|
||||
// }
|
||||
// Activities addressed to this special URI shall be accessible to all users, without authentication.
|
||||
// Implementations MUST NOT deliver to the "public" special collection; it is not capable of receiving
|
||||
// actual activities. However, actors MAY have a sharedInbox endpoint which is available for efficient
|
||||
// shared delivery of public posts (as well as posts to followers-only); see 7.1.3 Shared Inbox Delivery.
|
||||
//
|
||||
// NOTE
|
||||
// Compacting an ActivityStreams object using the ActivityStreams JSON-LD context might result in
|
||||
// https://www.w3.org/ns/activitystreams#Public being represented as simply Public or as:Public which are valid
|
||||
// representations of the Public collection. Implementations which treat ActivityStreams objects as simply JSON
|
||||
// rather than converting an incoming activity over to a local context using JSON-LD tooling should be aware
|
||||
// of this and should be prepared to accept all three representations.
|
||||
PublicNS = ActivityBaseURI + "#Public"
|
||||
)
|
||||
|
||||
// JsonLDContext is a slice of IRIs that form the default context for the objects in the
|
||||
// GoActivitypub vocabulary.
|
||||
// It does not represent just the default ActivityStreams public namespace, but it also
|
||||
// has the W3 Permanent Identifier Community Group's Security namespace, which appears
|
||||
// in the Actor type objects, which contain public key related data.
|
||||
var JsonLDContext = []IRI{
|
||||
ActivityBaseURI,
|
||||
SecurityContextURI,
|
||||
}
|
||||
|
||||
type (
|
||||
// IRI is a Internationalized Resource Identifiers (IRIs) RFC3987
|
||||
IRI string
|
||||
IRIs []IRI
|
||||
)
|
||||
|
||||
func (i IRI) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = io.WriteString(s, i.String())
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the String value of the IRI object
|
||||
func (i IRI) String() string {
|
||||
return string(i)
|
||||
}
|
||||
|
||||
// GetLink
|
||||
func (i IRI) GetLink() IRI {
|
||||
return i
|
||||
}
|
||||
|
||||
// URL
|
||||
func (i IRI) URL() (*url.URL, error) {
|
||||
if i == "" {
|
||||
return nil, errors.New("empty IRI")
|
||||
}
|
||||
return url.Parse(string(i))
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (i *IRI) UnmarshalJSON(s []byte) error {
|
||||
*i = IRI(strings.Trim(string(s), "\""))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (i IRI) MarshalJSON() ([]byte, error) {
|
||||
if i == "" {
|
||||
return nil, nil
|
||||
}
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '"')
|
||||
JSONWriteS(&b, i.String())
|
||||
JSONWrite(&b, '"')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (i *IRI) UnmarshalBinary(data []byte) error {
|
||||
return i.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (i IRI) MarshalBinary() ([]byte, error) {
|
||||
return i.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (i IRI) GobEncode() ([]byte, error) {
|
||||
return []byte(i), nil
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (i IRIs) GobEncode() ([]byte, error) {
|
||||
if len(i) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
bb := make([][]byte, 0)
|
||||
for _, iri := range i {
|
||||
bb = append(bb, []byte(iri))
|
||||
}
|
||||
if err := gg.Encode(bb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (i *IRI) GobDecode(data []byte) error {
|
||||
*i = IRI(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IRIs) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
err := gob.NewDecoder(bytes.NewReader(data)).Decode(i)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
bb := make([][]byte, 0)
|
||||
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&bb)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range bb {
|
||||
*i = append(*i, IRI(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddPath concatenates el elements as a path to i
|
||||
func (i IRI) AddPath(el ...string) IRI {
|
||||
iri := strings.TrimRight(i.String(), "/")
|
||||
return IRI(iri + filepath.Clean(filepath.Join("/", filepath.Join(el...))))
|
||||
}
|
||||
|
||||
// GetID
|
||||
func (i IRI) GetID() ID {
|
||||
return i
|
||||
}
|
||||
|
||||
// GetType
|
||||
func (i IRI) GetType() ActivityVocabularyType {
|
||||
return IRIType
|
||||
}
|
||||
|
||||
// IsLink
|
||||
func (i IRI) IsLink() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsObject
|
||||
func (i IRI) IsObject() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCollection returns false for IRI objects
|
||||
func (i IRI) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// FlattenToIRI checks if Item can be flatten to an IRI and returns it if so
|
||||
func FlattenToIRI(it Item) Item {
|
||||
if !IsNil(it) && it.IsObject() && len(it.GetLink()) > 0 {
|
||||
return it.GetLink()
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (i IRIs) MarshalJSON() ([]byte, error) {
|
||||
if len(i) == 0 {
|
||||
return []byte{'[', ']'}, nil
|
||||
}
|
||||
b := make([]byte, 0)
|
||||
writeCommaIfNotEmpty := func(notEmpty bool) {
|
||||
if notEmpty {
|
||||
JSONWriteS(&b, ",")
|
||||
}
|
||||
}
|
||||
JSONWrite(&b, '[')
|
||||
for k, iri := range i {
|
||||
writeCommaIfNotEmpty(k > 0)
|
||||
JSONWrite(&b, '"')
|
||||
JSONWriteS(&b, iri.String())
|
||||
JSONWrite(&b, '"')
|
||||
}
|
||||
JSONWrite(&b, ']')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (i *IRIs) UnmarshalJSON(data []byte) error {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeString:
|
||||
if iri, ok := asIRI(val); ok && len(iri) > 0 {
|
||||
*i = append(*i, iri)
|
||||
}
|
||||
case fastjson.TypeArray:
|
||||
for _, v := range val.GetArray() {
|
||||
if iri, ok := asIRI(v); ok && len(iri) > 0 {
|
||||
*i = append(*i, iri)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to ItemCollection
|
||||
func (i IRIs) GetID() ID {
|
||||
return EmptyID
|
||||
}
|
||||
|
||||
// GetLink returns the empty IRI
|
||||
func (i IRIs) GetLink() IRI {
|
||||
return EmptyIRI
|
||||
}
|
||||
|
||||
// GetType returns the ItemCollection's type
|
||||
func (i IRIs) GetType() ActivityVocabularyType {
|
||||
return CollectionOfIRIs
|
||||
}
|
||||
|
||||
// IsLink returns false for an ItemCollection object
|
||||
func (i IRIs) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a ItemCollection object
|
||||
func (i IRIs) IsObject() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCollection returns true for IRI slices
|
||||
func (i IRIs) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Append facilitates adding elements to IRI slices
|
||||
// and ensures IRIs implements the Collection interface
|
||||
func (i *IRIs) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if (*i).Contains(ob.GetLink()) {
|
||||
continue
|
||||
}
|
||||
*i = append(*i, ob.GetLink())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IRIs) Collection() ItemCollection {
|
||||
res := make(ItemCollection, len(*i))
|
||||
for k, iri := range *i {
|
||||
res[k] = iri
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (i *IRIs) Count() uint {
|
||||
return uint(len(*i))
|
||||
}
|
||||
|
||||
// Contains verifies if IRIs array contains the received one
|
||||
func (i IRIs) Contains(r Item) bool {
|
||||
if len(i) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, iri := range i {
|
||||
if r.GetLink().Equals(iri, false) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validURL(u *url.URL, checkScheme bool) bool {
|
||||
if u == nil {
|
||||
return false
|
||||
}
|
||||
if len(u.Host) == 0 {
|
||||
return false
|
||||
}
|
||||
if checkScheme {
|
||||
return len(u.Scheme) > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stripFragment(u string) string {
|
||||
p := strings.Index(u, "#")
|
||||
if p <= 0 {
|
||||
p = len(u)
|
||||
}
|
||||
return u[:p]
|
||||
}
|
||||
|
||||
func stripScheme(u string) string {
|
||||
p := strings.Index(u, "://")
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
return u[p:]
|
||||
}
|
||||
|
||||
func irisEqual(i1, i2 IRI, checkScheme bool) bool {
|
||||
u, e := i1.URL()
|
||||
uw, ew := i2.URL()
|
||||
if e != nil || ew != nil || !validURL(u, checkScheme) || !validURL(uw, checkScheme) {
|
||||
return strings.EqualFold(i1.String(), i2.String())
|
||||
}
|
||||
if checkScheme {
|
||||
if !strings.EqualFold(u.Scheme, uw.Scheme) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if !strings.EqualFold(u.Host, uw.Host) {
|
||||
return false
|
||||
}
|
||||
if !(u.Path == "/" && uw.Path == "" || u.Path == "" && uw.Path == "/") &&
|
||||
!strings.EqualFold(filepath.Clean(u.Path), filepath.Clean(uw.Path)) {
|
||||
return false
|
||||
}
|
||||
uq := u.Query()
|
||||
uwq := uw.Query()
|
||||
if len(uq) != len(uwq) {
|
||||
return false
|
||||
}
|
||||
for k, uqv := range uq {
|
||||
uwqv, ok := uwq[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if len(uqv) != len(uwqv) {
|
||||
return false
|
||||
}
|
||||
for _, uqvv := range uqv {
|
||||
eq := false
|
||||
for _, uwqvv := range uwqv {
|
||||
if uwqvv == uqvv {
|
||||
eq = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !eq {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals verifies if our receiver IRI is equals with the "with" IRI
|
||||
func (i IRI) Equals(with IRI, checkScheme bool) bool {
|
||||
is := stripFragment(string(i))
|
||||
ws := stripFragment(string(with))
|
||||
if !checkScheme {
|
||||
is = stripScheme(is)
|
||||
ws = stripScheme(ws)
|
||||
}
|
||||
if strings.EqualFold(is, ws) {
|
||||
return true
|
||||
}
|
||||
return irisEqual(i, with, checkScheme)
|
||||
}
|
||||
|
||||
func hostSplit(h string) (string, string) {
|
||||
pieces := strings.Split(h, ":")
|
||||
if len(pieces) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
if len(pieces) == 1 {
|
||||
return pieces[0], ""
|
||||
}
|
||||
return pieces[0], pieces[1]
|
||||
}
|
||||
|
||||
func (i IRI) Contains(what IRI, checkScheme bool) bool {
|
||||
u, e := i.URL()
|
||||
uw, ew := what.URL()
|
||||
if e != nil || ew != nil {
|
||||
return strings.Contains(i.String(), what.String())
|
||||
}
|
||||
if checkScheme {
|
||||
if u.Scheme != uw.Scheme {
|
||||
return false
|
||||
}
|
||||
}
|
||||
uHost, _ := hostSplit(u.Host)
|
||||
uwHost, _ := hostSplit(uw.Host)
|
||||
if uHost != uwHost {
|
||||
return false
|
||||
}
|
||||
p := u.Path
|
||||
if p != "" {
|
||||
p = filepath.Clean(p)
|
||||
}
|
||||
pw := uw.Path
|
||||
if pw != "" {
|
||||
pw = filepath.Clean(pw)
|
||||
}
|
||||
return strings.Contains(p, pw)
|
||||
}
|
||||
|
||||
func (i IRI) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := it.GetLink().Contains(i, false); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
405
iri_test.go
Normal file
405
iri_test.go
Normal file
|
@ -0,0 +1,405 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIRI_GetLink(t *testing.T) {
|
||||
val := "http://example.com"
|
||||
u := IRI(val)
|
||||
if u.GetLink() != IRI(val) {
|
||||
t.Errorf("IRI %q should equal %q", u, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_String(t *testing.T) {
|
||||
val := "http://example.com"
|
||||
u := IRI(val)
|
||||
if u.String() != val {
|
||||
t.Errorf("IRI %q should equal %q", u, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_GetID(t *testing.T) {
|
||||
i := IRI("http://example.com")
|
||||
if id := i.GetID(); !id.IsValid() || id != ID(i) {
|
||||
t.Errorf("ID %q (%T) should equal %q (%T)", id, id, i, ID(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_GetType(t *testing.T) {
|
||||
i := IRI("http://example.com")
|
||||
if i.GetType() != IRIType {
|
||||
t.Errorf("Invalid type for %T object %s, expected %s", i, i.GetType(), IRIType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_IsLink(t *testing.T) {
|
||||
i := IRI("http://example.com")
|
||||
if i.IsLink() != true {
|
||||
t.Errorf("%T.IsLink() returned %t, expected %t", i, i.IsLink(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_IsObject(t *testing.T) {
|
||||
i := IRI("http://example.com")
|
||||
if i.IsObject() {
|
||||
t.Errorf("%T.IsObject() returned %t, expected %t", i, i.IsObject(), false)
|
||||
}
|
||||
ii := IRI([]byte("https://example.com"))
|
||||
if ii.IsObject() {
|
||||
t.Errorf("%T.IsObject() returned %t, expected %t", ii, ii.IsObject(), false)
|
||||
}
|
||||
iii := &ii
|
||||
if iii.IsObject() {
|
||||
t.Errorf("%T.IsObject() returned %t, expected %t", iii, iii.IsObject(), false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_UnmarshalJSON(t *testing.T) {
|
||||
val := "http://example.com"
|
||||
i := IRI("")
|
||||
|
||||
err := i.UnmarshalJSON([]byte(val))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if val != i.String() {
|
||||
t.Errorf("%T invalid value after Unmarshal %q, expected %q", i, i, val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_MarshalJSON(t *testing.T) {
|
||||
value := []byte("http://example.com")
|
||||
i := IRI(value)
|
||||
|
||||
v, err := i.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
expected := fmt.Sprintf("%q", value)
|
||||
if expected != string(v) {
|
||||
t.Errorf("Invalid value after MarshalJSON: %s, expected %s", v, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenToIRI(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestIRI_URL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i IRI
|
||||
want *url.URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
i: "",
|
||||
want: nil,
|
||||
wantErr: true, // empty IRI
|
||||
},
|
||||
{
|
||||
name: "example with fragment",
|
||||
i: "https://example.com/#fragment",
|
||||
want: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "example.com",
|
||||
Path: "/",
|
||||
Fragment: "fragment",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.i.URL()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("URL() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("URL() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRIs_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestIRI_Contains(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestIRI_IsCollection(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestIRIs_UnmarshalJSON(t *testing.T) {
|
||||
type args struct {
|
||||
d []byte
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
obj IRIs
|
||||
want IRIs
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{[]byte{'{', '}'}},
|
||||
want: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "IRI",
|
||||
args: args{[]byte("\"http://example.com\"")},
|
||||
want: IRIs{IRI("http://example.com")},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "IRIs",
|
||||
args: args{[]byte(fmt.Sprintf("[%q, %q, %q]", "http://example.com", "http://example.net", "http://example.org"))},
|
||||
want: IRIs{
|
||||
IRI("http://example.com"),
|
||||
IRI("http://example.net"),
|
||||
IRI("http://example.org"),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.obj.UnmarshalJSON(tt.args.d)
|
||||
if (err != nil && tt.err == nil) || (err == nil && tt.err != nil) {
|
||||
if !errors.Is(err, tt.err) {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !assertDeepEquals(t.Errorf, tt.obj, tt.want) {
|
||||
t.Errorf("UnmarshalJSON() got = %#v, want %#v", tt.obj, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRIs_MarshalJSON(t *testing.T) {
|
||||
value1 := []byte("http://example.com")
|
||||
value2 := []byte("http://example.net")
|
||||
value3 := []byte("http://example.org")
|
||||
i := IRIs{
|
||||
IRI(value1),
|
||||
IRI(value2),
|
||||
IRI(value3),
|
||||
}
|
||||
|
||||
v, err := i.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
expected := fmt.Sprintf("[%q, %q, %q]", value1, value2, value3)
|
||||
if expected == string(v) {
|
||||
t.Errorf("Invalid value after MarshalJSON: %s, expected %s", v, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_AddPath(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestIRI_ItemMatches(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestIRI_GobDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i IRI
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
i: "",
|
||||
data: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some iri",
|
||||
i: "https://example.com",
|
||||
data: gobValue([]byte("https://example.com")),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.i.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_GobEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i IRI
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
i: "",
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some iri",
|
||||
i: "https://example.com",
|
||||
want: []byte("https://example.com"),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.i.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIRI_Equals(t *testing.T) {
|
||||
type args struct {
|
||||
with IRI
|
||||
check bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
i IRI
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "just host",
|
||||
i: "http://example.com",
|
||||
args: args{
|
||||
with: IRI("http://example.com"),
|
||||
check: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "host and path",
|
||||
i: "http://example.com/ana/are/mere",
|
||||
args: args{
|
||||
with: IRI("http://example.com/ana/are/mere"),
|
||||
check: true,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different schemes check scheme",
|
||||
i: "https://example.com/ana/are/mere",
|
||||
args: args{
|
||||
with: IRI("http://example.com/ana/are/mere"),
|
||||
check: true,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different schemes, don't check scheme",
|
||||
i: "https://example.com/ana/are/mere",
|
||||
args: args{
|
||||
with: IRI("http://example.com/ana/are/mere"),
|
||||
check: false,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same host different scheme, same query - different order",
|
||||
i: "https://example.com?ana=mere&foo=bar",
|
||||
args: args{
|
||||
with: "http://example.com?foo=bar&ana=mere",
|
||||
check: false,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same host, different scheme and same path, same query different order",
|
||||
i: "http://example.com/ana/are/mere?foo=bar&ana=mere",
|
||||
args: args{
|
||||
with: "https://example.com/ana/are/mere?ana=mere&foo=bar",
|
||||
check: false,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same host different scheme, same query",
|
||||
i: "https://example.com?ana=mere",
|
||||
args: args{
|
||||
with: "http://example.com?ana=mere",
|
||||
check: false,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different host same scheme",
|
||||
i: "http://example1.com",
|
||||
args: args{
|
||||
with: "http://example.com",
|
||||
check: true,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "same host, same scheme and different path",
|
||||
i: "same host, same scheme and different path",
|
||||
args: args{
|
||||
with: "http://example.com/ana/are/mere",
|
||||
check: true,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "same host same scheme, different query key",
|
||||
i: "http://example.com?ana1=mere",
|
||||
args: args{
|
||||
with: "http://example.com?ana=mere",
|
||||
check: false,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "same host same scheme, different query value",
|
||||
i: "http://example.com?ana=mere",
|
||||
args: args{
|
||||
with: "http://example.com?ana=mere1",
|
||||
check: false,
|
||||
},
|
||||
// This was true in the url.Parse implementation
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.i.Equals(tt.args.with, tt.args.check); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
212
item.go
Normal file
212
item.go
Normal file
|
@ -0,0 +1,212 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Item struct
|
||||
type Item = ObjectOrLink
|
||||
|
||||
const (
|
||||
// EmptyIRI represents a zero length IRI
|
||||
EmptyIRI IRI = ""
|
||||
// NilIRI represents by convention an IRI which is nil
|
||||
// Its use is mostly to check if a property of an ActivityPub Item is nil
|
||||
NilIRI IRI = "-"
|
||||
|
||||
// EmptyID represents a zero length ID
|
||||
EmptyID = EmptyIRI
|
||||
// NilID represents by convention an ID which is nil, see details of NilIRI
|
||||
NilID = NilIRI
|
||||
)
|
||||
|
||||
func itemsNeedSwapping(i1, i2 Item) bool {
|
||||
if IsIRI(i1) && !IsIRI(i2) {
|
||||
return true
|
||||
}
|
||||
t1 := i1.GetType()
|
||||
t2 := i2.GetType()
|
||||
if ObjectTypes.Contains(t2) {
|
||||
return !ObjectTypes.Contains(t1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ItemsEqual checks if it and with Items are equal
|
||||
func ItemsEqual(it, with Item) bool {
|
||||
if IsNil(it) || IsNil(with) {
|
||||
return IsNil(with) && IsNil(it)
|
||||
}
|
||||
if itemsNeedSwapping(it, with) {
|
||||
return ItemsEqual(with, it)
|
||||
}
|
||||
result := false
|
||||
if IsIRI(with) || IsIRI(it) {
|
||||
// NOTE(marius): I'm not sure this logic is sound:
|
||||
// if only one item is an IRI it should not be equal to the other even if it has the same ID
|
||||
result = it.GetLink().Equals(with.GetLink(), false)
|
||||
} else if IsItemCollection(it) {
|
||||
if !IsItemCollection(with) {
|
||||
return false
|
||||
}
|
||||
_ = OnItemCollection(it, func(c *ItemCollection) error {
|
||||
result = c.Equals(with)
|
||||
return nil
|
||||
})
|
||||
} else if IsObject(it) {
|
||||
_ = OnObject(it, func(i *Object) error {
|
||||
result = i.Equals(with)
|
||||
return nil
|
||||
})
|
||||
if ActivityTypes.Contains(with.GetType()) {
|
||||
_ = OnActivity(it, func(i *Activity) error {
|
||||
result = i.Equals(with)
|
||||
return nil
|
||||
})
|
||||
} else if ActorTypes.Contains(with.GetType()) {
|
||||
_ = OnActor(it, func(i *Actor) error {
|
||||
result = i.Equals(with)
|
||||
return nil
|
||||
})
|
||||
} else if it.IsCollection() {
|
||||
if it.GetType() == CollectionType {
|
||||
_ = OnCollection(it, func(c *Collection) error {
|
||||
result = c.Equals(with)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if it.GetType() == OrderedCollectionType {
|
||||
_ = OnOrderedCollection(it, func(c *OrderedCollection) error {
|
||||
result = c.Equals(with)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if it.GetType() == CollectionPageType {
|
||||
_ = OnCollectionPage(it, func(c *CollectionPage) error {
|
||||
result = c.Equals(with)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if it.GetType() == OrderedCollectionPageType {
|
||||
_ = OnOrderedCollectionPage(it, func(c *OrderedCollectionPage) error {
|
||||
result = c.Equals(with)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsItemCollection returns if the current Item interface holds a Collection
|
||||
func IsItemCollection(it LinkOrIRI) bool {
|
||||
_, ok := it.(ItemCollection)
|
||||
_, okP := it.(*ItemCollection)
|
||||
return ok || okP || IsIRIs(it)
|
||||
}
|
||||
|
||||
// IsIRI returns if the current Item interface holds an IRI
|
||||
func IsIRI(it LinkOrIRI) bool {
|
||||
_, okV := it.(IRI)
|
||||
_, okP := it.(*IRI)
|
||||
return okV || okP
|
||||
}
|
||||
|
||||
// IsIRIs returns if the current Item interface holds an IRI slice
|
||||
func IsIRIs(it LinkOrIRI) bool {
|
||||
_, okV := it.(IRIs)
|
||||
_, okP := it.(*IRIs)
|
||||
return okV || okP
|
||||
}
|
||||
|
||||
// IsLink returns if the current Item interface holds a Link
|
||||
func IsLink(it LinkOrIRI) bool {
|
||||
_, okV := it.(Link)
|
||||
_, okP := it.(*Link)
|
||||
return okV || okP
|
||||
}
|
||||
|
||||
// IsObject returns if the current Item interface holds an Object
|
||||
func IsObject(it LinkOrIRI) bool {
|
||||
switch ob := it.(type) {
|
||||
case Actor, *Actor,
|
||||
Object, *Object, Profile, *Profile, Place, *Place, Relationship, *Relationship, Tombstone, *Tombstone,
|
||||
Activity, *Activity, IntransitiveActivity, *IntransitiveActivity, Question, *Question,
|
||||
Collection, *Collection, CollectionPage, *CollectionPage,
|
||||
OrderedCollection, *OrderedCollection, OrderedCollectionPage, *OrderedCollectionPage:
|
||||
return ob != nil
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsNil checks if the object matching an ObjectOrLink interface is nil
|
||||
func IsNil(it LinkOrIRI) bool {
|
||||
if it == nil {
|
||||
return true
|
||||
}
|
||||
// This is the default if the argument can't be cast to Object, as is the case for an ItemCollection
|
||||
isNil := false
|
||||
if IsIRI(it) {
|
||||
isNil = len(it.GetLink()) == 0 || strings.EqualFold(it.GetLink().String(), NilIRI.String())
|
||||
} else if IsItemCollection(it) {
|
||||
if v, ok := it.(ItemCollection); ok {
|
||||
return v == nil
|
||||
}
|
||||
if v, ok := it.(*ItemCollection); ok {
|
||||
return v == nil
|
||||
}
|
||||
if v, ok := it.(IRIs); ok {
|
||||
return v == nil
|
||||
}
|
||||
if v, ok := it.(*IRIs); ok {
|
||||
return v == nil
|
||||
}
|
||||
} else if IsObject(it) {
|
||||
if ob, ok := it.(Item); ok {
|
||||
_ = OnObject(ob, func(o *Object) error {
|
||||
isNil = o == nil
|
||||
return nil
|
||||
})
|
||||
}
|
||||
} else if IsLink(it) {
|
||||
_ = OnLink(it, func(l *Link) error {
|
||||
isNil = l == nil
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
// NOTE(marius): we're not dealing with a type that we know about, so we use slow reflection
|
||||
// as we still care about the result
|
||||
v := reflect.ValueOf(it)
|
||||
isNil = v.Kind() == reflect.Pointer && v.IsNil()
|
||||
}
|
||||
return isNil
|
||||
}
|
||||
|
||||
func ErrorInvalidType[T Objects | Links | IRIs](received LinkOrIRI) error {
|
||||
return fmt.Errorf("unable to convert %T to %T", received, new(T))
|
||||
}
|
||||
|
||||
// OnItem runs function "fn" on the Item "it", with the benefit of destructuring "it" to individual
|
||||
// items if it's actually an ItemCollection or an object holding an ItemCollection
|
||||
//
|
||||
// It is expected that the caller handles the logic of dealing with different Item implementations
|
||||
// internally in "fn".
|
||||
func OnItem(it Item, fn func(Item) error) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if !IsItemCollection(it) {
|
||||
return fn(it)
|
||||
}
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := OnItem(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
301
item_collection.go
Normal file
301
item_collection.go
Normal file
|
@ -0,0 +1,301 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"sort"
|
||||
)
|
||||
|
||||
// ItemCollection represents an array of items
|
||||
type ItemCollection []Item
|
||||
|
||||
// GetID returns the ID corresponding to ItemCollection
|
||||
func (i ItemCollection) GetID() ID {
|
||||
return EmptyID
|
||||
}
|
||||
|
||||
// GetLink returns the empty IRI
|
||||
func (i ItemCollection) GetLink() IRI {
|
||||
return EmptyIRI
|
||||
}
|
||||
|
||||
// GetType returns the ItemCollection's type
|
||||
func (i ItemCollection) GetType() ActivityVocabularyType {
|
||||
return CollectionOfItems
|
||||
}
|
||||
|
||||
// IsLink returns false for an ItemCollection object
|
||||
func (i ItemCollection) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a ItemCollection object
|
||||
func (i ItemCollection) IsObject() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (i ItemCollection) IRIs() IRIs {
|
||||
if i == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
iris := make(IRIs, 0, len(i))
|
||||
for _, it := range i {
|
||||
iris = append(iris, it.GetLink())
|
||||
}
|
||||
return iris
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (i ItemCollection) MarshalJSON() ([]byte, error) {
|
||||
if i == nil {
|
||||
return nil, nil
|
||||
}
|
||||
b := make([]byte, 0)
|
||||
JSONWriteItemCollectionValue(&b, i, true)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Append facilitates adding elements to Item arrays
|
||||
// and ensures ItemCollection implements the Collection interface
|
||||
func (i *ItemCollection) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if i.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
*i = append(*i, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the length of Items in the item collection
|
||||
func (i *ItemCollection) Count() uint {
|
||||
if i == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(*i))
|
||||
}
|
||||
|
||||
// First returns the ID corresponding to ItemCollection
|
||||
func (i ItemCollection) First() Item {
|
||||
if len(i) == 0 {
|
||||
return nil
|
||||
}
|
||||
return i[0]
|
||||
}
|
||||
|
||||
// Normalize returns the first item if the collection contains only one,
|
||||
// the full collection if the collection contains more than one item,
|
||||
// or nil
|
||||
func (i ItemCollection) Normalize() Item {
|
||||
if len(i) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(i) == 1 {
|
||||
return i[0]
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Collection returns the current object as collection interface
|
||||
func (i *ItemCollection) Collection() ItemCollection {
|
||||
return *i
|
||||
}
|
||||
|
||||
// IsCollection returns true for ItemCollection arrays
|
||||
func (i ItemCollection) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains verifies if IRIs array contains the received one
|
||||
func (i ItemCollection) Contains(r Item) bool {
|
||||
if len(i) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range i {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Remove removes the r Item from the i ItemCollection if it contains it
|
||||
func (i *ItemCollection) Remove(r Item) {
|
||||
li := len(*i)
|
||||
if li == 0 {
|
||||
return
|
||||
}
|
||||
if r == nil {
|
||||
return
|
||||
}
|
||||
remIdx := -1
|
||||
for idx, it := range *i {
|
||||
if ItemsEqual(it, r) {
|
||||
remIdx = idx
|
||||
}
|
||||
}
|
||||
if remIdx == -1 {
|
||||
return
|
||||
}
|
||||
if remIdx < li-1 {
|
||||
*i = append((*i)[:remIdx], (*i)[remIdx+1:]...)
|
||||
} else {
|
||||
*i = (*i)[:remIdx]
|
||||
}
|
||||
}
|
||||
|
||||
// ItemCollectionDeduplication normalizes the received arguments lists into a single unified one
|
||||
func ItemCollectionDeduplication(recCols ...*ItemCollection) ItemCollection {
|
||||
rec := make(ItemCollection, 0)
|
||||
|
||||
for _, recCol := range recCols {
|
||||
if recCol == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
toRemove := make([]int, 0)
|
||||
for i, cur := range *recCol {
|
||||
save := true
|
||||
if cur == nil {
|
||||
continue
|
||||
}
|
||||
var testIt IRI
|
||||
if cur.IsObject() {
|
||||
testIt = cur.GetID()
|
||||
} else if cur.IsLink() {
|
||||
testIt = cur.GetLink()
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
for _, it := range rec {
|
||||
if testIt.Equals(it.GetID(), false) {
|
||||
// mark the element for removal
|
||||
toRemove = append(toRemove, i)
|
||||
save = false
|
||||
}
|
||||
}
|
||||
if save {
|
||||
rec = append(rec, testIt)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(sort.IntSlice(toRemove)))
|
||||
for _, idx := range toRemove {
|
||||
*recCol = append((*recCol)[:idx], (*recCol)[idx+1:]...)
|
||||
}
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
// ToItemCollection returns the item collection contained as part of OrderedCollection, OrderedCollectionPage,
|
||||
// Collection and CollectionPage.
|
||||
// It also converts an IRI slice into an equivalent ItemCollection.
|
||||
func ToItemCollection(it Item) (*ItemCollection, error) {
|
||||
switch i := it.(type) {
|
||||
case *ItemCollection:
|
||||
return i, nil
|
||||
case ItemCollection:
|
||||
return &i, nil
|
||||
case *OrderedCollection:
|
||||
return &i.OrderedItems, nil
|
||||
case *OrderedCollectionPage:
|
||||
return &i.OrderedItems, nil
|
||||
case *Collection:
|
||||
return &i.Items, nil
|
||||
case *CollectionPage:
|
||||
return &i.Items, nil
|
||||
case IRIs:
|
||||
iris := make(ItemCollection, len(i))
|
||||
for j, ob := range i {
|
||||
iris[j] = ob
|
||||
}
|
||||
return &iris, nil
|
||||
case *IRIs:
|
||||
iris := make(ItemCollection, len(*i))
|
||||
for j, ob := range *i {
|
||||
iris[j] = ob
|
||||
}
|
||||
return &iris, nil
|
||||
default:
|
||||
return reflectItemToType[ItemCollection](it)
|
||||
}
|
||||
return nil, ErrorInvalidType[ItemCollection](it)
|
||||
}
|
||||
|
||||
// ToIRIs
|
||||
func ToIRIs(it Item) (*IRIs, error) {
|
||||
switch i := it.(type) {
|
||||
case *IRIs:
|
||||
return i, nil
|
||||
case IRIs:
|
||||
return &i, nil
|
||||
case ItemCollection:
|
||||
iris := i.IRIs()
|
||||
return &iris, nil
|
||||
case *ItemCollection:
|
||||
iris := make(IRIs, len(*i))
|
||||
for j, ob := range *i {
|
||||
iris[j] = ob.GetLink()
|
||||
}
|
||||
return &iris, nil
|
||||
default:
|
||||
return reflectItemToType[IRIs](it)
|
||||
}
|
||||
return nil, ErrorInvalidType[IRIs](it)
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (i ItemCollection) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := i.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (i ItemCollection) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return IsNil(i) || len(i) == 0
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
if with.GetType() != CollectionOfItems {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
_ = OnItemCollection(with, func(w *ItemCollection) error {
|
||||
if w.Count() != i.Count() {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
for _, it := range i {
|
||||
if !w.Contains(it.GetLink()) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties on all the members of the collection
|
||||
func (i ItemCollection) Clean() {
|
||||
for j, it := range i {
|
||||
i[j] = CleanRecipients(it)
|
||||
}
|
||||
}
|
||||
|
||||
func (i ItemCollection) Recipients() ItemCollection {
|
||||
all := make(ItemCollection, 0)
|
||||
for _, it := range i {
|
||||
_ = OnObject(it, func(ob *Object) error {
|
||||
aud := ob.Audience
|
||||
_ = all.Append(ItemCollectionDeduplication(&ob.To, &ob.CC, &ob.Bto, &ob.BCC, &aud)...)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return ItemCollectionDeduplication(&all)
|
||||
}
|
415
item_collection_test.go
Normal file
415
item_collection_test.go
Normal file
|
@ -0,0 +1,415 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestItemCollection_Append(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_Collection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_First(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_Count(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToItemCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemCollection_Remove(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i ItemCollection
|
||||
arg Item
|
||||
}{
|
||||
{
|
||||
name: "empty_collection_nil_item",
|
||||
i: ItemCollection{},
|
||||
arg: nil,
|
||||
},
|
||||
{
|
||||
name: "empty_collection_non_nil_item",
|
||||
i: ItemCollection{},
|
||||
arg: &Object{},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_nil_item",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test"},
|
||||
},
|
||||
arg: nil,
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_non_contained_item_empty_ID",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test"},
|
||||
},
|
||||
arg: &Object{},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_non_contained_item",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test"},
|
||||
},
|
||||
arg: &Object{ID: "test123"},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_just_contained_item",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test"},
|
||||
},
|
||||
arg: &Object{ID: "test"},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_contained_item_first_pos",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test"},
|
||||
&Object{ID: "test123"},
|
||||
},
|
||||
arg: &Object{ID: "test"},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_contained_item_not_first_pos",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test123"},
|
||||
&Object{ID: "test"},
|
||||
&Object{ID: "test321"},
|
||||
},
|
||||
arg: &Object{ID: "test"},
|
||||
},
|
||||
{
|
||||
name: "non_empty_collection_contained_item_last_pos",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "test123"},
|
||||
&Object{ID: "test"},
|
||||
},
|
||||
arg: &Object{ID: "test"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
origContains := tt.i.Contains(tt.arg)
|
||||
origLen := tt.i.Count()
|
||||
should := ""
|
||||
does := "n't"
|
||||
if origContains {
|
||||
should = "n't"
|
||||
does = ""
|
||||
}
|
||||
|
||||
tt.i.Remove(tt.arg)
|
||||
if tt.i.Contains(tt.arg) {
|
||||
t.Errorf("%T should%s contain %T, but it does%s: %#v", tt.i, should, tt.arg, does, tt.i)
|
||||
}
|
||||
if origContains {
|
||||
if tt.i.Count() > origLen-1 {
|
||||
t.Errorf("%T should have a count lower than %d, got %d", tt.i, origLen, tt.i.Count())
|
||||
}
|
||||
} else {
|
||||
if tt.i.Count() != origLen {
|
||||
t.Errorf("%T should have a count equal to %d, got %d", tt.i, origLen, tt.i.Count())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemCollectionDeduplication(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []*ItemCollection
|
||||
want ItemCollection
|
||||
remaining []*ItemCollection
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "no-overlap",
|
||||
args: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.com/1"),
|
||||
},
|
||||
},
|
||||
want: ItemCollection{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
IRI("https://example.com/1"),
|
||||
},
|
||||
remaining: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.com/1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "some-overlap",
|
||||
args: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.com/1"),
|
||||
IRI("https://example.com/2"),
|
||||
},
|
||||
},
|
||||
want: ItemCollection{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
IRI("https://example.com/1"),
|
||||
},
|
||||
remaining: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.com"),
|
||||
IRI("https://example.com/2"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.com/1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test from spammy",
|
||||
args: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev"),
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
},
|
||||
want: ItemCollection{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
IRI("https://example.dev"),
|
||||
},
|
||||
remaining: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "different order for spammy test",
|
||||
args: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev"),
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
},
|
||||
},
|
||||
want: ItemCollection{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
IRI("https://example.dev"),
|
||||
},
|
||||
remaining: []*ItemCollection{
|
||||
{
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"),
|
||||
IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
},
|
||||
{
|
||||
IRI("https://example.dev"),
|
||||
IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"),
|
||||
},
|
||||
{},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ItemCollectionDeduplication(tt.args...); !tt.want.Equals(got) {
|
||||
t.Errorf("ItemCollectionDeduplication() = %v, want %v", got, tt.want)
|
||||
}
|
||||
if len(tt.remaining) != len(tt.args) {
|
||||
t.Errorf("ItemCollectionDeduplication() arguments count %d, want %d", len(tt.args), len(tt.remaining))
|
||||
}
|
||||
for i, remArg := range tt.remaining {
|
||||
arg := tt.args[i]
|
||||
if !remArg.Equals(arg) {
|
||||
t.Errorf("ItemCollectionDeduplication() argument at pos %d = %v, want %v", i, arg, remArg)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToItemCollection1(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
it Item
|
||||
want *ItemCollection
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "IRIs to ItemCollection",
|
||||
it: IRIs{"https://example.com", "https://example.com/example"},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "ItemCollection to ItemCollection",
|
||||
it: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "*ItemCollection to ItemCollection",
|
||||
it: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Collection to ItemCollection",
|
||||
it: &Collection{Items: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "CollectionPage to ItemCollection",
|
||||
it: &CollectionPage{Items: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "OrderedCollection to ItemCollection",
|
||||
it: &OrderedCollection{OrderedItems: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "OrderedCollectionPage to ItemOrderedCollection",
|
||||
it: &OrderedCollectionPage{OrderedItems: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}},
|
||||
want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ToItemCollection(tt.it)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ToItemCollection() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToItemCollection() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemCollection_IRIs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i ItemCollection
|
||||
want IRIs
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
i: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "one item",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
},
|
||||
want: IRIs{"https://example.com"},
|
||||
},
|
||||
{
|
||||
name: "two items",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
want: IRIs{"https://example.com", "https://example.com/~jdoe"},
|
||||
},
|
||||
{
|
||||
name: "mixed items",
|
||||
i: ItemCollection{
|
||||
&Object{ID: "https://example.com"},
|
||||
IRI("https://example.com/666"),
|
||||
&Actor{ID: "https://example.com/~jdoe"},
|
||||
},
|
||||
want: IRIs{"https://example.com", "https://example.com/666", "https://example.com/~jdoe"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.i.IRIs(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("IRIs() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
392
item_test.go
Normal file
392
item_test.go
Normal file
|
@ -0,0 +1,392 @@
|
|||
package activitypub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestItemsEqual(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
with Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil_items_equal",
|
||||
args: args{nil, nil},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nil_item_with_object",
|
||||
args: args{nil, &Object{}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "nil_item_with_object#1",
|
||||
args: args{&Object{}, nil},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty_objects",
|
||||
args: args{&Object{}, &Object{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "empty_objects_different_alias_type",
|
||||
args: args{&Activity{}, &Object{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "empty_objects_different_alias_type#1",
|
||||
args: args{&Actor{}, &Object{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same_id_object",
|
||||
args: args{&Object{ID: "test"}, &Object{ID: "test"}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same_id_object_different_alias",
|
||||
args: args{&Activity{ID: "test"}, &Object{ID: "test"}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "same_id_object_different_alias#1",
|
||||
args: args{&Activity{ID: "test"}, &Actor{ID: "test"}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different_id_objects",
|
||||
args: args{&Object{ID: "test1"}, &Object{ID: "test"}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different_id_types",
|
||||
args: args{&Object{ID: "test", Type: NoteType}, &Object{ID: "test", Type: ArticleType}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want {
|
||||
t.Errorf("ItemsEqual() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
}
|
||||
var (
|
||||
o *Object
|
||||
col *ItemCollection
|
||||
iris *IRIs
|
||||
obNil Item = o
|
||||
colNil Item = col
|
||||
itIRIs Item = iris
|
||||
)
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil is nil",
|
||||
args: args{
|
||||
it: nil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Item is nil",
|
||||
args: args{
|
||||
it: Item(nil),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Object nil",
|
||||
args: args{
|
||||
it: obNil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "IRIs nil",
|
||||
args: args{
|
||||
it: iris,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "IRIs as Item nil",
|
||||
args: args{
|
||||
it: itIRIs,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "IRIs not nil",
|
||||
args: args{
|
||||
it: IRIs{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "IRIs as Item not nil",
|
||||
args: args{
|
||||
it: Item(IRIs{}),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "ItemCollection nil",
|
||||
args: args{
|
||||
it: col,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ItemCollection as Item nil",
|
||||
args: args{
|
||||
it: colNil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "ItemCollection not nil",
|
||||
args: args{
|
||||
it: ItemCollection{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "object-not-nil",
|
||||
args: args{
|
||||
it: &Object{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "place-not-nil",
|
||||
args: args{
|
||||
it: &Place{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone-not-nil",
|
||||
args: args{
|
||||
it: &Tombstone{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "collection-not-nil",
|
||||
args: args{
|
||||
it: &Collection{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "activity-not-nil",
|
||||
args: args{
|
||||
it: &Activity{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "intransitive-activity-not-nil",
|
||||
args: args{
|
||||
it: &IntransitiveActivity{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "actor-not-nil",
|
||||
args: args{
|
||||
it: &Actor{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsNil(tt.args.it); got != tt.want {
|
||||
t.Errorf("IsNil() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsEqual1(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
with Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal empty items",
|
||||
args: args{
|
||||
it: &Object{},
|
||||
with: &Actor{},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal same ID items",
|
||||
args: args{
|
||||
it: &Object{ID: "example-1"},
|
||||
with: &Object{ID: "example-1"},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "different IDs",
|
||||
args: args{
|
||||
it: &Object{ID: "example-1"},
|
||||
with: &Object{ID: "example-2"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "different properties",
|
||||
args: args{
|
||||
it: &Object{ID: "example-1"},
|
||||
with: &Object{Type: ArticleType},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want {
|
||||
t.Errorf("ItemsEqual() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsObject(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
args: args{},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "interface with nil value",
|
||||
args: args{Item(nil)},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty object",
|
||||
args: args{Object{}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "pointer to empty object",
|
||||
args: args{&Object{}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsObject(tt.args.it); got != tt.want {
|
||||
t.Errorf("IsObject() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemsEqual2(t *testing.T) {
|
||||
type args struct {
|
||||
it Item
|
||||
with Item
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "nil vs nil",
|
||||
args: args{
|
||||
it: nil,
|
||||
with: nil,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "nil vs object",
|
||||
args: args{
|
||||
it: nil,
|
||||
with: Object{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "object vs nil",
|
||||
args: args{
|
||||
it: Object{},
|
||||
with: nil,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty object vs empty object",
|
||||
args: args{
|
||||
it: Object{},
|
||||
with: Object{},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "object-id vs empty object",
|
||||
args: args{
|
||||
it: Object{ID: "https://example.com"},
|
||||
with: Object{},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "empty object vs object-id",
|
||||
args: args{
|
||||
it: Object{},
|
||||
with: Object{ID: "https://example.com"},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want {
|
||||
t.Errorf("ItemsEqual() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
166
link.go
Normal file
166
link.go
Normal file
|
@ -0,0 +1,166 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// LinkTypes represent the valid values for a Link object
|
||||
var LinkTypes = ActivityVocabularyTypes{
|
||||
LinkType,
|
||||
MentionType,
|
||||
}
|
||||
|
||||
type Links interface {
|
||||
Link | IRI
|
||||
}
|
||||
|
||||
// A Link is an indirect, qualified reference to a resource identified by a URL.
|
||||
// The fundamental model for links is established by [ RFC5988].
|
||||
// Many of the properties defined by the Activity Vocabulary allow values that are either instances of APObject or Link.
|
||||
// When a Link is used, it establishes a qualified relation connecting the subject
|
||||
// (the containing object) to the resource identified by the href.
|
||||
// Properties of the Link are properties of the reference as opposed to properties of the resource.
|
||||
type Link struct {
|
||||
// Provides the globally unique identifier for an APObject or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Identifies the APObject or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// A simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// A link relation associated with a Link. The value must conform to both the [HTML5] and
|
||||
// [RFC5988](https://tools.ietf.org/html/rfc5988) "link relation" definitions.
|
||||
// In the [HTML5], any string not containing the "space" U+0020, "tab" (U+0009), "LF" (U+000A),
|
||||
// "FF" (U+000C), "CR" (U+000D) or "," (U+002C) characters can be used as a valid link relation.
|
||||
Rel IRI `jsonld:"rel,omitempty"`
|
||||
// When used on a Link, identifies the MIME media type of the referenced resource.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// On a Link, specifies a hint as to the rendering height in device-independent pixels of the linked resource.
|
||||
Height uint `jsonld:"height,omitempty"`
|
||||
// On a Link, specifies a hint as to the rendering width in device-independent pixels of the linked resource.
|
||||
Width uint `jsonld:"width,omitempty"`
|
||||
// Identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// The target resource pointed to by a Link.
|
||||
Href IRI `jsonld:"href,omitempty"`
|
||||
// Hints as to the language used by the target resource.
|
||||
// Value must be a [BCP47](https://tools.ietf.org/html/bcp47) Language-Tag.
|
||||
HrefLang LangRef `jsonld:"hrefLang,omitempty"`
|
||||
}
|
||||
|
||||
// Mention is a specialized Link that represents an @mention.
|
||||
type Mention = Link
|
||||
|
||||
// LinkNew initializes a new Link
|
||||
func LinkNew(id ID, typ ActivityVocabularyType) *Link {
|
||||
if !LinkTypes.Contains(typ) {
|
||||
typ = LinkType
|
||||
}
|
||||
return &Link{ID: id, Type: typ}
|
||||
}
|
||||
|
||||
// MentionNew initializes a new Mention
|
||||
func MentionNew(id ID) *Mention {
|
||||
return &Mention{ID: id, Type: MentionType}
|
||||
}
|
||||
|
||||
// IsLink validates if current Link is a Link
|
||||
func (l Link) IsLink() bool {
|
||||
return l.Type == LinkType || LinkTypes.Contains(l.Type)
|
||||
}
|
||||
|
||||
// IsObject validates if current Link is an GetID
|
||||
func (l Link) IsObject() bool {
|
||||
return l.Type == ObjectType || ObjectTypes.Contains(l.Type)
|
||||
}
|
||||
|
||||
// IsCollection returns false for Link objects
|
||||
func (l Link) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the Link object
|
||||
func (l Link) GetID() ID {
|
||||
return l.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Link
|
||||
func (l Link) GetLink() IRI {
|
||||
return IRI(l.ID)
|
||||
}
|
||||
|
||||
// GetType returns the Type corresponding to the Mention object
|
||||
func (l Link) GetType() ActivityVocabularyType {
|
||||
return l.Type
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (l Link) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if JSONWriteLinkValue(&b, l) {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (l *Link) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadLink(val, l)
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (l *Link) UnmarshalBinary(data []byte) error {
|
||||
return l.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (l Link) MarshalBinary() ([]byte, error) {
|
||||
return l.GobEncode()
|
||||
}
|
||||
|
||||
func (l Link) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapLinkProperties(mm, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
func (l *Link) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapLinkProperties(mm, l)
|
||||
}
|
||||
|
||||
func (l Link) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", l, l.Type)
|
||||
}
|
||||
}
|
165
link_test.go
Normal file
165
link_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinkNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
var testType ActivityVocabularyType
|
||||
|
||||
l := LinkNew(testValue, testType)
|
||||
|
||||
if l.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", l.ID, testValue)
|
||||
}
|
||||
if l.Type != LinkType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", l.Type, LinkType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLink_IsLink(t *testing.T) {
|
||||
l := LinkNew("test", LinkType)
|
||||
if !l.IsLink() {
|
||||
t.Errorf("%#v should be a valid link", l.Type)
|
||||
}
|
||||
m := LinkNew("test", MentionType)
|
||||
if !m.IsLink() {
|
||||
t.Errorf("%#v should be a valid link", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLink_IsObject(t *testing.T) {
|
||||
l := LinkNew("test", LinkType)
|
||||
if l.IsObject() {
|
||||
t.Errorf("%#v should not be a valid object", l.Type)
|
||||
}
|
||||
m := LinkNew("test", MentionType)
|
||||
if m.IsObject() {
|
||||
t.Errorf("%#v should not be a valid object", m.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLink_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestLink_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestLink_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestLink_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestMentionNew(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestLink_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestLink_GobEncode(t *testing.T) {
|
||||
type fields struct {
|
||||
ID ID
|
||||
Type ActivityVocabularyType
|
||||
Name NaturalLanguageValues
|
||||
Rel IRI
|
||||
MediaType MimeType
|
||||
Height uint
|
||||
Width uint
|
||||
Preview Item
|
||||
Href IRI
|
||||
HrefLang LangRef
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := Link{
|
||||
ID: tt.fields.ID,
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
Rel: tt.fields.Rel,
|
||||
MediaType: tt.fields.MediaType,
|
||||
Height: tt.fields.Height,
|
||||
Width: tt.fields.Width,
|
||||
Preview: tt.fields.Preview,
|
||||
Href: tt.fields.Href,
|
||||
HrefLang: tt.fields.HrefLang,
|
||||
}
|
||||
got, err := l.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLink_GobDecode(t *testing.T) {
|
||||
type fields struct {
|
||||
ID ID
|
||||
Type ActivityVocabularyType
|
||||
Name NaturalLanguageValues
|
||||
Rel IRI
|
||||
MediaType MimeType
|
||||
Height uint
|
||||
Width uint
|
||||
Preview Item
|
||||
Href IRI
|
||||
HrefLang LangRef
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
data: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &Link{
|
||||
ID: tt.fields.ID,
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
Rel: tt.fields.Rel,
|
||||
MediaType: tt.fields.MediaType,
|
||||
Height: tt.fields.Height,
|
||||
Width: tt.fields.Width,
|
||||
Preview: tt.fields.Preview,
|
||||
Href: tt.fields.Href,
|
||||
HrefLang: tt.fields.HrefLang,
|
||||
}
|
||||
if err := l.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
812
natural_language_values.go
Normal file
812
natural_language_values.go
Normal file
|
@ -0,0 +1,812 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// NilLangRef represents a convention for a nil language reference.
|
||||
// It is used for LangRefValue objects without an explicit language key.
|
||||
const NilLangRef LangRef = "-"
|
||||
|
||||
// DefaultLang represents the default language reference used when using the convenience content generation.
|
||||
var DefaultLang = NilLangRef
|
||||
|
||||
type (
|
||||
// LangRef is the type for a language reference code, should be an ISO639-1 language specifier.
|
||||
LangRef string
|
||||
Content []byte
|
||||
|
||||
// LangRefValue is a type for storing per language values
|
||||
LangRefValue struct {
|
||||
Ref LangRef
|
||||
Value Content
|
||||
}
|
||||
// NaturalLanguageValues is a mapping for multiple language values
|
||||
NaturalLanguageValues []LangRefValue
|
||||
)
|
||||
|
||||
func NaturalLanguageValuesNew(values ...LangRefValue) NaturalLanguageValues {
|
||||
n := make(NaturalLanguageValues, len(values))
|
||||
for i, val := range values {
|
||||
n[i] = val
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func DefaultNaturalLanguageValue(content string) NaturalLanguageValues {
|
||||
return NaturalLanguageValuesNew(DefaultLangRef(content))
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) String() string {
|
||||
cnt := len(n)
|
||||
if cnt == 1 {
|
||||
return n[0].String()
|
||||
}
|
||||
s := strings.Builder{}
|
||||
s.Write([]byte{'['})
|
||||
for k, v := range n {
|
||||
s.WriteString(v.String())
|
||||
if k != cnt-1 {
|
||||
s.Write([]byte{','})
|
||||
}
|
||||
}
|
||||
s.Write([]byte{']'})
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) Get(ref LangRef) Content {
|
||||
for _, val := range n {
|
||||
if val.Ref == ref {
|
||||
return val.Value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set sets a language, value pair in a NaturalLanguageValues array
|
||||
func (n *NaturalLanguageValues) Set(ref LangRef, v Content) error {
|
||||
found := false
|
||||
for k, vv := range *n {
|
||||
if vv.Ref == ref {
|
||||
(*n)[k] = LangRefValue{ref, v}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
n.Append(ref, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NaturalLanguageValues) Add(ref LangRefValue) {
|
||||
*n = append(*n, ref)
|
||||
}
|
||||
|
||||
var hex = "0123456789abcdef"
|
||||
|
||||
// safeSet holds the value true if the ASCII character with the given array
|
||||
// position can be represented inside a JSON string without any further
|
||||
// escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), and the backslash character ("\").
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
// htmlSafeSet holds the value true if the ASCII character with the given
|
||||
// array position can be safely represented inside a JSON string, embedded
|
||||
// inside of HTML <script> tags, without any additional escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), the backslash character ("\"), HTML opening and closing
|
||||
// tags ("<" and ">"), and the ampersand ("&").
|
||||
var htmlSafeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': false,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': false,
|
||||
'=': true,
|
||||
'>': false,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with string above.
|
||||
func stringBytes(e *bytes.Buffer, s []byte, escapeHTML bool) {
|
||||
e.WriteRune('"')
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteRune('\\')
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
e.WriteRune(rune(b))
|
||||
case '\n':
|
||||
e.WriteRune('n')
|
||||
case '\r':
|
||||
e.WriteRune('r')
|
||||
case '\t':
|
||||
e.WriteRune('t')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \t, \n and \r.
|
||||
// If escapeHTML is set, it also escapes <, >, and &
|
||||
// because they can lead to security holes when
|
||||
// user-controlled strings are rendered into JSON
|
||||
// and served to some browsers.
|
||||
e.WriteString(`u00`)
|
||||
e.WriteByte(hex[b>>4])
|
||||
e.WriteByte(hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRune(s[i:])
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteString(`\ufffd`)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// U+2028 is LINE SEPARATOR.
|
||||
// U+2029 is PARAGRAPH SEPARATOR.
|
||||
// They are both technically valid characters in JSON strings,
|
||||
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||
// and can lead to security holes there. It is valid JSON to
|
||||
// escape them, so we do so unconditionally.
|
||||
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteString(`\u202`)
|
||||
e.WriteByte(hex[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
e.Write(s[start:])
|
||||
}
|
||||
e.WriteByte('"')
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) {
|
||||
l := len(n)
|
||||
if l <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b := bytes.Buffer{}
|
||||
if l == 1 {
|
||||
v := n[0]
|
||||
if len(v.Value) > 0 {
|
||||
v.Value = unescape(v.Value)
|
||||
stringBytes(&b, v.Value, false)
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
}
|
||||
b.Write([]byte{'{'})
|
||||
empty := true
|
||||
for _, val := range n {
|
||||
if len(val.Ref) == 0 || len(val.Value) == 0 {
|
||||
continue
|
||||
}
|
||||
if !empty {
|
||||
b.Write([]byte{','})
|
||||
}
|
||||
if v, err := val.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
l, err := b.Write(v)
|
||||
if err == nil && l > 0 {
|
||||
empty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Write([]byte{'}'})
|
||||
if !empty {
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// First returns the first element in the array
|
||||
func (n NaturalLanguageValues) First() LangRefValue {
|
||||
for _, v := range n {
|
||||
return v
|
||||
}
|
||||
return LangRefValue{}
|
||||
}
|
||||
|
||||
// MarshalText serializes the NaturalLanguageValues into Text
|
||||
func (n NaturalLanguageValues) MarshalText() ([]byte, error) {
|
||||
for _, v := range n {
|
||||
return []byte(fmt.Sprintf("%q", v)), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
_, _ = io.WriteString(s, "[")
|
||||
for _, nn := range n {
|
||||
nn.Format(s, verb)
|
||||
}
|
||||
_, _ = io.WriteString(s, "]")
|
||||
case 'v':
|
||||
_, _ = io.WriteString(s, "[")
|
||||
for _, nn := range n {
|
||||
nn.Format(s, verb)
|
||||
}
|
||||
_, _ = io.WriteString(s, "]")
|
||||
}
|
||||
}
|
||||
|
||||
// Append is syntactic sugar for resizing the NaturalLanguageValues map
|
||||
// and appending an element
|
||||
func (n *NaturalLanguageValues) Append(lang LangRef, value Content) error {
|
||||
*n = append(*n, LangRefValue{lang, value})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the length of Items in the item collection
|
||||
func (n *NaturalLanguageValues) Count() uint {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(*n))
|
||||
}
|
||||
|
||||
// String adds support for Stringer interface. It returns the Value[LangRef] text or just Value if LangRef is NIL
|
||||
func (l LangRefValue) String() string {
|
||||
if l.Ref == NilLangRef {
|
||||
return l.Value.String()
|
||||
}
|
||||
return fmt.Sprintf("%s[%s]", l.Value, l.Ref)
|
||||
}
|
||||
|
||||
func DefaultLangRef(value string) LangRefValue {
|
||||
return LangRefValue{Ref: DefaultLang, Value: Content(value)}
|
||||
}
|
||||
|
||||
func LangRefValueNew(lang LangRef, value string) LangRefValue {
|
||||
return LangRefValue{Ref: lang, Value: Content(value)}
|
||||
}
|
||||
|
||||
func (l LangRefValue) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
if l.Ref == NilLangRef {
|
||||
_, _ = io.WriteString(s, string(l.Value))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%q[%s]", l.Value, l.Ref)
|
||||
}
|
||||
case 'v':
|
||||
if l.Ref == NilLangRef {
|
||||
_, _ = fmt.Fprintf(s, "%q", string(l.Value))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%q[%s]", string(l.Value), l.Ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (l *LangRefValue) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(data)
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeObject:
|
||||
o, _ := val.Object()
|
||||
o.Visit(func(key []byte, v *fastjson.Value) {
|
||||
l.Ref = LangRef(key)
|
||||
l.Value = unescape(v.GetStringBytes())
|
||||
})
|
||||
case fastjson.TypeString:
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(val.GetStringBytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the TextEncoder interface
|
||||
func (l *LangRefValue) UnmarshalText(data []byte) error {
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LangRef) GobEncode() ([]byte, error) {
|
||||
if len(l) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
if err := gobEncodeStringLikeType(gg, []byte(l)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (l *LangRef) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
var bb []byte
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&bb); err != nil {
|
||||
return err
|
||||
}
|
||||
*l = LangRef(bb)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (l LangRefValue) MarshalJSON() ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
if l.Ref != NilLangRef && len(l.Ref) > 0 {
|
||||
if l.Value.Equals(Content("")) {
|
||||
return nil, nil
|
||||
}
|
||||
stringBytes(&buf, []byte(l.Ref), false)
|
||||
buf.Write([]byte{':'})
|
||||
}
|
||||
stringBytes(&buf, l.Value, false)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalText serializes the LangRefValue into JSON
|
||||
func (l LangRefValue) MarshalText() ([]byte, error) {
|
||||
if l.Ref != NilLangRef && l.Value.Equals(Content("")) {
|
||||
return nil, nil
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(l.Value.String())
|
||||
if l.Ref != NilLangRef {
|
||||
buf.WriteByte('[')
|
||||
buf.WriteString(l.Ref.String())
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
K []byte
|
||||
V []byte
|
||||
}
|
||||
|
||||
func (l LangRefValue) GobEncode() ([]byte, error) {
|
||||
if len(l.Value) == 0 && len(l.Ref) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
mm := kv{
|
||||
K: []byte(l.Ref),
|
||||
V: []byte(l.Value),
|
||||
}
|
||||
if err := gg.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (l *LangRefValue) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
mm := kv{}
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&mm); err != nil {
|
||||
return err
|
||||
}
|
||||
l.Ref = LangRef(mm.K)
|
||||
l.Value = mm.V
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (l *LangRef) UnmarshalJSON(data []byte) error {
|
||||
return l.UnmarshalText(data)
|
||||
}
|
||||
|
||||
// UnmarshalText implements the TextEncoder interface
|
||||
func (l *LangRef) UnmarshalText(data []byte) error {
|
||||
*l = ""
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) > 2 {
|
||||
if data[0] == '"' && data[len(data)-1] == '"' {
|
||||
*l = LangRef(data[1 : len(data)-1])
|
||||
}
|
||||
} else {
|
||||
*l = LangRef(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LangRef) String() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
func (l LangRefValue) Equals(other LangRefValue) bool {
|
||||
return l.Ref == other.Ref && l.Value.Equals(other.Value)
|
||||
}
|
||||
|
||||
func (c *Content) UnmarshalJSON(data []byte) error {
|
||||
return c.UnmarshalText(data)
|
||||
}
|
||||
|
||||
func (c *Content) UnmarshalText(data []byte) error {
|
||||
*c = Content{}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) > 2 {
|
||||
if data[0] == '"' && data[len(data)-1] == '"' {
|
||||
*c = Content(data[1 : len(data)-1])
|
||||
}
|
||||
} else {
|
||||
*c = Content(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Content) GobEncode() ([]byte, error) {
|
||||
if len(c) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
if err := gobEncodeStringLikeType(gg, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Content) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
bb := make([]byte, 0)
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&bb); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = bb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Content) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func (c Content) Equals(other Content) bool {
|
||||
return bytes.Equal(c, other)
|
||||
}
|
||||
|
||||
func (c Content) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
_, _ = io.WriteString(s, string(c))
|
||||
case 'v':
|
||||
_, _ = fmt.Fprintf(s, "%q", string(c))
|
||||
}
|
||||
}
|
||||
|
||||
func unescape(b []byte) []byte {
|
||||
// FIXME(marius): I feel like I'm missing something really obvious about encoding/decoding from Json regarding
|
||||
// escape characters, and that this function is just a hack. Be better future Marius, find the real problem!
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'a'}, []byte{'\a'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'f'}, []byte{'\f'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'n'}, []byte{'\n'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'r'}, []byte{'\r'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 't'}, []byte{'\t'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'v'}, []byte{'\v'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', '"'}, []byte{'"'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', '\\'}, []byte{'\\'}) // this should cover the case of \\u -> \u
|
||||
return b
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (n *NaturalLanguageValues) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
// try our luck if data contains an unquoted string
|
||||
n.Append(NilLangRef, unescape(data))
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeObject:
|
||||
ob, _ := val.Object()
|
||||
ob.Visit(func(key []byte, v *fastjson.Value) {
|
||||
if dat := v.GetStringBytes(); len(dat) > 0 {
|
||||
n.Append(LangRef(key), unescape(dat))
|
||||
}
|
||||
})
|
||||
case fastjson.TypeString:
|
||||
if dat := val.GetStringBytes(); len(dat) > 0 {
|
||||
n.Append(NilLangRef, unescape(dat))
|
||||
}
|
||||
case fastjson.TypeArray:
|
||||
for _, v := range val.GetArray() {
|
||||
l := LangRefValue{}
|
||||
l.UnmarshalJSON([]byte(v.String()))
|
||||
if len(l.Value) > 0 {
|
||||
n.Append(l.Ref, l.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText tries to load the NaturalLanguage array from the incoming Text value
|
||||
func (n *NaturalLanguageValues) UnmarshalText(data []byte) error {
|
||||
if data[0] == '"' {
|
||||
// a quoted string - loading it to c.URL
|
||||
if data[len(data)-1] != '"' {
|
||||
return fmt.Errorf("invalid string value when unmarshaling %T value", n)
|
||||
}
|
||||
n.Append(LangRef(NilLangRef), Content(data[1:len(data)-1]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) GobEncode() ([]byte, error) {
|
||||
if len(n) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
mm := make([]kv, len(n))
|
||||
for i, l := range n {
|
||||
mm[i] = kv{K: []byte(l.Ref), V: l.Value}
|
||||
}
|
||||
if err := gg.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (n *NaturalLanguageValues) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
mm := make([]kv, 0)
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&mm); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range mm {
|
||||
*n = append(*n, LangRefValue{Ref: LangRef(m.K), Value: m.V})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (n NaturalLanguageValues) Equals(with NaturalLanguageValues) bool {
|
||||
if n.Count() != with.Count() {
|
||||
return false
|
||||
}
|
||||
for _, wv := range with {
|
||||
for _, nv := range n {
|
||||
if nv.Equals(wv) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
843
natural_language_values_test.go
Normal file
843
natural_language_values_test.go
Normal file
|
@ -0,0 +1,843 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
json "github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
func TestNaturalLanguageValue_MarshalJSON(t *testing.T) {
|
||||
p := NaturalLanguageValues{
|
||||
{
|
||||
"en", Content("the test"),
|
||||
},
|
||||
{
|
||||
"fr", Content("le test"),
|
||||
},
|
||||
}
|
||||
js := "{\"en\":\"the test\",\"fr\":\"le test\"}"
|
||||
out, err := p.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error: '%s'", err)
|
||||
}
|
||||
if js != string(out) {
|
||||
t.Errorf("Different marshal result '%s', instead of '%s'", out, js)
|
||||
}
|
||||
p1 := NaturalLanguageValues{
|
||||
{
|
||||
"en", Content("the test"),
|
||||
},
|
||||
}
|
||||
|
||||
out1, err1 := p1.MarshalJSON()
|
||||
|
||||
if err1 != nil {
|
||||
t.Errorf("Error: '%s'", err1)
|
||||
}
|
||||
txt := `"the test"`
|
||||
if txt != string(out1) {
|
||||
t.Errorf("Different marshal result '%s', instead of '%s'", out1, txt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRefValue_MarshalJSON(t *testing.T) {
|
||||
{
|
||||
tst := LangRefValue{
|
||||
Ref: NilLangRef,
|
||||
Value: Content("test"),
|
||||
}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
expected := `"test"`
|
||||
if string(j) != expected {
|
||||
t.Errorf("Different marshal result '%s', expected '%s'", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
tst := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test"),
|
||||
}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
expected := `"en":"test"`
|
||||
if string(j) != expected {
|
||||
t.Errorf("Different marshal result '%s', expected '%s'", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
tst := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test\nwith characters\tneeding escaping\r\n"),
|
||||
}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
expected := `"en":"test\nwith characters\tneeding escaping\r\n"`
|
||||
if string(j) != expected {
|
||||
t.Errorf("Different marshal result '%s', expected '%s'", j, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRefValue_MarshalText(t *testing.T) {
|
||||
{
|
||||
tst := LangRefValue{
|
||||
Ref: NilLangRef,
|
||||
Value: Content("test"),
|
||||
}
|
||||
j, err := tst.MarshalText()
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
expected := "test"
|
||||
if string(j) != expected {
|
||||
t.Errorf("Different marshal result '%s', expected '%s'", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
tst := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test"),
|
||||
}
|
||||
j, err := tst.MarshalText()
|
||||
if err != nil {
|
||||
t.Errorf("Error: %s", err)
|
||||
}
|
||||
expected := "test[en]"
|
||||
if string(j) != expected {
|
||||
t.Errorf("Different marshal result '%s', expected '%s'", j, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_Get(t *testing.T) {
|
||||
testVal := Content("test")
|
||||
a := NaturalLanguageValues{{NilLangRef, testVal}}
|
||||
if !a.Get(NilLangRef).Equals(testVal) {
|
||||
t.Errorf("Invalid Get result. Expected %s received %s", testVal, a.Get(NilLangRef))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_Set(t *testing.T) {
|
||||
testVal := Content("test")
|
||||
a := NaturalLanguageValues{{NilLangRef, Content("ana are mere")}}
|
||||
err := a.Set(LangRef("en"), testVal)
|
||||
if err != nil {
|
||||
t.Errorf("Received error when doing Set %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_Append(t *testing.T) {
|
||||
var a NaturalLanguageValues
|
||||
|
||||
if len(a) != 0 {
|
||||
t.Errorf("Invalid initialization of %T. Size %d > 0 ", a, len(a))
|
||||
}
|
||||
langEn := LangRef("en")
|
||||
valEn := Content("random value")
|
||||
|
||||
a.Append(langEn, valEn)
|
||||
if len(a) != 1 {
|
||||
t.Errorf("Invalid append of one element to %T. Size %d != 1", a, len(a))
|
||||
}
|
||||
if !a.Get(langEn).Equals(valEn) {
|
||||
t.Errorf("Invalid append of one element to %T. Value of %q not equal to %q, but %q", a, langEn, valEn, a.Get(langEn))
|
||||
}
|
||||
langDe := LangRef("de")
|
||||
valDe := Content("randomisch")
|
||||
a.Append(langDe, valDe)
|
||||
|
||||
if len(a) != 2 {
|
||||
t.Errorf("Invalid append of one element to %T. Size %d != 2", a, len(a))
|
||||
}
|
||||
if !a.Get(langEn).Equals(valEn) {
|
||||
t.Errorf("Invalid append of one element to %T. Value of %q not equal to %q, but %q", a, langEn, valEn, a.Get(langEn))
|
||||
}
|
||||
if !a.Get(langDe).Equals(valDe) {
|
||||
t.Errorf("Invalid append of one element to %T. Value of %q not equal to %q, but %q", a, langDe, valDe, a.Get(langDe))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRef_UnmarshalJSON(t *testing.T) {
|
||||
lang := "en-US"
|
||||
json := `"` + lang + `"`
|
||||
|
||||
var a LangRef
|
||||
a.UnmarshalJSON([]byte(json))
|
||||
|
||||
if string(a) != lang {
|
||||
t.Errorf("Invalid json unmarshal for %T. Expected %q, found %q", lang, lang, string(a))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_UnmarshalFullObjectJSON(t *testing.T) {
|
||||
langEn := "en-US"
|
||||
valEn := Content("random")
|
||||
langDe := "de-DE"
|
||||
valDe := Content("zufällig\n")
|
||||
|
||||
// m := make(map[string]string)
|
||||
// m[langEn] = valEn
|
||||
// m[langDe] = valDe
|
||||
|
||||
json := `{
|
||||
"` + langEn + `": "` + valEn.String() + `",
|
||||
"` + langDe + `": "` + valDe.String() + `"
|
||||
}`
|
||||
|
||||
a := make(NaturalLanguageValues, 0)
|
||||
_ = a.Append(LangRef(langEn), valEn)
|
||||
_ = a.Append(LangRef(langDe), valDe)
|
||||
err := a.UnmarshalJSON([]byte(json))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
for lang, val := range a {
|
||||
if val.Ref != LangRef(langEn) && val.Ref != LangRef(langDe) {
|
||||
t.Errorf("Invalid json unmarshal for %T. Expected lang %q or %q, found %q", a, langEn, langDe, lang)
|
||||
}
|
||||
|
||||
if val.Ref == LangRef(langEn) && !val.Value.Equals(valEn) {
|
||||
t.Errorf("Invalid json unmarshal for %T. Expected value %q, found %q", a, valEn, val.Value)
|
||||
}
|
||||
if val.Ref == LangRef(langDe) && !val.Value.Equals(valDe) {
|
||||
t.Errorf("Invalid json unmarshal for %T. Expected value %q, found %q", a, valDe, val.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_UnmarshalJSON(t *testing.T) {
|
||||
l := LangRef("")
|
||||
dataEmpty := []byte("")
|
||||
|
||||
l.UnmarshalJSON(dataEmpty)
|
||||
if l != "" {
|
||||
t.Errorf("Unmarshaled object %T should be an empty string, received %q", l, l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_UnmarshalText(t *testing.T) {
|
||||
l := LangRef("")
|
||||
dataEmpty := []byte("")
|
||||
|
||||
l.UnmarshalText(dataEmpty)
|
||||
if l != "" {
|
||||
t.Errorf("Unmarshaled object %T should be an empty string, received %q", l, l)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_First(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValueNew(t *testing.T) {
|
||||
n := NaturalLanguageValuesNew()
|
||||
|
||||
if len(n) != 0 {
|
||||
t.Errorf("Initial %T should have length 0, received %d", n, len(n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValue_MarshalText(t *testing.T) {
|
||||
nlv := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test"),
|
||||
}
|
||||
tst := NaturalLanguageValues{nlv}
|
||||
j, err := tst.MarshalText()
|
||||
if err != nil {
|
||||
t.Errorf("Error marshaling: %s", err)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Error marshaling: nil value returned")
|
||||
}
|
||||
expected := fmt.Sprintf("%s[%s]", nlv.Value, nlv.Ref)
|
||||
if string(j) != expected {
|
||||
t.Errorf("Wrong value: %s, expected %s", j, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_Append(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_First(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_Get(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_MarshalJSON(t *testing.T) {
|
||||
{
|
||||
m := NaturalLanguageValues{
|
||||
{
|
||||
"en", Content("test"),
|
||||
},
|
||||
{
|
||||
"de", Content("test"),
|
||||
},
|
||||
}
|
||||
result, err := m.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Failed marshaling '%v'", err)
|
||||
}
|
||||
mRes := "{\"en\":\"test\",\"de\":\"test\"}"
|
||||
if string(result) != mRes {
|
||||
t.Errorf("Different results '%v' vs. '%v'", string(result), mRes)
|
||||
}
|
||||
// n := NaturalLanguageValuesNew()
|
||||
// result, err := n.MarshalJSON()
|
||||
|
||||
s := make(map[LangRef]string)
|
||||
s["en"] = "test"
|
||||
n1 := NaturalLanguageValues{{
|
||||
"en", Content("test"),
|
||||
}}
|
||||
result1, err1 := n1.MarshalJSON()
|
||||
if err1 != nil {
|
||||
t.Errorf("Failed marshaling '%v'", err1)
|
||||
}
|
||||
mRes1 := `"test"`
|
||||
if string(result1) != mRes1 {
|
||||
t.Errorf("Different results '%v' vs. '%v'", string(result1), mRes1)
|
||||
}
|
||||
}
|
||||
{
|
||||
nlv := LangRefValue{
|
||||
Ref: NilLangRef,
|
||||
Value: Content("test"),
|
||||
}
|
||||
tst := NaturalLanguageValues{nlv}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error marshaling: %s", err)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Error marshaling: nil value returned")
|
||||
}
|
||||
expected := fmt.Sprintf("\"%s\"", nlv.Value)
|
||||
if string(j) != expected {
|
||||
t.Errorf("Wrong value: %s, expected %s", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
nlv := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test"),
|
||||
}
|
||||
tst := NaturalLanguageValues{nlv}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error marshaling: %s", err)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Error marshaling: nil value returned")
|
||||
}
|
||||
expected := fmt.Sprintf("\"%s\"", nlv.Value)
|
||||
if string(j) != expected {
|
||||
t.Errorf("Wrong value: %s, expected %s", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
nlvEn := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test"),
|
||||
}
|
||||
nlvFr := LangRefValue{
|
||||
Ref: "fr",
|
||||
Value: Content("teste"),
|
||||
}
|
||||
tst := NaturalLanguageValues{nlvEn, nlvFr}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error marshaling: %s", err)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Error marshaling: nil value returned")
|
||||
}
|
||||
expected := fmt.Sprintf("{\"%s\":\"%s\",\"%s\":\"%s\"}", nlvEn.Ref, nlvEn.Value, nlvFr.Ref, nlvFr.Value)
|
||||
if string(j) != expected {
|
||||
t.Errorf("Wrong value: %s, expected %s", j, expected)
|
||||
}
|
||||
}
|
||||
{
|
||||
nlvEn := LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test\nwith new line"),
|
||||
}
|
||||
nlvFr := LangRefValue{
|
||||
Ref: "fr",
|
||||
Value: Content("teste\navec une ligne nouvelle"),
|
||||
}
|
||||
tst := NaturalLanguageValues{nlvEn, nlvFr}
|
||||
j, err := tst.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("Error marshaling: %s", err)
|
||||
}
|
||||
if j == nil {
|
||||
t.Errorf("Error marshaling: nil value returned")
|
||||
}
|
||||
expected := fmt.Sprintf("{\"%s\":%s,\"%s\":%s}", nlvEn.Ref, strconv.Quote(nlvEn.Value.String()), nlvFr.Ref, strconv.Quote(nlvFr.Value.String()))
|
||||
if string(j) != expected {
|
||||
t.Errorf("Wrong value: %s, expected %s", j, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_MarshalText(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_Set(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_UnmarshalJSON(t *testing.T) {
|
||||
{
|
||||
lang := []byte{'e', 'n'}
|
||||
val := []byte{'a', 'n', 'a', ' ', 'a', 'r', 'e', ' ', 'm', 'e', 'r', 'e', '\n'}
|
||||
js := fmt.Sprintf(`[{"%s": "%s"}]`, lang, val)
|
||||
n := NaturalLanguageValues{}
|
||||
err := n.UnmarshalJSON([]byte(js))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when unmarshaling %T: %s", n, err)
|
||||
}
|
||||
|
||||
if n.Count() != 1 {
|
||||
t.Errorf("Invalid number of elements %d, expected %d", n.Count(), 1)
|
||||
}
|
||||
l := n.First()
|
||||
if !l.Value.Equals(Content("ana are mere\n")) {
|
||||
t.Errorf("Invalid %T value %q, expected %q", l, l.Value, "ana are mere\n")
|
||||
}
|
||||
if l.Ref != "en" {
|
||||
t.Errorf("Invalid %T ref %q, expected %q", l, l.Ref, "en")
|
||||
}
|
||||
}
|
||||
{
|
||||
ob := make(map[string]string)
|
||||
ob["en"] = "ana are mere\n"
|
||||
js, err := json.Marshal(ob)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when marshaling %T: %s", ob, err)
|
||||
}
|
||||
n := NaturalLanguageValues{}
|
||||
err = n.UnmarshalJSON(js)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error when unmarshaling %T: %s", n, err)
|
||||
}
|
||||
|
||||
if n.Count() != 1 {
|
||||
t.Errorf("Invalid number of elements %d, expected %d", n.Count(), 1)
|
||||
}
|
||||
l := n.First()
|
||||
if !l.Value.Equals(Content("ana are mere\n")) {
|
||||
t.Errorf("Invalid %T value %q, expected %q", l, l.Value, "ana are mere\n")
|
||||
}
|
||||
if l.Ref != "en" {
|
||||
t.Errorf("Invalid %T ref %q, expected %q", l, l.Ref, "en")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_UnmarshalText(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValuesNew(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_String(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_Count(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_Equals(t *testing.T) {
|
||||
type args struct {
|
||||
with NaturalLanguageValues
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
n NaturalLanguageValues
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "equal-key-value",
|
||||
n: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test123#"),
|
||||
}},
|
||||
args: args{
|
||||
with: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test123#"),
|
||||
}},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not-equal-key",
|
||||
n: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test123#"),
|
||||
}},
|
||||
args: args{
|
||||
with: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "fr",
|
||||
Value: Content("test123#"),
|
||||
}},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not-equal-value",
|
||||
n: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test123#"),
|
||||
}},
|
||||
args: args{
|
||||
with: NaturalLanguageValues{LangRefValue{
|
||||
Ref: "en",
|
||||
Value: Content("test123"),
|
||||
}},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.n.Equals(tt.args.with); got != tt.want {
|
||||
t.Errorf("Equals() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContent_String(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestContent_UnmarshalJSON(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestContent_UnmarshalText(t *testing.T) {
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func gobValue(a interface{}) []byte {
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
gg.Encode(a)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func TestContent_GobEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c Content
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
c: Content{},
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
c: Content{'0'},
|
||||
want: gobValue([]byte{'0'}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some text",
|
||||
c: Content{'a', 'n', 'a', ' ', 'a', 'r', 'e'},
|
||||
want: gobValue([]byte{'a', 'n', 'a', ' ', 'a', 'r', 'e'}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.c.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContent_GobDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c Content
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
c: Content{},
|
||||
data: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
c: Content{'0'},
|
||||
data: gobValue([]byte{'0'}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some text",
|
||||
c: Content{'a', 'n', 'a', ' ', 'a', 'r', 'e'},
|
||||
data: gobValue([]byte{'a', 'n', 'a', ' ', 'a', 'r', 'e'}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.c.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRef_GobDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
l LangRef
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
l: "",
|
||||
data: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some text",
|
||||
l: LangRef("ana are"),
|
||||
data: gobValue([]byte("ana are")),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.l.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRef_GobEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
l LangRef
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
l: "",
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some text",
|
||||
l: LangRef("ana are"),
|
||||
want: gobValue([]byte{'a', 'n', 'a', ' ', 'a', 'r', 'e'}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.l.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRefValue_GobEncode(t *testing.T) {
|
||||
type fields struct {
|
||||
Ref LangRef
|
||||
Value Content
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some values",
|
||||
fields: fields{
|
||||
Ref: "ana",
|
||||
Value: Content("are mere"),
|
||||
},
|
||||
want: gobValue(kv{K: []byte("ana"), V: []byte("are mere")}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := LangRefValue{
|
||||
Ref: tt.fields.Ref,
|
||||
Value: tt.fields.Value,
|
||||
}
|
||||
got, err := l.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLangRefValue_GobDecode(t *testing.T) {
|
||||
type fields struct {
|
||||
Ref LangRef
|
||||
Value Content
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
fields: fields{},
|
||||
data: gobValue(kv{}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some values",
|
||||
fields: fields{
|
||||
Ref: "ana",
|
||||
Value: Content("are mere"),
|
||||
},
|
||||
data: gobValue(kv{K: []byte("ana"), V: []byte("are mere")}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &LangRefValue{
|
||||
Ref: tt.fields.Ref,
|
||||
Value: tt.fields.Value,
|
||||
}
|
||||
if err := l.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_GobEncode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n NaturalLanguageValues
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
n: NaturalLanguageValues{},
|
||||
want: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some values",
|
||||
n: NaturalLanguageValues{{
|
||||
Ref: "ana",
|
||||
Value: []byte("are mere"),
|
||||
}},
|
||||
want: gobValue([]kv{{K: []byte("ana"), V: []byte("are mere")}}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.n.GobEncode()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("GobEncode() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNaturalLanguageValues_GobDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
n NaturalLanguageValues
|
||||
data []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
n: NaturalLanguageValues{},
|
||||
data: []byte{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "some values",
|
||||
n: NaturalLanguageValues{{
|
||||
Ref: "ana",
|
||||
Value: []byte("are mere"),
|
||||
}},
|
||||
data: gobValue([]kv{{K: []byte("ana"), V: []byte("are mere")}}),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.n.GobDecode(tt.data); (err != nil) != tt.wantErr {
|
||||
t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
20
object_id.go
Normal file
20
object_id.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package activitypub
|
||||
|
||||
// ID designates a unique global identifier.
|
||||
// All Objects in [ActivityStreams] should have unique global identifiers.
|
||||
// ActivityPub extends this requirement; all objects distributed by the ActivityPub protocol MUST
|
||||
// have unique global identifiers, unless they are intentionally transient
|
||||
// (short-lived activities that are not intended to be able to be looked up,
|
||||
// such as some kinds of chat messages or game notifications).
|
||||
// These identifiers must fall into one of the following groups:
|
||||
//
|
||||
// 1. Publicly de-referenceable URIs, such as HTTPS URIs, with their authority belonging
|
||||
// to that of their originating server. (Publicly facing content SHOULD use HTTPS URIs).
|
||||
// 2. An ID explicitly specified as the JSON null object, which implies an anonymous object
|
||||
// (a part of its parent context)
|
||||
type ID = IRI
|
||||
|
||||
// IsValid returns if the receiver pointer is not nil and if dereferenced it has a positive length.
|
||||
func (i *ID) IsValid() bool {
|
||||
return i != nil && len(*i) > 0
|
||||
}
|
110
object_id_test.go
Normal file
110
object_id_test.go
Normal file
|
@ -0,0 +1,110 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestID_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want ID
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
data: []byte(nil),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
data: []byte(""),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "something",
|
||||
data: []byte("something"),
|
||||
want: "something",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ID("")
|
||||
got.UnmarshalJSON(tt.data)
|
||||
if got != tt.want {
|
||||
t.Errorf("UnmarshalJSON() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestID_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i ID
|
||||
want []byte
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "nil",
|
||||
i: "",
|
||||
want: []byte(nil),
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
i: "",
|
||||
want: []byte(""),
|
||||
},
|
||||
{
|
||||
name: "something",
|
||||
i: "something",
|
||||
want: []byte(`"something"`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.i.MarshalJSON()
|
||||
if tt.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("MarshalJSON() returned no error but expected %v", tt.wantErr)
|
||||
}
|
||||
if tt.wantErr.Error() != err.Error() {
|
||||
t.Errorf("MarshalJSON() returned error %v but expected %v", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("MarshalJSON() = %s, want %s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Skip("TODO")
|
||||
}
|
||||
|
||||
func TestID_IsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
i ID
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
i: "",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "something",
|
||||
i: "something",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.i.IsValid(); got != tt.want {
|
||||
t.Errorf("IsValid() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
1267
object_test.go
Normal file
1267
object_test.go
Normal file
File diff suppressed because it is too large
Load diff
428
ordered_collection.go
Normal file
428
ordered_collection.go
Normal file
|
@ -0,0 +1,428 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// OrderedCollection is a subtype of Collection in which members of the logical
|
||||
// collection are assumed to always be strictly ordered.
|
||||
type OrderedCollection struct {
|
||||
// ID provides the globally unique identifier for an Activity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be ordered or unordered.
|
||||
OrderedItems ItemCollection `jsonld:"orderedItems,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
// InboxStream contains all activities received by the actor.
|
||||
// The server SHOULD filter content according to the requester's permission.
|
||||
// In general, the owner of an inbox is likely to be able to access all of their inbox contents.
|
||||
// Depending on access control, some other content may be public, whereas other content may
|
||||
// require authentication for non-owner users, if they can access the inbox at all.
|
||||
InboxStream = OrderedCollection
|
||||
|
||||
// LikedCollection is a list of every object from all of the actor's Like activities,
|
||||
// added as a side effect. The liked collection MUST be either an OrderedCollection or
|
||||
// a Collection and MAY be filtered on privileges of an authenticated user or as
|
||||
// appropriate when no authentication is given.
|
||||
LikedCollection = OrderedCollection
|
||||
|
||||
// LikesCollection is a list of all Like activities with this object as the object property,
|
||||
// added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when
|
||||
// no authentication is given.
|
||||
LikesCollection = OrderedCollection
|
||||
|
||||
// OutboxStream contains activities the user has published,
|
||||
// subject to the ability of the requestor to retrieve the activity (that is,
|
||||
// the contents of the outbox are filtered by the permissions of the person reading it).
|
||||
OutboxStream = OrderedCollection
|
||||
|
||||
// SharesCollection is a list of all Announce activities with this object as the object property,
|
||||
// added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication
|
||||
// is given.
|
||||
SharesCollection = OrderedCollection
|
||||
)
|
||||
|
||||
// GetType returns the OrderedCollection's type
|
||||
func (o OrderedCollection) GetType() ActivityVocabularyType {
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for an OrderedCollection object
|
||||
func (o OrderedCollection) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the OrderedCollection
|
||||
func (o OrderedCollection) GetID() ID {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the OrderedCollection object
|
||||
func (o OrderedCollection) GetLink() IRI {
|
||||
return IRI(o.ID)
|
||||
}
|
||||
|
||||
// IsObject returns true for am OrderedCollection object
|
||||
func (o OrderedCollection) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Collection returns the underlying Collection type
|
||||
func (o OrderedCollection) Collection() ItemCollection {
|
||||
return o.OrderedItems
|
||||
}
|
||||
|
||||
// IsCollection returns true for OrderedCollection objects.
|
||||
func (o OrderedCollection) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains verifies if OrderedCollection array contains the received item r.
|
||||
func (o OrderedCollection) Contains(r Item) bool {
|
||||
if len(o.OrderedItems) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range o.OrderedItems {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in collection and its TotalItems property
|
||||
func (o *OrderedCollection) Count() uint {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(o.OrderedItems))
|
||||
}
|
||||
|
||||
// Append adds an element to an the receiver collection object.
|
||||
func (o *OrderedCollection) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if o.OrderedItems.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
o.OrderedItems = append(o.OrderedItems, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (o *OrderedCollection) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadOrderedCollection(val, o)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (o OrderedCollection) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(o, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if o.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", o.Current) || notEmpty
|
||||
}
|
||||
if o.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", o.First) || notEmpty
|
||||
}
|
||||
if o.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", o.Last) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty
|
||||
if o.OrderedItems != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "orderedItems", o.OrderedItems, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (o *OrderedCollection) UnmarshalBinary(data []byte) error {
|
||||
return o.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (o OrderedCollection) MarshalBinary() ([]byte, error) {
|
||||
return o.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (o OrderedCollection) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapOrderedCollectionProperties(mm, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (o *OrderedCollection) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapOrderedCollectionProperties(mm, o)
|
||||
}
|
||||
|
||||
// OrderedCollectionPageNew initializes a new OrderedCollectionPage
|
||||
func OrderedCollectionPageNew(parent CollectionInterface) *OrderedCollectionPage {
|
||||
p := OrderedCollectionPage{
|
||||
PartOf: parent.GetLink(),
|
||||
}
|
||||
if pc, ok := parent.(*OrderedCollection); ok {
|
||||
copyOrderedCollectionToPage(pc, &p)
|
||||
}
|
||||
p.Type = OrderedCollectionPageType
|
||||
return &p
|
||||
}
|
||||
|
||||
// ToOrderedCollection
|
||||
func ToOrderedCollection(it Item) (*OrderedCollection, error) {
|
||||
switch i := it.(type) {
|
||||
case *OrderedCollection:
|
||||
return i, nil
|
||||
case OrderedCollection:
|
||||
return &i, nil
|
||||
case *OrderedCollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(&i)), nil
|
||||
// NOTE(marius): let's try again to convert Collection -> OrderedCollection, as they have the same
|
||||
// shape in memory.
|
||||
case *Collection:
|
||||
return (*OrderedCollection)(unsafe.Pointer(i)), nil
|
||||
case Collection:
|
||||
return (*OrderedCollection)(unsafe.Pointer(&i)), nil
|
||||
case *CollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(i)), nil
|
||||
case CollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[OrderedCollection](it)
|
||||
}
|
||||
}
|
||||
|
||||
func copyOrderedCollectionToPage(c *OrderedCollection, p *OrderedCollectionPage) error {
|
||||
p.Type = OrderedCollectionPageType
|
||||
p.Name = c.Name
|
||||
p.Content = c.Content
|
||||
p.Summary = c.Summary
|
||||
p.Context = c.Context
|
||||
p.URL = c.URL
|
||||
p.MediaType = c.MediaType
|
||||
p.Generator = c.Generator
|
||||
p.AttributedTo = c.AttributedTo
|
||||
p.Attachment = c.Attachment
|
||||
p.Location = c.Location
|
||||
p.Published = c.Published
|
||||
p.StartTime = c.StartTime
|
||||
p.EndTime = c.EndTime
|
||||
p.Duration = c.Duration
|
||||
p.Icon = c.Icon
|
||||
p.Preview = c.Preview
|
||||
p.Image = c.Image
|
||||
p.Updated = c.Updated
|
||||
p.InReplyTo = c.InReplyTo
|
||||
p.To = c.To
|
||||
p.Audience = c.Audience
|
||||
p.Bto = c.Bto
|
||||
p.CC = c.CC
|
||||
p.BCC = c.BCC
|
||||
p.Replies = c.Replies
|
||||
p.Tag = c.Tag
|
||||
p.TotalItems = c.TotalItems
|
||||
p.OrderedItems = c.OrderedItems
|
||||
p.Current = c.Current
|
||||
p.First = c.First
|
||||
p.PartOf = c.GetLink()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (o OrderedCollection) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := o.OrderedItems.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (o OrderedCollection) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
_ = OnOrderedCollection(with, func(w *OrderedCollection) error {
|
||||
_ = OnCollection(w, func(wo *Collection) error {
|
||||
if !wo.Equals(o) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.OrderedItems != nil {
|
||||
if !o.OrderedItems.Equals(w.OrderedItems) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (o OrderedCollection) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", o, o.Type, o.TotalItems)
|
||||
}
|
||||
}
|
||||
func (o *OrderedCollection) Recipients() ItemCollection {
|
||||
aud := o.Audience
|
||||
return ItemCollectionDeduplication(&o.To, &o.CC, &o.Bto, &o.BCC, &aud)
|
||||
}
|
||||
|
||||
func (o *OrderedCollection) Clean() {
|
||||
_ = OnObject(o, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
391
ordered_collection_page.go
Normal file
391
ordered_collection_page.go
Normal file
|
@ -0,0 +1,391 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// OrderedCollectionPage type extends from both CollectionPage and OrderedCollection.
|
||||
// In addition to the properties inherited from each of those, the OrderedCollectionPage
|
||||
// may contain an additional startIndex property whose value indicates the relative index position
|
||||
// of the first item contained by the page within the OrderedCollection to which the page belongs.
|
||||
type OrderedCollectionPage struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be ordered or unordered.
|
||||
OrderedItems ItemCollection `jsonld:"orderedItems,omitempty"`
|
||||
// Identifies the Collection to which a CollectionPage objects items belong.
|
||||
PartOf Item `jsonld:"partOf,omitempty"`
|
||||
// In a paged Collection, indicates the next page of items.
|
||||
Next Item `jsonld:"next,omitempty"`
|
||||
// In a paged Collection, identifies the previous page of items.
|
||||
Prev Item `jsonld:"prev,omitempty"`
|
||||
// A non-negative integer value identifying the relative position within the logical view of a strictly ordered collection.
|
||||
StartIndex uint `jsonld:"startIndex,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) GetID() ID {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
// GetType returns the OrderedCollectionPage's type
|
||||
func (o OrderedCollectionPage) GetType() ActivityVocabularyType {
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for a OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns true for OrderedCollectionPage objects
|
||||
func (o OrderedCollectionPage) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) GetLink() IRI {
|
||||
return IRI(o.ID)
|
||||
}
|
||||
|
||||
// Collection returns the underlying Collection type
|
||||
func (o OrderedCollectionPage) Collection() ItemCollection {
|
||||
return o.OrderedItems
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in the collection page and its TotalItems property
|
||||
func (o *OrderedCollectionPage) Count() uint {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(o.OrderedItems))
|
||||
}
|
||||
|
||||
// Append adds an element to an OrderedCollectionPage
|
||||
func (o *OrderedCollectionPage) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if o.OrderedItems.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
o.OrderedItems = append(o.OrderedItems, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains verifies if OrderedCollectionPage array contains the received one
|
||||
func (o OrderedCollectionPage) Contains(r Item) bool {
|
||||
if len(o.OrderedItems) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range o.OrderedItems {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadOrderedCollectionPage(val, o)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (o OrderedCollectionPage) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(o, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if o.PartOf != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "partOf", o.PartOf) || notEmpty
|
||||
}
|
||||
if o.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", o.Current) || notEmpty
|
||||
}
|
||||
if o.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", o.First) || notEmpty
|
||||
}
|
||||
if o.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", o.Last) || notEmpty
|
||||
}
|
||||
if o.Next != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "next", o.Next) || notEmpty
|
||||
}
|
||||
if o.Prev != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "prev", o.Prev) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty
|
||||
if o.OrderedItems != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "orderedItems", o.OrderedItems, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (o *OrderedCollectionPage) UnmarshalBinary(data []byte) error {
|
||||
return o.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (o OrderedCollectionPage) MarshalBinary() ([]byte, error) {
|
||||
return o.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (o OrderedCollectionPage) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapOrderedCollectionPageProperties(mm, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (o *OrderedCollectionPage) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapOrderedCollectionPageProperties(mm, o)
|
||||
}
|
||||
|
||||
// ToOrderedCollectionPage
|
||||
func ToOrderedCollectionPage(it Item) (*OrderedCollectionPage, error) {
|
||||
switch i := it.(type) {
|
||||
case *OrderedCollectionPage:
|
||||
return i, nil
|
||||
case OrderedCollectionPage:
|
||||
return &i, nil
|
||||
// NOTE(marius): let's try again to convert CollectionPage -> OrderedCollectionPage, as they have the same
|
||||
// shape in memory.
|
||||
case *CollectionPage:
|
||||
return (*OrderedCollectionPage)(unsafe.Pointer(i)), nil
|
||||
case CollectionPage:
|
||||
return (*OrderedCollectionPage)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[OrderedCollectionPage](it)
|
||||
}
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (o OrderedCollectionPage) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := o.OrderedItems.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (o OrderedCollectionPage) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
|
||||
OnOrderedCollectionPage(with, func(w *OrderedCollectionPage) error {
|
||||
OnOrderedCollection(w, func(wo *OrderedCollection) error {
|
||||
if !wo.Equals(o) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.PartOf != nil {
|
||||
if !ItemsEqual(o.PartOf, w.PartOf) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Current != nil {
|
||||
if !ItemsEqual(o.Current, w.Current) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.First != nil {
|
||||
if !ItemsEqual(o.First, w.First) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Last != nil {
|
||||
if !ItemsEqual(o.Last, w.Last) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Next != nil {
|
||||
if !ItemsEqual(o.Next, w.Next) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Prev != nil {
|
||||
if !ItemsEqual(o.Prev, w.Prev) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (o OrderedCollectionPage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", o, o.Type, o.TotalItems)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedCollectionPage) Recipients() ItemCollection {
|
||||
aud := o.Audience
|
||||
return ItemCollectionDeduplication(&o.To, &o.CC, &o.Bto, &o.BCC, &aud)
|
||||
}
|
||||
|
||||
func (o *OrderedCollectionPage) Clean() {
|
||||
_ = OnObject(o, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
169
ordered_collection_page_test.go
Normal file
169
ordered_collection_page_test.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrderedCollectionPageNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(testValue)
|
||||
p := OrderedCollectionPageNew(c)
|
||||
if reflect.DeepEqual(p, c) {
|
||||
t.Errorf("Invalid ordered collection parent '%v'", p.PartOf)
|
||||
}
|
||||
if p.PartOf != c.GetLink() {
|
||||
t.Errorf("Invalid collection '%v'", p.PartOf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollectionPage_UnmarshalJSON(t *testing.T) {
|
||||
p := OrderedCollectionPage{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
p.UnmarshalJSON(dataEmpty)
|
||||
if p.ID != "" {
|
||||
t.Errorf("Unmarshaled object should have empty ID, received %q", p.ID)
|
||||
}
|
||||
if p.Type != "" {
|
||||
t.Errorf("Unmarshaled object should have empty Type, received %q", p.Type)
|
||||
}
|
||||
if p.AttributedTo != nil {
|
||||
t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", p.AttributedTo)
|
||||
}
|
||||
if len(p.Name) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Name, received %q", p.Name)
|
||||
}
|
||||
if len(p.Summary) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Summary, received %q", p.Summary)
|
||||
}
|
||||
if len(p.Content) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Content, received %q", p.Content)
|
||||
}
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if len(p.OrderedItems) > 0 {
|
||||
t.Errorf("Unmarshaled object should have empty OrderedItems, received %v", p.OrderedItems)
|
||||
}
|
||||
if p.URL != nil {
|
||||
t.Errorf("Unmarshaled object should have empty URL, received %v", p.URL)
|
||||
}
|
||||
if !p.Published.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Published, received %q", p.Published)
|
||||
}
|
||||
if !p.StartTime.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty StartTime, received %q", p.StartTime)
|
||||
}
|
||||
if !p.Updated.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Updated, received %q", p.Updated)
|
||||
}
|
||||
if p.PartOf != nil {
|
||||
t.Errorf("Unmarshaled object should have empty PartOf, received %q", p.PartOf)
|
||||
}
|
||||
if p.Current != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Current, received %q", p.Current)
|
||||
}
|
||||
if p.First != nil {
|
||||
t.Errorf("Unmarshaled object should have empty First, received %q", p.First)
|
||||
}
|
||||
if p.Last != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Last, received %q", p.Last)
|
||||
}
|
||||
if p.Next != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Next, received %q", p.Next)
|
||||
}
|
||||
if p.Prev != nil {
|
||||
t.Errorf("Unmarshaled object should have empty Prev, received %q", p.Prev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollectionPage_Append(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
val := Object{ID: ID("grrr")}
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
p := OrderedCollectionPageNew(c)
|
||||
p.Append(val)
|
||||
|
||||
if p.PartOf != c.GetLink() {
|
||||
t.Errorf("OrderedCollection page should point to OrderedCollection %q", c.GetLink())
|
||||
}
|
||||
if p.Count() != 1 {
|
||||
t.Errorf("OrderedCollection page of %q should have exactly one element", p.GetID())
|
||||
}
|
||||
if !reflect.DeepEqual(p.OrderedItems[0], val) {
|
||||
t.Errorf("First item in Inbox is does not match %q", val.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollectionPage_Collection(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
p := OrderedCollectionPageNew(c)
|
||||
|
||||
if !reflect.DeepEqual(p.Collection(), p.OrderedItems) {
|
||||
t.Errorf("Collection items should be equal %v %v", p.Collection(), p.OrderedItems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollectionPage_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToOrderedCollectionPage(t *testing.T) {
|
||||
err := func(it Item) error { return ErrorInvalidType[OrderedCollectionPage](it) }
|
||||
tests := map[string]struct {
|
||||
it Item
|
||||
want *OrderedCollectionPage
|
||||
wantErr error
|
||||
}{
|
||||
"OrderedCollectionPage": {
|
||||
it: new(OrderedCollectionPage),
|
||||
want: new(OrderedCollectionPage),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollection": {
|
||||
it: new(OrderedCollection),
|
||||
want: new(OrderedCollectionPage),
|
||||
wantErr: err(new(OrderedCollection)),
|
||||
},
|
||||
"Collection": {
|
||||
it: new(Collection),
|
||||
want: new(OrderedCollectionPage),
|
||||
wantErr: err(new(Collection)),
|
||||
},
|
||||
"CollectionPage": {
|
||||
it: new(CollectionPage),
|
||||
want: new(OrderedCollectionPage),
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := ToOrderedCollectionPage(tt.it)
|
||||
if tt.wantErr != nil && err == nil {
|
||||
t.Errorf("ToOrderedCollectionPage() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if tt.wantErr == nil {
|
||||
t.Errorf("ToOrderedCollectionPage() returned unexpected error[%T]%s", err, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.wantErr) {
|
||||
t.Errorf("ToOrderedCollectionPage() received error %v, wanted error %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToOrderedCollectionPage() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
282
ordered_collection_test.go
Normal file
282
ordered_collection_test.go
Normal file
|
@ -0,0 +1,282 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOrderedCollectionNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(testValue)
|
||||
|
||||
if c.ID != testValue {
|
||||
t.Errorf("APObject Id '%v' different than expected '%v'", c.ID, testValue)
|
||||
}
|
||||
if c.Type != OrderedCollectionType {
|
||||
t.Errorf("APObject Type '%v' different than expected '%v'", c.Type, OrderedCollectionType)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_OrderedCollection_Append(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
val := Object{ID: ID("grrr")}
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
c.Append(val)
|
||||
|
||||
if c.Count() != 1 {
|
||||
t.Errorf("Inbox collection of %q should have one element", c.GetID())
|
||||
}
|
||||
if !reflect.DeepEqual(c.OrderedItems[0], val) {
|
||||
t.Errorf("First item in Inbox is does not match %q", val.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_Append(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
val := Object{ID: ID("grrr")}
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
p := OrderedCollectionPageNew(c)
|
||||
p.Append(val)
|
||||
|
||||
if p.PartOf != c.GetLink() {
|
||||
t.Errorf("Ordereed collection page should point to ordered collection %q", c.GetLink())
|
||||
}
|
||||
if p.Count() != 1 {
|
||||
t.Errorf("Ordered collection page of %q should have exactly one element", p.GetID())
|
||||
}
|
||||
if !reflect.DeepEqual(p.OrderedItems[0], val) {
|
||||
t.Errorf("First item in Inbox is does not match %q", val.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_Collection(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
o := OrderedCollectionNew(id)
|
||||
|
||||
if !reflect.DeepEqual(o.Collection(), o.OrderedItems) {
|
||||
t.Errorf("Collection items should be equal %v %v", o.Collection(), o.OrderedItems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_GetID(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.GetID() != id {
|
||||
t.Errorf("GetID should return %q, received %q", id, c.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_GetLink(t *testing.T) {
|
||||
id := ID("test")
|
||||
link := IRI(id)
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.GetLink() != link {
|
||||
t.Errorf("GetLink should return %q, received %q", link, c.GetLink())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_GetType(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.GetType() != OrderedCollectionType {
|
||||
t.Errorf("OrderedCollection Type should be %q, received %q", OrderedCollectionType, c.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_IsLink(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.IsLink() != false {
|
||||
t.Errorf("OrderedCollection should not be a link, received %t", c.IsLink())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_IsObject(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.IsObject() != true {
|
||||
t.Errorf("OrderedCollection should be an object, received %t", c.IsObject())
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_UnmarshalJSON(t *testing.T) {
|
||||
c := OrderedCollection{}
|
||||
|
||||
dataEmpty := []byte("{}")
|
||||
c.UnmarshalJSON(dataEmpty)
|
||||
if c.ID != "" {
|
||||
t.Errorf("Unmarshaled object should have empty ID, received %q", c.ID)
|
||||
}
|
||||
if c.Type != "" {
|
||||
t.Errorf("Unmarshaled object should have empty Type, received %q", c.Type)
|
||||
}
|
||||
if c.AttributedTo != nil {
|
||||
t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", c.AttributedTo)
|
||||
}
|
||||
if len(c.Name) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Name, received %q", c.Name)
|
||||
}
|
||||
if len(c.Summary) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Summary, received %q", c.Summary)
|
||||
}
|
||||
if len(c.Content) != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty Content, received %q", c.Content)
|
||||
}
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Unmarshaled object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if len(c.OrderedItems) > 0 {
|
||||
t.Errorf("Unmarshaled object should have empty OrderedItems, received %v", c.OrderedItems)
|
||||
}
|
||||
if c.URL != nil {
|
||||
t.Errorf("Unmarshaled object should have empty URL, received %v", c.URL)
|
||||
}
|
||||
if !c.Published.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Published, received %q", c.Published)
|
||||
}
|
||||
if !c.StartTime.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty StartTime, received %q", c.StartTime)
|
||||
}
|
||||
if !c.Updated.IsZero() {
|
||||
t.Errorf("Unmarshaled object should have empty Updated, received %q", c.Updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_Count(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if len(c.OrderedItems) > 0 {
|
||||
t.Errorf("Empty object should have empty Items, received %v", c.OrderedItems)
|
||||
}
|
||||
if c.Count() != uint(len(c.OrderedItems)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.OrderedItems))
|
||||
}
|
||||
|
||||
c.Append(IRI("test"))
|
||||
if c.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems)
|
||||
}
|
||||
if c.Count() != uint(len(c.OrderedItems)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.OrderedItems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollectionPage_Count(t *testing.T) {
|
||||
id := ID("test")
|
||||
|
||||
c := OrderedCollectionNew(id)
|
||||
p := OrderedCollectionPageNew(c)
|
||||
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if len(p.OrderedItems) > 0 {
|
||||
t.Errorf("Empty object should have empty Items, received %v", p.OrderedItems)
|
||||
}
|
||||
if p.Count() != uint(len(p.OrderedItems)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.OrderedItems))
|
||||
}
|
||||
|
||||
p.Append(IRI("test"))
|
||||
if p.TotalItems != 0 {
|
||||
t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems)
|
||||
}
|
||||
if p.Count() != uint(len(p.OrderedItems)) {
|
||||
t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.OrderedItems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnOrderedCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToOrderedCollection(t *testing.T) {
|
||||
//err := func(it Item) error { return ErrorInvalidType[OrderedCollection](it) }
|
||||
tests := map[string]struct {
|
||||
it Item
|
||||
want *OrderedCollection
|
||||
wantErr error
|
||||
}{
|
||||
"OrderedCollection": {
|
||||
it: new(OrderedCollection),
|
||||
want: new(OrderedCollection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"OrderedCollectionPage": {
|
||||
it: new(OrderedCollectionPage),
|
||||
want: new(OrderedCollection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"Collection": {
|
||||
it: new(Collection),
|
||||
want: new(OrderedCollection),
|
||||
wantErr: nil,
|
||||
},
|
||||
"CollectionPage": {
|
||||
it: new(CollectionPage),
|
||||
want: new(OrderedCollection),
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := ToOrderedCollection(tt.it)
|
||||
if tt.wantErr != nil && err == nil {
|
||||
t.Errorf("ToOrderedCollection() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if tt.wantErr == nil {
|
||||
t.Errorf("ToOrderedCollection() returned unexpected error[%T]%s", err, err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(err, tt.wantErr) {
|
||||
t.Errorf("ToOrderedCollection() received error %v, wanted error %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ToOrderedCollection() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderedCollection_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrderedCollection_MarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrderedCollection_ItemMatches(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestOrderedCollection_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
303
place.go
Normal file
303
place.go
Normal file
|
@ -0,0 +1,303 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Place represents a logical or physical location. See 5.3 Representing Places for additional information.
|
||||
type Place struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Accuracy indicates the accuracy of position coordinates on a Place objects.
|
||||
// Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
|
||||
Accuracy float64 `jsonld:"accuracy,omitempty"`
|
||||
// Altitude indicates the altitude of a place. The measurement units is indicated using the units property.
|
||||
// If units is not specified, the default is assumed to be "m" indicating meters.
|
||||
Altitude float64 `jsonld:"altitude,omitempty"`
|
||||
// Latitude the latitude of a place
|
||||
Latitude float64 `jsonld:"latitude,omitempty"`
|
||||
// Longitude the longitude of a place
|
||||
Longitude float64 `jsonld:"longitude,omitempty"`
|
||||
// Radius the radius from the given latitude and longitude for a Place.
|
||||
// The units is expressed by the units property. If units is not specified,
|
||||
// the default is assumed to be "m" indicating "meters".
|
||||
Radius int64 `jsonld:"radius,omitempty"`
|
||||
// Specifies the measurement units for the radius and altitude properties on a Place object.
|
||||
// If not specified, the default is assumed to be "m" for "meters".
|
||||
// Values "cm" | " feet" | " inches" | " km" | " m" | " miles" | xsd:anyURI
|
||||
Units string `jsonld:"units,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Place objects
|
||||
func (p Place) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Place objects
|
||||
func (p Place) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Place objects
|
||||
func (p Place) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Place object
|
||||
func (p Place) GetLink() IRI {
|
||||
return IRI(p.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Place
|
||||
func (p Place) GetType() ActivityVocabularyType {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Place
|
||||
func (p Place) GetID() ID {
|
||||
return p.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (p *Place) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadPlace(val, p)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (p Place) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(p, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if p.Accuracy > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "accuracy", p.Accuracy) || notEmpty
|
||||
}
|
||||
if p.Altitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "altitude", p.Altitude) || notEmpty
|
||||
}
|
||||
if p.Latitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "latitude", p.Latitude) || notEmpty
|
||||
}
|
||||
if p.Longitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "longitude", p.Longitude) || notEmpty
|
||||
}
|
||||
if p.Radius > 0 {
|
||||
notEmpty = JSONWriteIntProp(&b, "radius", p.Radius) || notEmpty
|
||||
}
|
||||
if len(p.Units) > 0 {
|
||||
notEmpty = JSONWriteStringProp(&b, "radius", p.Units) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (p *Place) UnmarshalBinary(data []byte) error {
|
||||
return p.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (p Place) MarshalBinary() ([]byte, error) {
|
||||
return p.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (p Place) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapPlaceProperties(mm, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (p *Place) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapPlaceProperties(mm, p)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Place object's To, Bto, CC and BCC properties
|
||||
func (p *Place) Recipients() ItemCollection {
|
||||
aud := p.Audience
|
||||
return ItemCollectionDeduplication(&p.To, &p.CC, &p.Bto, &p.BCC, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (p *Place) Clean() {
|
||||
_ = OnObject(p, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p Place) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", p, p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToPlace
|
||||
func ToPlace(it Item) (*Place, error) {
|
||||
switch i := it.(type) {
|
||||
case *Place:
|
||||
return i, nil
|
||||
case Place:
|
||||
return &i, nil
|
||||
default:
|
||||
return reflectItemToType[Place](it)
|
||||
}
|
||||
}
|
||||
|
||||
type withPlaceFn func(*Place) error
|
||||
|
||||
// OnPlace calls function fn on it Item if it can be asserted to type *Place
|
||||
//
|
||||
// This function should be called if trying to access the Place specific properties
|
||||
// like "accuracy", "altitude", "latitude", "longitude", "radius", or "units".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnPlace(it Item, fn withPlaceFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnPlace(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToPlace(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
104
place_test.go
Normal file
104
place_test.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlace_Recipients(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToPlace(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestPlace_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func assertPlaceWithTesting(fn canErrorFunc, expected *Place) withPlaceFn {
|
||||
return func(p *Place) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnPlace(t *testing.T) {
|
||||
testPlace := Place{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, *Place) withPlaceFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testPlace, assertPlaceWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{Place{ID: "https://not-equals"}, assertPlaceWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collectionOfPlaces",
|
||||
args: args{ItemCollection{testPlace, testPlace}, assertPlaceWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collectionOfPlaces fails",
|
||||
args: args{ItemCollection{testPlace, Place{ID: "https://not-equals"}}, assertPlaceWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := OnPlace(tt.args.it, tt.args.fn(logFn, &testPlace)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnPlace() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
275
profile.go
Normal file
275
profile.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Profile a Profile is a content object that describes another Object,
|
||||
// typically used to describe CanReceiveActivities Type objects.
|
||||
// The describes property is used to reference the object being described by the profile.
|
||||
type Profile struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Describes On a Profile object, the describes property identifies the object described by the Profile.
|
||||
Describes Item `jsonld:"describes,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Profile objects
|
||||
func (p Profile) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Profile objects
|
||||
func (p Profile) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Profile objects
|
||||
func (p Profile) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Profile object
|
||||
func (p Profile) GetLink() IRI {
|
||||
return IRI(p.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Profile
|
||||
func (p Profile) GetType() ActivityVocabularyType {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Profile
|
||||
func (p Profile) GetID() ID {
|
||||
return p.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (p *Profile) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadProfile(val, p)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (p Profile) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(p, func(o *Object) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if p.Describes != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "describes", p.Describes) || notEmpty
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (p *Profile) UnmarshalBinary(data []byte) error {
|
||||
return p.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (p Profile) MarshalBinary() ([]byte, error) {
|
||||
return p.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (p Profile) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapProfileProperties(mm, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (p *Profile) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapProfileProperties(mm, p)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Profile object's To, Bto, CC and BCC properties
|
||||
func (p *Profile) Recipients() ItemCollection {
|
||||
aud := p.Audience
|
||||
return ItemCollectionDeduplication(&p.To, &p.CC, &p.Bto, &p.BCC, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (p *Profile) Clean() {
|
||||
_ = OnObject(p, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p Profile) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", p, p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToProfile tries to convert the "it" Item to a Profile object
|
||||
func ToProfile(it Item) (*Profile, error) {
|
||||
switch i := it.(type) {
|
||||
case *Profile:
|
||||
return i, nil
|
||||
case Profile:
|
||||
return &i, nil
|
||||
default:
|
||||
return reflectItemToType[Profile](it)
|
||||
}
|
||||
}
|
||||
|
||||
type withProfileFn func(*Profile) error
|
||||
|
||||
// OnProfile calls function fn on it Item if it can be asserted to type *Profile
|
||||
//
|
||||
// This function should be called if trying to access the Profile specific properties
|
||||
// like "describes".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnProfile(it Item, fn withProfileFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnProfile(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToProfile(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
104
profile_test.go
Normal file
104
profile_test.go
Normal file
|
@ -0,0 +1,104 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProfile_Recipients(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestToProfile(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestProfile_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func assertProfileWithTesting(fn canErrorFunc, expected *Profile) withProfileFn {
|
||||
return func(p *Profile) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnProfile(t *testing.T) {
|
||||
testProfile := Profile{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, *Profile) withProfileFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testProfile, assertProfileWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{&Profile{ID: "https://not-equal"}, assertProfileWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collection of profiles",
|
||||
args: args{ItemCollection{testProfile, testProfile}, assertProfileWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collection of profiles fails",
|
||||
args: args{ItemCollection{testProfile, &Profile{ID: "not-equal"}}, assertProfileWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
if err := OnProfile(tt.args.it, tt.args.fn(logFn, &testProfile)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnProfile() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
269
question.go
Normal file
269
question.go
Normal file
|
@ -0,0 +1,269 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Question represents a question being asked. Question objects are an extension of IntransitiveActivity.
|
||||
// That is, the Question object is an Activity, but the direct object is the question
|
||||
// itself and therefore it would not contain an object property.
|
||||
// Either of the anyOf and oneOf properties may be used to express possible answers,
|
||||
// but a Question object must not have both properties.
|
||||
type Question struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
|
||||
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
|
||||
Actor CanReceiveActivities `jsonld:"actor,omitempty"`
|
||||
// Target describes the indirect object, or target, of the activity.
|
||||
// The precise meaning of the target is largely dependent on the type of action being described
|
||||
// but will often be the object of the English preposition "to".
|
||||
// For instance, in the activity "John added a movie to his wishlist",
|
||||
// the target of the activity is John's wishlist. An activity can have more than one target.
|
||||
Target Item `jsonld:"target,omitempty"`
|
||||
// Result describes the result of the activity. For instance, if a particular action results in the creation
|
||||
// of a new resource, the result property can be used to describe that new resource.
|
||||
Result Item `jsonld:"result,omitempty"`
|
||||
// Origin describes an indirect object of the activity from which the activity is directed.
|
||||
// The precise meaning of the origin is the object of the English preposition "from".
|
||||
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
|
||||
Origin Item `jsonld:"origin,omitempty"`
|
||||
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
|
||||
Instrument Item `jsonld:"instrument,omitempty"`
|
||||
// OneOf identifies an exclusive option for a Question. Use of oneOf implies that the Question
|
||||
// can have only a single answer. To indicate that a Question can have multiple answers, use anyOf.
|
||||
OneOf Item `jsonld:"oneOf,omitempty"`
|
||||
// AnyOf identifies an inclusive option for a Question. Use of anyOf implies that the Question can have multiple answers.
|
||||
// To indicate that a Question can have only one answer, use oneOf.
|
||||
AnyOf Item `jsonld:"anyOf,omitempty"`
|
||||
// Closed indicates that a question has been closed, and answers are no longer accepted.
|
||||
Closed bool `jsonld:"closed,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the Question object
|
||||
func (q Question) GetID() ID {
|
||||
return q.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the Question object
|
||||
func (q Question) GetLink() IRI {
|
||||
return IRI(q.ID)
|
||||
}
|
||||
|
||||
// GetType returns the ActivityVocabulary type of the current Activity
|
||||
func (q Question) GetType() ActivityVocabularyType {
|
||||
return q.Type
|
||||
}
|
||||
|
||||
// IsObject returns true for Question objects
|
||||
func (q Question) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsLink returns false for Question objects
|
||||
func (q Question) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCollection returns false for Question objects
|
||||
func (q Question) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (q *Question) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadQuestion(val, q)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (q Question) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if !JSONWriteQuestionValue(&b, q) {
|
||||
return nil, nil
|
||||
}
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (q *Question) UnmarshalBinary(data []byte) error {
|
||||
return q.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (q Question) MarshalBinary() ([]byte, error) {
|
||||
return q.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (q Question) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapQuestionProperties(mm, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (q *Question) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapQuestionProperties(mm, q)
|
||||
}
|
||||
|
||||
func (q Question) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", q, q.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// QuestionNew initializes a Question activity
|
||||
func QuestionNew(id ID) *Question {
|
||||
q := Question{ID: id, Type: QuestionType}
|
||||
q.Name = NaturalLanguageValuesNew()
|
||||
q.Content = NaturalLanguageValuesNew()
|
||||
return &q
|
||||
}
|
||||
|
||||
// ToQuestion tries to convert the it Item to a Question object.
|
||||
func ToQuestion(it Item) (*Question, error) {
|
||||
switch i := it.(type) {
|
||||
case *Question:
|
||||
return i, nil
|
||||
case Question:
|
||||
return &i, nil
|
||||
default:
|
||||
return reflectItemToType[Question](it)
|
||||
}
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Question's To, Bto, CC and BCC properties
|
||||
func (q *Question) Recipients() ItemCollection {
|
||||
aud := q.Audience
|
||||
return ItemCollectionDeduplication(&q.To, &q.CC, &q.Bto, &q.BCC, &ItemCollection{q.Actor}, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (q *Question) Clean() {
|
||||
_ = OnObject(q, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
89
question_test.go
Normal file
89
question_test.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package activitypub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestQuestionNew(t *testing.T) {
|
||||
testValue := ID("test")
|
||||
|
||||
a := QuestionNew(testValue)
|
||||
|
||||
if a.ID != testValue {
|
||||
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
|
||||
}
|
||||
if a.Type != QuestionType {
|
||||
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, QuestionType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_GetID(t *testing.T) {
|
||||
a := QuestionNew("test")
|
||||
|
||||
if a.GetID() != "test" {
|
||||
t.Errorf("%T should return an empty %T object. Received %#v", a, a.GetID(), a.GetID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_IsObject(t *testing.T) {
|
||||
a := QuestionNew("test")
|
||||
|
||||
if !a.IsObject() {
|
||||
t.Errorf("%T should respond true to IsObject", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_IsLink(t *testing.T) {
|
||||
a := QuestionNew("test")
|
||||
|
||||
if a.IsLink() {
|
||||
t.Errorf("%T should respond false to IsLink", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_GetLink(t *testing.T) {
|
||||
a := QuestionNew("test")
|
||||
|
||||
if a.GetLink() != "test" {
|
||||
t.Errorf("GetLink should return \"test\" for %T, received %q", a, a.GetLink())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_GetType(t *testing.T) {
|
||||
a := QuestionNew("test")
|
||||
|
||||
if a.GetType() != QuestionType {
|
||||
t.Errorf("GetType should return %q for %T, received %q", QuestionType, a, a.GetType())
|
||||
}
|
||||
}
|
||||
|
||||
func TestToQuestion(t *testing.T) {
|
||||
var it Item
|
||||
act := QuestionNew("test")
|
||||
it = act
|
||||
|
||||
a, err := ToQuestion(it)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if a != act {
|
||||
t.Errorf("Invalid activity returned by ToActivity #%v", a)
|
||||
}
|
||||
|
||||
ob := ObjectNew(ArticleType)
|
||||
it = ob
|
||||
|
||||
o, err := ToQuestion(it)
|
||||
if err == nil {
|
||||
t.Errorf("Error returned when calling ToActivity with object should not be nil")
|
||||
}
|
||||
if o != nil {
|
||||
t.Errorf("Invalid return by ToActivity #%v, should have been nil", o)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuestion_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestQuestion_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
285
relationship.go
Normal file
285
relationship.go
Normal file
|
@ -0,0 +1,285 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Relationship describes a relationship between two individuals.
|
||||
// The subject and object properties are used to identify the connected individuals.
|
||||
// See 5.2 Representing Relationships Between Entities for additional information.
|
||||
//
|
||||
// 5.2: The relationship property specifies the kind of relationship that exists between the two individuals identified
|
||||
// by the subject and object properties. Used together, these three properties form what is commonly known
|
||||
// as a "reified statement" where subject identifies the subject, relationship identifies the predicate,
|
||||
// and object identifies the object.
|
||||
type Relationship struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Subject property identifies one of the connected individuals.
|
||||
// For instance, for a Relationship object describing "John is related to Sally", subject would refer to John.
|
||||
Subject Item `jsonld:"subject,omitempty"`
|
||||
// Object property identifies one of the connected individuals.
|
||||
// For instance, for a Relationship object describing "John is related to Sally", object would refer to Sally.
|
||||
Object Item `jsonld:"object,omitempty"`
|
||||
// Relationship property identifies the kind of relationship that exists between subject and object.
|
||||
Relationship Item `jsonld:"relationship,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Relationship objects
|
||||
func (r Relationship) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Relationship objects
|
||||
func (r Relationship) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Relationship objects
|
||||
func (r Relationship) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Relationship object
|
||||
func (r Relationship) GetLink() IRI {
|
||||
return IRI(r.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Relationship
|
||||
func (r Relationship) GetType() ActivityVocabularyType {
|
||||
return r.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Relationship
|
||||
func (r Relationship) GetID() ID {
|
||||
return r.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (r *Relationship) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadRelationship(val, r)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (r Relationship) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(r, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
|
||||
if r.Subject != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "subject", r.Subject) || notEmpty
|
||||
}
|
||||
if r.Object != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "object", r.Object) || notEmpty
|
||||
}
|
||||
if r.Relationship != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "relationship", r.Relationship) || notEmpty
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (r *Relationship) UnmarshalBinary(data []byte) error {
|
||||
return r.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (r Relationship) MarshalBinary() ([]byte, error) {
|
||||
return r.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (r Relationship) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapRelationshipProperties(mm, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (r *Relationship) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapRelationshipProperties(mm, r)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Relationship object's To, Bto, CC and BCC properties
|
||||
func (r *Relationship) Recipients() ItemCollection {
|
||||
aud := r.Audience
|
||||
return ItemCollectionDeduplication(&r.To, &r.CC, &r.Bto, &r.BCC, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (r *Relationship) Clean() {
|
||||
_ = OnObject(r, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r Relationship) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", r, r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToRelationship tries to convert the "it" Item to a Relationship object.
|
||||
func ToRelationship(it Item) (*Relationship, error) {
|
||||
switch i := it.(type) {
|
||||
case *Relationship:
|
||||
return i, nil
|
||||
case Relationship:
|
||||
return &i, nil
|
||||
case *Object:
|
||||
return (*Relationship)(unsafe.Pointer(i)), nil
|
||||
case Object:
|
||||
return (*Relationship)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[Relationship](it)
|
||||
}
|
||||
}
|
||||
|
||||
type withRelationshipFn func(*Relationship) error
|
||||
|
||||
// OnRelationship calls function fn on it Item if it can be asserted to type *Relationship
|
||||
//
|
||||
// This function should be called if trying to access the Relationship specific properties
|
||||
// like "subject", "object", or "relationship".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnRelationship(it Item, fn withRelationshipFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToRelationship(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
35
relationship_test.go
Normal file
35
relationship_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package activitypub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRelationship_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestRelationship_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
67
tests/integration_test.go
Normal file
67
tests/integration_test.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
j "github.com/go-ap/jsonld"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func TestAcceptSerialization(t *testing.T) {
|
||||
obj := pub.AcceptNew("https://localhost/myactivity", nil)
|
||||
obj.Name = make(pub.NaturalLanguageValues, 1)
|
||||
obj.Name.Set("en", pub.Content("test"))
|
||||
obj.Name.Set("fr", pub.Content("teste"))
|
||||
|
||||
uri := "https://www.w3.org/ns/activitystreams"
|
||||
p := j.WithContext(j.IRI(uri))
|
||||
|
||||
data, err := p.Marshal(obj)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), uri) {
|
||||
t.Errorf("Could not find context url %#v in output %s", p.Context, data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.ID)) {
|
||||
t.Errorf("Could not find id %#v in output %s", string(obj.ID), data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.Name.Get("en"))) {
|
||||
t.Errorf("Could not find name %#v in output %s", string(obj.Name.Get("en")), data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.Name.Get("fr"))) {
|
||||
t.Errorf("Could not find name %#v in output %s", string(obj.Name.Get("fr")), data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.Type)) {
|
||||
t.Errorf("Could not find activity type %#v in output %s", obj.Type, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateActivityHTTPSerialization(t *testing.T) {
|
||||
id := pub.ID("test_object")
|
||||
obj := pub.AcceptNew(id, nil)
|
||||
obj.Name.Set("en", pub.Content("Accept New"))
|
||||
|
||||
uri := string(pub.ActivityBaseURI)
|
||||
|
||||
data, err := j.WithContext(j.IRI(uri)).Marshal(obj)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(data), uri) {
|
||||
t.Errorf("Could not find context url %#v in output %s", j.GetContext(), data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.ID)) {
|
||||
t.Errorf("Could not find id %#v in output %s", string(obj.ID), data)
|
||||
}
|
||||
if !strings.Contains(string(data), obj.Name.Get("en").String()) {
|
||||
t.Errorf("Could not find name %s in output %s", obj.Name.Get("en"), data)
|
||||
}
|
||||
if !strings.Contains(string(data), string(obj.Type)) {
|
||||
t.Errorf("Could not find activity type %#v in output %s", obj.Type, data)
|
||||
}
|
||||
}
|
22
tests/mocks/activity_create_multiple_objects.json
Normal file
22
tests/mocks/activity_create_multiple_objects.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"type": "Create",
|
||||
"actor": "https://littr.git/api/accounts/anonymous",
|
||||
"object": [
|
||||
{
|
||||
"type": "Note",
|
||||
"attributedTo": "https://littr.git/api/accounts/anonymous",
|
||||
"inReplyTo": "https://littr.git/api/accounts/system/outbox/7ca154ff",
|
||||
"content": "<p>Hello world</p>",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public"
|
||||
},
|
||||
{
|
||||
"type": "Article",
|
||||
"id": "http://www.test.example/article/1",
|
||||
"name": "This someday will grow up to be an article",
|
||||
"inReplyTo": [
|
||||
"http://www.test.example/object/1",
|
||||
"http://www.test.example/object/778"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
12
tests/mocks/activity_create_simple.json
Normal file
12
tests/mocks/activity_create_simple.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"type": "Create",
|
||||
"actor": "https://littr.git/api/accounts/anonymous",
|
||||
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"attributedTo": "https://littr.git/api/accounts/anonymous",
|
||||
"inReplyTo": "https://littr.git/api/accounts/system/outbox/7ca154ff",
|
||||
"content": "<p>Hello world</p>",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public"
|
||||
}
|
||||
}
|
13
tests/mocks/activity_simple.json
Normal file
13
tests/mocks/activity_simple.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Activity",
|
||||
"summary": "Sally did something to a note",
|
||||
"actor": {
|
||||
"type": "Person",
|
||||
"name": "Sally"
|
||||
},
|
||||
"object": {
|
||||
"type": "Note",
|
||||
"name": "A Note"
|
||||
}
|
||||
}
|
10
tests/mocks/article_with_multiple_inreplyto.json
Normal file
10
tests/mocks/article_with_multiple_inreplyto.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Article",
|
||||
"id": "http://www.test.example/article/1",
|
||||
"name": "This someday will grow up to be an article",
|
||||
"inReplyTo": [
|
||||
"http://www.test.example/object/1",
|
||||
"http://www.test.example/object/778"
|
||||
]
|
||||
}
|
1
tests/mocks/empty.json
Normal file
1
tests/mocks/empty.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
11
tests/mocks/like_activity_with_iri_actor.json
Normal file
11
tests/mocks/like_activity_with_iri_actor.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"type": "Like",
|
||||
"actor": "https://littr.git/api/accounts/24d4b96f",
|
||||
"object": {
|
||||
"id": "https://littr.git/api/accounts/ana/liked/7ca154ff",
|
||||
"type": "Article"
|
||||
},
|
||||
"published": "2018-09-06T15:15:09Z",
|
||||
"to": null,
|
||||
"cc": null
|
||||
}
|
8
tests/mocks/link_simple.json
Normal file
8
tests/mocks/link_simple.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Link",
|
||||
"href": "http://example.org/abc",
|
||||
"hrefLang": "en",
|
||||
"mediaType": "text/html",
|
||||
"name": "An example link"
|
||||
}
|
6
tests/mocks/natural_language_values.json
Normal file
6
tests/mocks/natural_language_values.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"-": "\n\t\t\n",
|
||||
"en": "Ana got apples ⓐ",
|
||||
"fr": "Aná a des pommes ⒜",
|
||||
"ro": "Ana are mere"
|
||||
}
|
5
tests/mocks/object_no_type.json
Normal file
5
tests/mocks/object_no_type.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://www.test.example/object/1",
|
||||
"name": "A Simple, non-specific object without a type"
|
||||
}
|
6
tests/mocks/object_simple.json
Normal file
6
tests/mocks/object_simple.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Object",
|
||||
"id": "http://www.test.example/object/1",
|
||||
"name": "A Simple, non-specific object"
|
||||
}
|
18
tests/mocks/object_with_audience.json
Normal file
18
tests/mocks/object_with_audience.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Object",
|
||||
"id": "http://www.test.example/object/1",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"bto": [
|
||||
"http://example.com/sharedInbox"
|
||||
],
|
||||
"cc": [
|
||||
"https://example.com/actors/ana",
|
||||
"https://example.com/actors/bob"
|
||||
],
|
||||
"bcc": [
|
||||
"https://darkside.cookie/actors/darthvader"
|
||||
]
|
||||
}
|
17
tests/mocks/object_with_replies.json
Normal file
17
tests/mocks/object_with_replies.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Object",
|
||||
"id": "http://www.test.example/object/1",
|
||||
"replies": {
|
||||
"id": "http://www.test.example/object/1/replies",
|
||||
"type": "Collection",
|
||||
"totalItems": 1,
|
||||
"items": [
|
||||
{
|
||||
"id": "http://www.test.example/object/1/replies/2",
|
||||
"type": "Article",
|
||||
"name": "Example title"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
18
tests/mocks/object_with_tags.json
Normal file
18
tests/mocks/object_with_tags.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Object",
|
||||
"id": "http://www.test.example/object/1",
|
||||
"name": "A Simple, non-specific object",
|
||||
"tag": [
|
||||
{
|
||||
"name": "#my_tag",
|
||||
"id": "http://example.com/tag/my_tag",
|
||||
"type": "Mention"
|
||||
},
|
||||
{
|
||||
"name": "@ana",
|
||||
"type": "Mention",
|
||||
"id": "http://example.com/users/ana"
|
||||
}
|
||||
]
|
||||
}
|
4
tests/mocks/object_with_url.json
Normal file
4
tests/mocks/object_with_url.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"@context":"https://www.w3.org/ns/activitystreams",
|
||||
"url":"http://littr.git/api/accounts/system"
|
||||
}
|
7
tests/mocks/object_with_url_collection.json
Normal file
7
tests/mocks/object_with_url_collection.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"@context":"https://www.w3.org/ns/activitystreams",
|
||||
"url": [
|
||||
"http://littr.git/api/accounts/system",
|
||||
"http://littr.git/~system"
|
||||
]
|
||||
}
|
20
tests/mocks/ordered_collection.json
Normal file
20
tests/mocks/ordered_collection.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://example.com/outbox",
|
||||
"type": "OrderedCollection",
|
||||
"url": "http://example.com/outbox",
|
||||
"totalItems": 1,
|
||||
"orderedItems": [
|
||||
{
|
||||
"id": "http://example.com/outbox/53c6fb47",
|
||||
"type": "Article",
|
||||
"name": "Example title",
|
||||
"content": "Example content!",
|
||||
"url": "http://example.com/53c6fb47",
|
||||
"mediaType": "text/markdown",
|
||||
"published": "2018-07-05T16:46:44.00000Z",
|
||||
"generator": "http://example.com",
|
||||
"attributedTo": "http://example.com/accounts/alice"
|
||||
}
|
||||
]
|
||||
}
|
25
tests/mocks/ordered_collection_page.json
Normal file
25
tests/mocks/ordered_collection_page.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://example.com/outbox?page=2",
|
||||
"type": "OrderedCollectionPage",
|
||||
"url": "http://example.com/outbox?page=2",
|
||||
"totalItems": 1,
|
||||
"partOf": "http://example.com/outbox",
|
||||
"current": "http://example.com/outbox?page=2",
|
||||
"next": "http://example.com/outbox?page=3",
|
||||
"prev" : "http://example.com/outbox?page=1",
|
||||
"startIndex": 100,
|
||||
"orderedItems": [
|
||||
{
|
||||
"id": "http://example.com/outbox/53c6fb47",
|
||||
"type": "Article",
|
||||
"name": "Example title",
|
||||
"content": "Example content!",
|
||||
"url": "http://example.com/53c6fb47",
|
||||
"mediaType": "text/markdown",
|
||||
"published": "2018-07-05T16:46:44.00000Z",
|
||||
"generator": "http://example.com",
|
||||
"attributedTo": "http://example.com/accounts/alice"
|
||||
}
|
||||
]
|
||||
}
|
13
tests/mocks/person_with_outbox.json
Normal file
13
tests/mocks/person_with_outbox.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "http://example.com/accounts/ana",
|
||||
"type": "Person",
|
||||
"name": "ana",
|
||||
"url": "http://example.com/accounts/ana",
|
||||
"outbox": {
|
||||
"id": "http://example.com/accounts/ana/outbox",
|
||||
"type": "OrderedCollection",
|
||||
"url": "http://example.com/outbox"
|
||||
},
|
||||
"preferredUsername": "Ana"
|
||||
}
|
13
tests/mocks/travel_simple.json
Normal file
13
tests/mocks/travel_simple.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"type": "Travel",
|
||||
"summary": "Sally went to work",
|
||||
"actor": {
|
||||
"type": "Person",
|
||||
"name": "Sally"
|
||||
},
|
||||
"target": {
|
||||
"type": "Place",
|
||||
"name": "Work"
|
||||
}
|
||||
}
|
54
tests/server_common_test.go
Normal file
54
tests/server_common_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
package tests
|
||||
|
||||
// Common server tests...
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Server: Fetching the inbox
|
||||
// Try retrieving the actor's inbox of an actor.
|
||||
// Server responds to GET request at inbox URL
|
||||
func TestInboxGETRequest(t *testing.T) {
|
||||
desc := `
|
||||
Server: Fetching the inbox
|
||||
Try retrieving the actor's inbox of an actor.
|
||||
|
||||
Server responds to GET request at inbox URL
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// Server: Fetching the inbox
|
||||
// Try retrieving the actor's inbox of an actor.
|
||||
// inbox is an OrderedCollection
|
||||
func TestInboxIsOrderedCollection(t *testing.T) {
|
||||
desc := `
|
||||
Server: Fetching the inbox
|
||||
Try retrieving the actor's inbox of an actor.
|
||||
|
||||
inbox is an OrderedCollection
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// Server: Fetching the inbox
|
||||
// Try retrieving the actor's inbox of an actor.
|
||||
// Server filters inbox content according to the requester's permission
|
||||
func TestInboxFilteringBasedOnPermissions(t *testing.T) {
|
||||
desc := `
|
||||
Server: Fetching the inbox
|
||||
Try retrieving the actor's inbox of an actor.
|
||||
|
||||
Server filters inbox content according to the requester's permission
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
/*
|
||||
func Test_(t *testing.T) {
|
||||
desc := `
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
*/
|
569
tests/server_to_server_test.go
Normal file
569
tests/server_to_server_test.go
Normal file
|
@ -0,0 +1,569 @@
|
|||
package tests
|
||||
|
||||
// Server to Server tests from: https://test.activitypub.rocks/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// S2S Server: Activities requiring the object property
|
||||
// The distribution of the following activities require that they contain the object property:
|
||||
// Create, Update, Delete, Follow, Add, Remove, Like, Block, Undo.
|
||||
// Implementation always includes object property for each of the above supported activities
|
||||
func TestObjectPropertyExists(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activities requiring the object property
|
||||
The distribution of the following activities require that they contain the object property:
|
||||
Create, Update, Delete, Follow, Add, Remove, Like, Block, Undo.
|
||||
|
||||
Implementation always includes object property for each of the above supported activities
|
||||
`
|
||||
t.Log(desc)
|
||||
|
||||
obj := pub.MentionNew("gigel")
|
||||
|
||||
add := pub.AddNew("https://localhost/myactivity", obj, nil)
|
||||
if pub.IsNil(add.Object) {
|
||||
t.Errorf("Missing GetID in Add activity %#v", add.Object)
|
||||
}
|
||||
if add.Object != obj {
|
||||
t.Errorf("Add.GetID different than what we initialized %#v %#v", add.Object, obj)
|
||||
}
|
||||
|
||||
block := pub.BlockNew("https://localhost/myactivity", obj)
|
||||
if pub.IsNil(block.Object) {
|
||||
t.Errorf("Missing GetID in Add activity %#v", block.Object)
|
||||
}
|
||||
if block.Object != obj {
|
||||
t.Errorf("Block.GetID different than what we initialized %#v %#v", block.Object, obj)
|
||||
}
|
||||
|
||||
create := pub.CreateNew("https://localhost/myactivity", obj)
|
||||
if create.Object == nil {
|
||||
t.Errorf("Missing GetID in Add activity %#v", create.Object)
|
||||
}
|
||||
if create.Object != obj {
|
||||
t.Errorf("Create.GetID different than what we initialized %#v %#v", create.Object, obj)
|
||||
}
|
||||
|
||||
delete := pub.DeleteNew("https://localhost/myactivity", obj)
|
||||
if pub.IsNil(delete.Object) {
|
||||
t.Errorf("Missing GetID in Delete activity %#v", delete.Object)
|
||||
}
|
||||
if delete.Object != obj {
|
||||
t.Errorf("Delete.GetID different than what we initialized %#v %#v", delete.Object, obj)
|
||||
}
|
||||
|
||||
follow := pub.FollowNew("https://localhost/myactivity", obj)
|
||||
if pub.IsNil(follow.Object) {
|
||||
t.Errorf("Missing GetID in Follow activity %#v", follow.Object)
|
||||
}
|
||||
if follow.Object != obj {
|
||||
t.Errorf("Follow.GetID different than what we initialized %#v %#v", follow.Object, obj)
|
||||
}
|
||||
|
||||
like := pub.LikeNew("https://localhost/myactivity", obj)
|
||||
if pub.IsNil(like.Object) {
|
||||
t.Errorf("Missing GetID in Like activity %#v", like.Object)
|
||||
}
|
||||
if like.Object != obj {
|
||||
t.Errorf("Like.GetID different than what we initialized %#v %#v", add.Object, obj)
|
||||
}
|
||||
|
||||
update := pub.UpdateNew("https://localhost/myactivity", obj)
|
||||
if pub.IsNil(update.Object) {
|
||||
t.Errorf("Missing GetID in Update activity %#v", update.Object)
|
||||
}
|
||||
if update.Object != obj {
|
||||
t.Errorf("Update.GetID different than what we initialized %#v %#v", update.Object, obj)
|
||||
}
|
||||
|
||||
undo := pub.UndoNew("https://localhost/myactivity", obj)
|
||||
if undo.Object == nil {
|
||||
t.Errorf("Missing GetID in Undo activity %#v", undo.Object)
|
||||
}
|
||||
if undo.Object != obj {
|
||||
t.Errorf("Undo.GetID different than what we initialized %#v %#v", undo.Object, obj)
|
||||
}
|
||||
}
|
||||
|
||||
// S2S Server: Activities requiring the target property
|
||||
// The distribution of the following activities require that they contain the target property:
|
||||
// Add, Remove.
|
||||
// Implementation always includes target property for each of the above supported activities.
|
||||
func TestTargetPropertyExists(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activities requiring the target property
|
||||
The distribution of the following activities require that they contain the target
|
||||
property: Add, Remove.
|
||||
|
||||
Implementation always includes target property for each of the above supported activities.
|
||||
`
|
||||
t.Log(desc)
|
||||
|
||||
obj := pub.MentionNew("foo")
|
||||
target := pub.MentionNew("bar")
|
||||
|
||||
add := pub.AddNew("https://localhost/myactivity", obj, target)
|
||||
if pub.IsNil(add.Target) {
|
||||
t.Errorf("Missing Target in Add activity %#v", add.Target)
|
||||
}
|
||||
if add.Target != target {
|
||||
t.Errorf("Add.Target different than what we initialized %#v %#v", add.Target, target)
|
||||
}
|
||||
|
||||
remove := pub.RemoveNew("https://localhost/myactivity", obj, target)
|
||||
if pub.IsNil(remove.Target) {
|
||||
t.Errorf("Missing Target in Remove activity %#v", remove.Target)
|
||||
}
|
||||
if remove.Target != target {
|
||||
t.Errorf("Remove.Target different than what we initialized %#v %#v", remove.Target, target)
|
||||
}
|
||||
}
|
||||
|
||||
// S2S Server: Deduplication of recipient list
|
||||
// Attempt to submit for delivery an activity that addresses the same actor
|
||||
// (ie an actor with the same id) twice.
|
||||
// (For example, the same actor could appear on both the to and cc fields,
|
||||
// or the actor could be explicitly addressed
|
||||
// in to but could also be a member of the addressed followers collection of the sending actor.)
|
||||
// The server should deduplicate the list of inboxes to deliver to before delivering.
|
||||
// The final recipient list is deduplicated before delivery.
|
||||
func TestDeduplication(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Deduplication of recipient list
|
||||
Attempt to submit for delivery an activity that addresses the same actor
|
||||
(ie an actor with the same id) twice.
|
||||
|
||||
The final recipient list is deduplicated before delivery.
|
||||
`
|
||||
t.Log(desc)
|
||||
|
||||
to := pub.PersonNew("bob")
|
||||
o := pub.ObjectNew(pub.ArticleType)
|
||||
cc := pub.PersonNew("alice")
|
||||
|
||||
o.ID = "something"
|
||||
c := pub.CreateNew("create", o)
|
||||
c.To.Append(to)
|
||||
c.CC.Append(cc)
|
||||
c.BCC.Append(cc)
|
||||
|
||||
c.Recipients()
|
||||
|
||||
checkDedup := func(list pub.ItemCollection, recIds *[]pub.ID) error {
|
||||
for _, rec := range list {
|
||||
for _, id := range *recIds {
|
||||
if rec.GetID() == id {
|
||||
return fmt.Errorf("%T[%s] already stored in recipients list, Deduplication faild", rec, id)
|
||||
}
|
||||
}
|
||||
*recIds = append(*recIds, rec.GetID())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
recIds := make([]pub.ID, 0)
|
||||
err = checkDedup(c.To, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.Bto, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.CC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkDedup(c.BCC, &recIds)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// S2S Server: Do-not-deliver considerations
|
||||
// Server does not deliver to recipients which are the same as the actor of the
|
||||
// Activity being notified about
|
||||
func TestDoNotDeliverToActor(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Do-not-deliver considerations
|
||||
|
||||
Server does not deliver to recipients which are the same as the actor of the
|
||||
Activity being notified about
|
||||
`
|
||||
t.Log(desc)
|
||||
|
||||
p := pub.PersonNew("main actor")
|
||||
|
||||
to := pub.PersonNew("bob")
|
||||
o := pub.ObjectNew(pub.ArticleType)
|
||||
cc := pub.PersonNew("alice")
|
||||
|
||||
o.ID = "something"
|
||||
c := pub.CreateNew("create", o)
|
||||
c.Actor = *p
|
||||
|
||||
c.To.Append(p)
|
||||
c.To.Append(to)
|
||||
c.CC.Append(cc)
|
||||
c.CC.Append(p)
|
||||
c.BCC.Append(cc)
|
||||
c.BCC.Append(p)
|
||||
|
||||
c.Recipients()
|
||||
|
||||
checkActor := func(list pub.ItemCollection, actor pub.Item) error {
|
||||
for _, rec := range list {
|
||||
if rec.GetID() == actor.GetID() {
|
||||
return fmt.Errorf("%T[%s] Actor of activity should not be in the recipients list", rec, actor.GetID())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
err = checkActor(c.To, c.Actor)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkActor(c.Bto, c.Actor)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkActor(c.CC, c.Actor)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkActor(c.BCC, c.Actor)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// S2S Server: Do-not-deliver considerations
|
||||
// Server does not deliver Block activities to their object.
|
||||
func TestDoNotDeliverBlockToObject(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Do-not-deliver considerations
|
||||
|
||||
Server does not deliver Block activities to their object.
|
||||
`
|
||||
t.Log(desc)
|
||||
|
||||
p := pub.PersonNew("blocked")
|
||||
|
||||
bob := pub.PersonNew("bob")
|
||||
jane := pub.PersonNew("jane doe")
|
||||
|
||||
b := pub.BlockNew("block actor", p)
|
||||
b.Actor = *bob
|
||||
|
||||
b.To.Append(jane)
|
||||
b.To.Append(p)
|
||||
b.To.Append(bob)
|
||||
|
||||
b.Recipients()
|
||||
|
||||
checkActor := func(list pub.ItemCollection, ob pub.Item) error {
|
||||
for _, rec := range list {
|
||||
if rec.GetID() == ob.GetID() {
|
||||
return fmt.Errorf("%T[%s] of activity should not be in the recipients list", rec, ob.GetID())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
err = checkActor(b.To, b.Object)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = checkActor(b.To, b.Actor)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// S2S Sever: Support for sharedInbox
|
||||
// Delivers to sharedInbox endpoints to reduce the number of receiving actors delivered
|
||||
// to by identifying all followers which share the same sharedInbox who would otherwise be
|
||||
// individual recipients and instead deliver objects to said sharedInbox.
|
||||
func TestSharedInboxIdentifySharedInbox(t *testing.T) {
|
||||
desc := `
|
||||
S2S Sever: Support for sharedInbox
|
||||
|
||||
Delivers to sharedInbox endpoints to reduce the number of receiving actors delivered
|
||||
to by identifying all followers which share the same sharedInbox who would otherwise be
|
||||
individual recipients and instead deliver objects to said sharedInbox.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Sever: Support for sharedInbox
|
||||
// Deliver to actor inboxes and collections otherwise addressed which do not have a sharedInbox.
|
||||
func TestSharedInboxActorsWOSharedInbox(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Do-not-deliver considerations
|
||||
|
||||
Server does not deliver Block activities to their object.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Deduplicating received activities
|
||||
// Server deduplicates activities received in inbox by comparing activity ids
|
||||
func TestInboxDeduplication(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Deduplicating received activities
|
||||
|
||||
Server deduplicates activities received in inbox by comparing activity ids
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Special forwarding mechanism
|
||||
// ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
// Forwards incoming activities to the values of to, bto, cc, bcc, audience if and only if criteria are met.
|
||||
func TestForwardingMechanismsToRecipients(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Special forwarding mechanism
|
||||
ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
|
||||
Forwards incoming activities to the values of to, bto, cc, bcc, audience if and only if criteria are met.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Special forwarding mechanism
|
||||
// ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
// Recurse through to, bto, cc, bcc, audience object values to determine whether/where
|
||||
// to forward according to criteria in 7.1.2
|
||||
func TestForwardingMechanismsRecurseRecipients(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Special forwarding mechanism
|
||||
ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
|
||||
Recurse through to, bto, cc, bcc, audience object values to determine whether/where
|
||||
to forward according to criteria in 7.1.2
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Special forwarding mechanism
|
||||
// ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
// Limits depth of this recursion.
|
||||
func TestForwardingMechanismsLimitsRecursion(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Special forwarding mechanism
|
||||
ActivityPub contains a special mechanism for forwarding replies to avoid "ghost replies".
|
||||
|
||||
Limits depth of this recursion.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Verification of content authorship
|
||||
// Before accepting activities delivered to an actor's inbox some sort of verification
|
||||
// should be performed. (For example, if the delivering actor has a public key on their profile,
|
||||
// the request delivering the activity may be signed with HTTP Signatures.)
|
||||
// Don't trust content received from a server other than the content's origin without some form of verification.
|
||||
func TestVerification(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Verification of content authorship
|
||||
Before accepting activities delivered to an actor's inbox some sort of verification
|
||||
should be performed. (For example, if the delivering actor has a public key on their profile,
|
||||
the request delivering the activity may be signed with HTTP Signatures.)
|
||||
|
||||
Don't trust content received from a server other than the content's origin without some form of verification.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Update activity
|
||||
// On receiving an Update activity to an actor's inbox, the server:
|
||||
// Takes care to be sure that the Update is authorized to modify its object
|
||||
func TestUpdateIsAuthorized(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Update activity
|
||||
On receiving an Update activity to an actor's inbox, the server:
|
||||
|
||||
Takes care to be sure that the Update is authorized to modify its object
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Update activity
|
||||
// On receiving an Update activity to an actor's inbox, the server:
|
||||
// Completely replaces its copy of the activity with the newly received value
|
||||
func TestUpdateReplacesActivity(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Update activity
|
||||
On receiving an Update activity to an actor's inbox, the server:
|
||||
|
||||
Completely replaces its copy of the activity with the newly received value
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Delete activity
|
||||
// Delete removes object's representation, assuming object is owned by sending actor/server
|
||||
func TestDeleteRemoves(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Delete activity
|
||||
|
||||
Delete removes object's representation, assuming object is owned by sending actor/server
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Delete activity
|
||||
// Replaces deleted object with a Tombstone object (optional)
|
||||
func TestDeleteReplacesWithTombstone(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Delete activity
|
||||
|
||||
Replaces deleted object with a Tombstone object (optional)
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Following, and handling accept/reject of follows
|
||||
// Follow should add the activity's actor to the receiving actor's Followers Collection.
|
||||
func TestFollowAddsToFollowers(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Following, and handling accept/reject of follows
|
||||
|
||||
Follow should add the activity's actor to the receiving actor's Followers Collection.
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Following, and handling accept/reject of follows
|
||||
// Generates either an Accept or Reject activity with Follow as object and deliver to actor of the Follow
|
||||
func TestGeneratesAcceptOrReject(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Following, and handling accept/reject of follows
|
||||
|
||||
Generates either an Accept or Reject activity with Follow as object and deliver to actor of the Follow
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Following, and handling accept/reject of follows
|
||||
// If receiving an Accept in reply to a Follow activity, adds actor to receiver's Following Collection
|
||||
func TestAddsFollowerIfAccept(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Following, and handling accept/reject of follows
|
||||
|
||||
If receiving an Accept in reply to a Follow activity, adds actor to receiver's Following Collection
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
// S2S Server: Following, and handling accept/reject of follows
|
||||
// If receiving a Reject in reply to a Follow activity, does not add actor to receiver's Following Collection
|
||||
func TestDoesntAddFollowerIfReject(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Following, and handling accept/reject of follows
|
||||
|
||||
If receiving a Reject in reply to a Follow activity, does not add actor to receiver's Following Collection
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Create makes record of the object existing
|
||||
func TestCreateMakesRecord(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Create makes record of the object existing
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Add should add the activity's object to the Collection specified in the target property,
|
||||
// unless not allowed per requirements
|
||||
func TestAddObjectToTarget(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Add should add the activity's object to the Collection specified in the target property,
|
||||
unless not allowed per requirements
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Remove should remove the object from the Collection specified in the target property,
|
||||
// unless not allowed per requirements
|
||||
func TestRemoveObjectFromTarget(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Remove should remove the object from the Collection specified in the target property,
|
||||
unless not allowed per requirements
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Like increments the object's count of likes by adding the received activity to the likes
|
||||
// collection if this collection is present
|
||||
func TestLikeIncrementsLikes(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Like increments the object's count of likes by adding the received activity to the likes
|
||||
collection if this collection is present
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Announce increments object's count of shares by adding the received activity to the
|
||||
// 'shares' collection if this collection is present
|
||||
func TestAnnounceIncrementsShares(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Announce increments object's count of shares by adding the received activity to the
|
||||
'shares' collection if this collection is present
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
||||
|
||||
//S2S Server: Activity acceptance side-effects
|
||||
// Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
//
|
||||
// Undo performs Undo of object in federated context
|
||||
func TestUndoPerformsUndoOnObject(t *testing.T) {
|
||||
desc := `
|
||||
S2S Server: Activity acceptance side-effects
|
||||
Test accepting the following activities to an actor's inbox and observe the side effects:
|
||||
|
||||
Undo performs Undo of object in federated context
|
||||
`
|
||||
t.Skip(desc)
|
||||
}
|
580
tests/unmarshal_test.go
Normal file
580
tests/unmarshal_test.go
Normal file
|
@ -0,0 +1,580 @@
|
|||
package tests
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
|
||||
j "github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
const dir = "./mocks"
|
||||
|
||||
var stopOnFailure = false
|
||||
|
||||
type testPair struct {
|
||||
expected bool
|
||||
blank interface{}
|
||||
result interface{}
|
||||
}
|
||||
|
||||
type testMaps map[string]testPair
|
||||
|
||||
type visit struct {
|
||||
a1 unsafe.Pointer
|
||||
a2 unsafe.Pointer
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
type canErrorFunc func(format string, args ...interface{})
|
||||
|
||||
// See reflect.DeepEqual
|
||||
func assertDeepEquals(t canErrorFunc, x, y interface{}) bool {
|
||||
if x == nil || y == nil {
|
||||
return x == y
|
||||
}
|
||||
v1 := reflect.ValueOf(x)
|
||||
v2 := reflect.ValueOf(y)
|
||||
if v1.Type() != v2.Type() {
|
||||
t("%T != %T", x, y)
|
||||
return false
|
||||
}
|
||||
return deepValueEqual(t, v1, v2, make(map[visit]bool), 0)
|
||||
}
|
||||
|
||||
// See reflect.deepValueEqual
|
||||
func deepValueEqual(t canErrorFunc, v1, v2 reflect.Value, visited map[visit]bool, depth int) bool {
|
||||
if !v1.IsValid() || !v2.IsValid() {
|
||||
return v1.IsValid() == v2.IsValid()
|
||||
}
|
||||
if v1.Type() != v2.Type() {
|
||||
t("types differ %s != %s", v1.Type().Name(), v2.Type().Name())
|
||||
return false
|
||||
}
|
||||
|
||||
hard := func(v1, v2 reflect.Value) bool {
|
||||
switch v1.Kind() {
|
||||
case reflect.Ptr:
|
||||
return false
|
||||
case reflect.Map, reflect.Slice, reflect.Interface:
|
||||
// Nil pointers cannot be cyclic. Avoid putting them in the visited map.
|
||||
return !v1.IsNil() && !v2.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if hard(v1, v2) {
|
||||
var addr1, addr2 unsafe.Pointer
|
||||
if v1.CanAddr() {
|
||||
addr1 = unsafe.Pointer(v1.UnsafeAddr())
|
||||
} else {
|
||||
addr1 = unsafe.Pointer(v1.Pointer())
|
||||
}
|
||||
if v2.CanAddr() {
|
||||
addr2 = unsafe.Pointer(v2.UnsafeAddr())
|
||||
} else {
|
||||
addr2 = unsafe.Pointer(v2.Pointer())
|
||||
}
|
||||
if uintptr(addr1) > uintptr(addr2) {
|
||||
// Canonicalize order to reduce number of entries in visited.
|
||||
// Assumes non-moving garbage collector.
|
||||
addr1, addr2 = addr2, addr1
|
||||
}
|
||||
// Short circuit if references are already seen.
|
||||
typ := v1.Type()
|
||||
v := visit{addr1, addr2, typ}
|
||||
if visited[v] {
|
||||
return true
|
||||
}
|
||||
|
||||
// Remember for later.
|
||||
visited[v] = true
|
||||
}
|
||||
|
||||
switch v1.Kind() {
|
||||
case reflect.Array:
|
||||
for i := 0; i < v1.Len(); i++ {
|
||||
if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) {
|
||||
t("Arrays not equal at index %d %s %s", i, v1.Index(i), v2.Index(i))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Slice:
|
||||
if v1.IsNil() != v2.IsNil() {
|
||||
t("One of the slices is not nil %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Len() != v2.Len() {
|
||||
t("Slices lengths are different %s[%d] vs %s[%d]", v1.Type().Name(), v1.Len(), v2.Type().Name(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < v1.Len(); i++ {
|
||||
if !deepValueEqual(t, v1.Index(i), v2.Index(i), visited, depth+1) {
|
||||
t("Slices elements at pos %d are not equal %#v vs %#v", i, v1.Index(i), v2.Index(i))
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Interface:
|
||||
if v1.IsNil() || v2.IsNil() {
|
||||
if v1.IsNil() == v2.IsNil() {
|
||||
return true
|
||||
}
|
||||
var isNil1, isNil2 string
|
||||
if v1.IsNil() {
|
||||
isNil1 = "is"
|
||||
} else {
|
||||
isNil1 = "is not"
|
||||
}
|
||||
if v2.IsNil() {
|
||||
isNil2 = "is"
|
||||
} else {
|
||||
isNil2 = "is not"
|
||||
}
|
||||
t("Interface '%s' %s nil and '%s' %s nil", v1.Type().Name(), isNil1, v2.Type().Name(), isNil2)
|
||||
return false
|
||||
}
|
||||
return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
case reflect.Ptr:
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
return deepValueEqual(t, v1.Elem(), v2.Elem(), visited, depth+1)
|
||||
case reflect.Struct:
|
||||
for i, n := 0, v1.NumField(); i < n; i++ {
|
||||
var (
|
||||
f1 = v1.Field(i)
|
||||
f2 = v2.Field(i)
|
||||
n1 = v1.Type().Field(i).Name
|
||||
n2 = v2.Type().Field(i).Name
|
||||
t1 = f1.Type().Name()
|
||||
t2 = f2.Type().Name()
|
||||
)
|
||||
if !deepValueEqual(t, v1.Field(i), v2.Field(i), visited, depth+1) {
|
||||
t("Struct fields at pos %d %s[%s] and %s[%s] are not deeply equal", i, n1, t1, n2, t2)
|
||||
if f1.CanInterface() && f2.CanInterface() {
|
||||
t(" Values: %#v - %#v", v1.Field(i).Interface(), v2.Field(i).Interface())
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Map:
|
||||
if v1.IsNil() != v2.IsNil() {
|
||||
t("Maps are not nil", v1.Type().Name(), v2.Type().Name())
|
||||
return false
|
||||
}
|
||||
if v1.Len() != v2.Len() {
|
||||
t("Maps don't have the same length %d vs %d", v1.Len(), v2.Len())
|
||||
return false
|
||||
}
|
||||
if v1.Pointer() == v2.Pointer() {
|
||||
return true
|
||||
}
|
||||
for _, k := range v1.MapKeys() {
|
||||
val1 := v1.MapIndex(k)
|
||||
val2 := v2.MapIndex(k)
|
||||
if !val1.IsValid() || !val2.IsValid() || !deepValueEqual(t, v1.MapIndex(k), v2.MapIndex(k), visited, depth+1) {
|
||||
t("Maps values at index %s are not equal", k.String())
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case reflect.Func:
|
||||
if v1.IsNil() && v2.IsNil() {
|
||||
return true
|
||||
}
|
||||
// Can't do better than this:
|
||||
return false
|
||||
case reflect.String:
|
||||
return v1.String() == v2.String()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v1.Int() == v2.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return v1.Uint() == v2.Uint()
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v1.Float() == v2.Float()
|
||||
case reflect.Bool:
|
||||
return v1.Bool() == v2.Bool()
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
return v1.Complex() == v2.Complex()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var zLoc, _ = time.LoadLocation("UTC")
|
||||
|
||||
var allTests = testMaps{
|
||||
//"empty": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{},
|
||||
//},
|
||||
//"link_simple": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Link{},
|
||||
// result: &pub.Link{
|
||||
// Type: pub.LinkType,
|
||||
// Href: pub.IRI("http://example.org/abc"),
|
||||
// HrefLang: pub.LangRef("en"),
|
||||
// MediaType: pub.MimeType("text/html"),
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("An example link"),
|
||||
// }},
|
||||
// },
|
||||
//},
|
||||
//"object_with_url": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// URL: pub.IRI("http://littr.git/api/accounts/system"),
|
||||
// },
|
||||
//},
|
||||
//"object_with_url_collection": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// URL: pub.ItemCollection{
|
||||
// pub.IRI("http://littr.git/api/accounts/system"),
|
||||
// pub.IRI("http://littr.git/~system"),
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"object_simple": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// Type: pub.ObjectType,
|
||||
// ID: pub.ID("http://www.test.example/object/1"),
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("A Simple, non-specific object"),
|
||||
// }},
|
||||
// },
|
||||
//},
|
||||
//"object_no_type": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// ID: pub.ID("http://www.test.example/object/1"),
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("A Simple, non-specific object without a type"),
|
||||
// }},
|
||||
// },
|
||||
//},
|
||||
//"object_with_tags": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// Type: pub.ObjectType,
|
||||
// ID: pub.ID("http://www.test.example/object/1"),
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("A Simple, non-specific object"),
|
||||
// }},
|
||||
// Tag: pub.ItemCollection{
|
||||
// &pub.Mention{
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("#my_tag"),
|
||||
// }},
|
||||
// Type: pub.MentionType,
|
||||
// ID: pub.ID("http://example.com/tag/my_tag"),
|
||||
// },
|
||||
// &pub.Mention{
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("@ana"),
|
||||
// }},
|
||||
// Type: pub.MentionType,
|
||||
// ID: pub.ID("http://example.com/users/ana"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"object_with_replies": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// Type: pub.ObjectType,
|
||||
// ID: pub.ID("http://www.test.example/object/1"),
|
||||
// Replies: &pub.Collection{
|
||||
// ID: pub.ID("http://www.test.example/object/1/replies"),
|
||||
// Type: pub.CollectionType,
|
||||
// TotalItems: 1,
|
||||
// Items: pub.ItemCollection{
|
||||
// &pub.Object{
|
||||
// ID: pub.ID("http://www.test.example/object/1/replies/2"),
|
||||
// Type: pub.ArticleType,
|
||||
// Name: pub.NaturalLanguageValues{{
|
||||
// pub.NilLangRef, pub.Content("Example title"),
|
||||
// }},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"activity_simple": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Activity{
|
||||
// Actor: &pub.Person{},
|
||||
// },
|
||||
// result: &pub.Activity{
|
||||
// Type: pub.ActivityType,
|
||||
// Summary: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("Sally did something to a note")}},
|
||||
// Actor: &pub.Person{
|
||||
// Type: pub.PersonType,
|
||||
// Name: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("Sally")}},
|
||||
// },
|
||||
// Object: &pub.Object{
|
||||
// Type: pub.NoteType,
|
||||
// Name: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("A Note")}},
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"person_with_outbox": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Person{},
|
||||
// result: &pub.Person{
|
||||
// ID: pub.ID("http://example.com/accounts/ana"),
|
||||
// Type: pub.PersonType,
|
||||
// Name: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("ana")}},
|
||||
// PreferredUsername: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("Ana")}},
|
||||
// URL: pub.IRI("http://example.com/accounts/ana"),
|
||||
// Outbox: &pub.OrderedCollection{
|
||||
// ID: "http://example.com/accounts/ana/outbox",
|
||||
// Type: pub.OrderedCollectionType,
|
||||
// URL: pub.IRI("http://example.com/outbox"),
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"ordered_collection": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.OrderedCollection{},
|
||||
// result: &pub.OrderedCollection{
|
||||
// ID: pub.ID("http://example.com/outbox"),
|
||||
// Type: pub.OrderedCollectionType,
|
||||
// URL: pub.IRI("http://example.com/outbox"),
|
||||
// TotalItems: 1,
|
||||
// OrderedItems: pub.ItemCollection{
|
||||
// &pub.Object{
|
||||
// ID: pub.ID("http://example.com/outbox/53c6fb47"),
|
||||
// Type: pub.ArticleType,
|
||||
// Name: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("Example title")}},
|
||||
// Content: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("Example content!")}},
|
||||
// URL: pub.IRI("http://example.com/53c6fb47"),
|
||||
// MediaType: pub.MimeType("text/markdown"),
|
||||
// Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
|
||||
// Generator: pub.IRI("http://example.com"),
|
||||
// AttributedTo: pub.IRI("http://example.com/accounts/alice"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
"ordered_collection_page": testPair{
|
||||
expected: true,
|
||||
blank: &pub.OrderedCollectionPage{},
|
||||
result: &pub.OrderedCollectionPage{
|
||||
PartOf: pub.IRI("http://example.com/outbox"),
|
||||
Next: pub.IRI("http://example.com/outbox?page=3"),
|
||||
Prev: pub.IRI("http://example.com/outbox?page=1"),
|
||||
ID: pub.ID("http://example.com/outbox?page=2"),
|
||||
Type: pub.OrderedCollectionPageType,
|
||||
URL: pub.IRI("http://example.com/outbox?page=2"),
|
||||
Current: pub.IRI("http://example.com/outbox?page=2"),
|
||||
TotalItems: 1,
|
||||
StartIndex: 100,
|
||||
OrderedItems: pub.ItemCollection{
|
||||
&pub.Object{
|
||||
ID: pub.ID("http://example.com/outbox/53c6fb47"),
|
||||
Type: pub.ArticleType,
|
||||
Name: pub.NaturalLanguageValues{{Ref: pub.NilLangRef, Value: pub.Content("Example title")}},
|
||||
Content: pub.NaturalLanguageValues{{Ref: pub.NilLangRef, Value: pub.Content("Example content!")}},
|
||||
URL: pub.IRI("http://example.com/53c6fb47"),
|
||||
MediaType: pub.MimeType("text/markdown"),
|
||||
Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
|
||||
Generator: pub.IRI("http://example.com"),
|
||||
AttributedTo: pub.IRI("http://example.com/accounts/alice"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
//"natural_language_values": {
|
||||
// expected: true,
|
||||
// blank: &pub.NaturalLanguageValues{},
|
||||
// result: &pub.NaturalLanguageValues{
|
||||
// {
|
||||
// pub.NilLangRef, pub.Content([]byte{'\n','\t', '\t', '\n'}),
|
||||
// },
|
||||
// {pub.LangRef("en"), pub.Content("Ana got apples ⓐ")},
|
||||
// {pub.LangRef("fr"), pub.Content("Aná a des pommes ⒜")},
|
||||
// {pub.LangRef("ro"), pub.Content("Ana are mere")},
|
||||
// },
|
||||
//},
|
||||
//"activity_create_simple": {
|
||||
// expected: true,
|
||||
// blank: &pub.Create{},
|
||||
// result: &pub.Create{
|
||||
// Type: pub.CreateType,
|
||||
// Actor: pub.IRI("https://littr.git/api/accounts/anonymous"),
|
||||
// Object: &pub.Object{
|
||||
// Type: pub.NoteType,
|
||||
// AttributedTo: pub.IRI("https://littr.git/api/accounts/anonymous"),
|
||||
// InReplyTo: pub.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff"),
|
||||
// Content: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("<p>Hello world</p>")}},
|
||||
// To: pub.ItemCollection{pub.IRI("https://www.w3.org/ns/activitystreams#Public")},
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"activity_create_multiple_objects": {
|
||||
// expected: true,
|
||||
// blank: &pub.Create{},
|
||||
// result: &pub.Create{
|
||||
// Type: pub.CreateType,
|
||||
// Actor: pub.IRI("https://littr.git/api/accounts/anonymous"),
|
||||
// Object: pub.ItemCollection{
|
||||
// &pub.Object{
|
||||
// Type: pub.NoteType,
|
||||
// AttributedTo: pub.IRI("https://littr.git/api/accounts/anonymous"),
|
||||
// InReplyTo: pub.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff"),
|
||||
// Content: pub.NaturalLanguageValues{{pub.NilLangRef, pub.Content("<p>Hello world</p>")}},
|
||||
// To: pub.ItemCollection{pub.IRI("https://www.w3.org/ns/activitystreams#Public")},
|
||||
// },
|
||||
// &pub.Article{
|
||||
// Type: pub.ArticleType,
|
||||
// ID: pub.ID("http://www.test.example/article/1"),
|
||||
// Name: pub.NaturalLanguageValues{
|
||||
// {
|
||||
// pub.NilLangRef,
|
||||
// pub.Content("This someday will grow up to be an article"),
|
||||
// },
|
||||
// },
|
||||
// InReplyTo: pub.ItemCollection{
|
||||
// pub.IRI("http://www.test.example/object/1"),
|
||||
// pub.IRI("http://www.test.example/object/778"),
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"object_with_audience": testPair{
|
||||
// expected: true,
|
||||
// blank: &pub.Object{},
|
||||
// result: &pub.Object{
|
||||
// Type: pub.ObjectType,
|
||||
// ID: pub.ID("http://www.test.example/object/1"),
|
||||
// To: pub.ItemCollection{
|
||||
// pub.IRI("https://www.w3.org/ns/activitystreams#Public"),
|
||||
// },
|
||||
// Bto: pub.ItemCollection{
|
||||
// pub.IRI("http://example.com/sharedInbox"),
|
||||
// },
|
||||
// CC: pub.ItemCollection{
|
||||
// pub.IRI("https://example.com/actors/ana"),
|
||||
// pub.IRI("https://example.com/actors/bob"),
|
||||
// },
|
||||
// BCC: pub.ItemCollection{
|
||||
// pub.IRI("https://darkside.cookie/actors/darthvader"),
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
//"article_with_multiple_inreplyto": {
|
||||
// expected: true,
|
||||
// blank: &pub.Article{},
|
||||
// result: &pub.Article{
|
||||
// Type: pub.ArticleType,
|
||||
// ID: pub.ID("http://www.test.example/article/1"),
|
||||
// Name: pub.NaturalLanguageValues{
|
||||
// {
|
||||
// pub.NilLangRef,
|
||||
// pub.Content("This someday will grow up to be an article"),
|
||||
// },
|
||||
// },
|
||||
// InReplyTo: pub.ItemCollection{
|
||||
// pub.IRI("http://www.test.example/object/1"),
|
||||
// pub.IRI("http://www.test.example/object/778"),
|
||||
// },
|
||||
// },
|
||||
//},
|
||||
}
|
||||
|
||||
func getFileContents(path string) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
st, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := make([]byte, st.Size())
|
||||
_, _ = io.ReadFull(f, data)
|
||||
data = bytes.Trim(data, "\x00")
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func TestUnmarshal(t *testing.T) {
|
||||
var err error
|
||||
|
||||
f := t.Errorf
|
||||
if len(allTests) == 0 {
|
||||
t.Skip("No tests found")
|
||||
}
|
||||
|
||||
for k, pair := range allTests {
|
||||
path := filepath.Join(dir, fmt.Sprintf("%s.json", k))
|
||||
t.Run(path, func(t *testing.T) {
|
||||
var data []byte
|
||||
data, err = getFileContents(path)
|
||||
if err != nil {
|
||||
f("Error: %s for %s", err, path)
|
||||
return
|
||||
}
|
||||
object := pair.blank
|
||||
|
||||
err = j.Unmarshal(data, object)
|
||||
if err != nil {
|
||||
f("Error: %s for %s", err, data)
|
||||
return
|
||||
}
|
||||
expLbl := ""
|
||||
if !pair.expected {
|
||||
expLbl = "not be "
|
||||
}
|
||||
status := assertDeepEquals(f, object, pair.result)
|
||||
if pair.expected != status {
|
||||
if stopOnFailure {
|
||||
f = t.Fatalf
|
||||
}
|
||||
|
||||
f("Mock: %s: %s\n%#v\n should %sequal to expected\n%#v", k, path, object, expLbl, pair.result)
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
oj, err := j.Marshal(object)
|
||||
if err != nil {
|
||||
f(err.Error())
|
||||
}
|
||||
tj, err := j.Marshal(pair.result)
|
||||
if err != nil {
|
||||
f(err.Error())
|
||||
}
|
||||
f("Mock: %s: %s\n%s\n should %sequal to expected\n%s", k, path, oj, expLbl, tj)
|
||||
}
|
||||
//if err == nil {
|
||||
// fmt.Printf(" --- %s: %s\n %s\n", "PASS", k, path)
|
||||
//}
|
||||
})
|
||||
}
|
||||
}
|
283
tombstone.go
Normal file
283
tombstone.go
Normal file
|
@ -0,0 +1,283 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Tombstone a Tombstone represents a content object that has been deleted.
|
||||
// It can be used in Collections to signify that there used to be an object at this position,
|
||||
// but it has been deleted.
|
||||
type Tombstone struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// FormerType On a Tombstone object, the formerType property identifies the type of the object that was deleted.
|
||||
FormerType ActivityVocabularyType `jsonld:"formerType,omitempty"`
|
||||
// Deleted On a Tombstone object, the deleted property is a timestamp for when the object was deleted.
|
||||
Deleted time.Time `jsonld:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Tombstone objects
|
||||
func (t Tombstone) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Tombstone objects
|
||||
func (t Tombstone) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Tombstone objects
|
||||
func (t Tombstone) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Tombstone object
|
||||
func (t Tombstone) GetLink() IRI {
|
||||
return IRI(t.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Tombstone
|
||||
func (t Tombstone) GetType() ActivityVocabularyType {
|
||||
return t.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Tombstone
|
||||
func (t Tombstone) GetID() ID {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (t *Tombstone) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadTombstone(val, t)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (t Tombstone) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(t, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if len(t.FormerType) > 0 {
|
||||
if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(&b, "formerType", v) || notEmpty
|
||||
}
|
||||
}
|
||||
if !t.Deleted.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(&b, "deleted", t.Deleted) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (t *Tombstone) UnmarshalBinary(data []byte) error {
|
||||
return t.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (t Tombstone) MarshalBinary() ([]byte, error) {
|
||||
return t.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (t Tombstone) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapTombstoneProperties(mm, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (t *Tombstone) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapTombstoneProperties(mm, t)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Tombstone object's To, Bto, CC and BCC properties
|
||||
func (t *Tombstone) Recipients() ItemCollection {
|
||||
aud := t.Audience
|
||||
return ItemCollectionDeduplication(&t.To, &t.CC, &t.Bto, &t.BCC, &aud)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (t *Tombstone) Clean() {
|
||||
_ = OnObject(t, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t Tombstone) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { formerType: %q }", t, t.Type, t.FormerType)
|
||||
}
|
||||
}
|
||||
|
||||
// ToTombstone
|
||||
func ToTombstone(it Item) (*Tombstone, error) {
|
||||
switch i := it.(type) {
|
||||
case *Tombstone:
|
||||
return i, nil
|
||||
case Tombstone:
|
||||
return &i, nil
|
||||
case *Object:
|
||||
return (*Tombstone)(unsafe.Pointer(i)), nil
|
||||
case Object:
|
||||
return (*Tombstone)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
return reflectItemToType[Tombstone](it)
|
||||
}
|
||||
}
|
||||
|
||||
type withTombstoneFn func(*Tombstone) error
|
||||
|
||||
// OnTombstone calls function fn on it Item if it can be asserted to type *Tombstone
|
||||
//
|
||||
// This function should be called if trying to access the Tombstone specific properties
|
||||
// like "formerType" or "deleted".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnTombstone(it Item, fn withTombstoneFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := OnTombstone(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToTombstone(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
96
tombstone_test.go
Normal file
96
tombstone_test.go
Normal file
|
@ -0,0 +1,96 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTombstone_GetID(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_GetLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_GetType(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_IsCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_IsLink(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_IsObject(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_UnmarshalJSON(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestTombstone_Clean(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func assertTombstoneWithTesting(fn canErrorFunc, expected *Tombstone) withTombstoneFn {
|
||||
return func(p *Tombstone) error {
|
||||
if !assertDeepEquals(fn, p, expected) {
|
||||
return fmt.Errorf("not equal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnTombstone(t *testing.T) {
|
||||
testTombstone := Tombstone{
|
||||
ID: "https://example.com",
|
||||
}
|
||||
type args struct {
|
||||
it Item
|
||||
fn func(canErrorFunc, *Tombstone) withTombstoneFn
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
args: args{testTombstone, assertTombstoneWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "single fails",
|
||||
args: args{&Tombstone{ID: "https://not-equal"}, assertTombstoneWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "collection of profiles",
|
||||
args: args{ItemCollection{testTombstone, testTombstone}, assertTombstoneWithTesting},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "collection of profiles fails",
|
||||
args: args{ItemCollection{testTombstone, &Tombstone{ID: "not-equal"}}, assertTombstoneWithTesting},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var logFn canErrorFunc
|
||||
if tt.wantErr {
|
||||
logFn = t.Logf
|
||||
} else {
|
||||
logFn = t.Errorf
|
||||
}
|
||||
if err := OnTombstone(tt.args.it, tt.args.fn(logFn, &testTombstone)); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OnTombstone() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
396
typer.go
Normal file
396
typer.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ap/errors"
|
||||
)
|
||||
|
||||
// CollectionPath
|
||||
type CollectionPath string
|
||||
|
||||
// CollectionPaths
|
||||
type CollectionPaths []CollectionPath
|
||||
|
||||
const (
|
||||
Unknown = CollectionPath("")
|
||||
// Outbox
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#outbox
|
||||
//
|
||||
// The outbox is discovered through the outbox property of an actor's profile.
|
||||
// The outbox MUST be an OrderedCollection.
|
||||
//
|
||||
// The outbox stream contains activities the user has published, subject to the ability of the requestor
|
||||
// to retrieve the activity (that is, the contents of the outbox are filtered by the permissions
|
||||
// of the person reading it). If a user submits a request without Authorization the server should respond
|
||||
// with all of the Public posts. This could potentially be all relevant objects published by the user,
|
||||
// though the number of available items is left to the discretion of those implementing and deploying the server.
|
||||
//
|
||||
// The outbox accepts HTTP POST requests, with behaviour described in Client to Server Interactions.
|
||||
Outbox = CollectionPath("outbox")
|
||||
// Inbox
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#inbox
|
||||
//
|
||||
// The inbox is discovered through the inbox property of an actor's profile. The inbox MUST be an OrderedCollection.
|
||||
//
|
||||
// The inbox stream contains all activities received by the actor. The server SHOULD filter content according
|
||||
// to the requester's permission. In general, the owner of an inbox is likely to be able to access
|
||||
// all of their inbox contents. Depending on access control, some other content may be public,
|
||||
// whereas other content may require authentication for non-owner users, if they can access the inbox at all.
|
||||
//
|
||||
// The server MUST perform de-duplication of activities returned by the inbox. Duplication can occur
|
||||
// if an activity is addressed both to an actor's followers, and a specific actor who also follows
|
||||
// the recipient actor, and the server has failed to de-duplicate the recipients list.
|
||||
// Such deduplication MUST be performed by comparing the id of the activities and dropping any activities already seen.
|
||||
//
|
||||
// The inboxes of actors on federated servers accepts HTTP POST requests, with behaviour described in Delivery.
|
||||
// Non-federated servers SHOULD return a 405 Method Not Allowed upon receipt of a POST request.
|
||||
Inbox = CollectionPath("inbox")
|
||||
// Followers
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#followers
|
||||
//
|
||||
// Every actor SHOULD have a followers collection. This is a list of everyone who has sent a Follow activity
|
||||
// for the actor, added as a side effect. This is where one would find a list of all the actors that are following
|
||||
// the actor. The followers collection MUST be either an OrderedCollection or a Collection and MAY be filtered
|
||||
// on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
//
|
||||
// NOTE: Default for notification targeting
|
||||
// The follow activity generally is a request to see the objects an actor creates.
|
||||
// This makes the Followers collection an appropriate default target for delivery of notifications.
|
||||
Followers = CollectionPath("followers")
|
||||
// Following
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#following
|
||||
//
|
||||
// Every actor SHOULD have a following collection. This is a list of everybody that the actor has followed,
|
||||
// added as a side effect. The following collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
Following = CollectionPath("following")
|
||||
// Liked
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#liked
|
||||
//
|
||||
// Every actor MAY have a liked collection. This is a list of every object from all of the actor's Like activities,
|
||||
// added as a side effect. The liked collection MUST be either an OrderedCollection or a Collection and
|
||||
// MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
Liked = CollectionPath("liked")
|
||||
// Likes
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#likes
|
||||
//
|
||||
// Every object MAY have a likes collection. This is a list of all Like activities with this object as the object
|
||||
// property, added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
//
|
||||
// NOTE
|
||||
// Care should be taken to not confuse the the likes collection with the similarly named but different liked
|
||||
// collection. In sum:
|
||||
//
|
||||
// * liked: Specifically a property of actors. This is a collection of Like activities performed by the actor,
|
||||
// added to the collection as a side effect of delivery to the outbox.
|
||||
// * likes: May be a property of any object. This is a collection of Like activities referencing this object,
|
||||
// added to the collection as a side effect of delivery to the inbox.
|
||||
Likes = CollectionPath("likes")
|
||||
// Shares
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#shares
|
||||
//
|
||||
// Every object MAY have a shares collection. This is a list of all Announce activities with this object
|
||||
// as the object property, added as a side effect. The shares collection MUST be either an OrderedCollection
|
||||
// or a Collection and MAY be filtered on privileges of an authenticated user or as appropriate when
|
||||
// no authentication is given.
|
||||
Shares = CollectionPath("shares")
|
||||
Replies = CollectionPath("replies") // activitystreams
|
||||
)
|
||||
|
||||
var (
|
||||
validActivityCollection = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Likes,
|
||||
Shares,
|
||||
Replies, // activitystreams
|
||||
}
|
||||
OfObject = CollectionPaths{
|
||||
Likes,
|
||||
Shares,
|
||||
Replies,
|
||||
}
|
||||
OfActor = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Liked,
|
||||
Following,
|
||||
Followers,
|
||||
}
|
||||
|
||||
ActivityPubCollections = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Liked,
|
||||
Following,
|
||||
Followers,
|
||||
Likes,
|
||||
Shares,
|
||||
Replies,
|
||||
}
|
||||
)
|
||||
|
||||
func (t CollectionPaths) Contains(typ CollectionPath) bool {
|
||||
for _, tt := range t {
|
||||
if strings.EqualFold(string(typ), string(tt)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Split splits the IRI in an actor IRI and its CollectionPath
|
||||
// if the CollectionPath is found in the elements in the t CollectionPaths slice
|
||||
func (t CollectionPaths) Split(i IRI) (IRI, CollectionPath) {
|
||||
if u, err := i.URL(); err == nil {
|
||||
maybeActor, maybeCol := filepath.Split(u.Path)
|
||||
if len(maybeActor) == 0 {
|
||||
return i, Unknown
|
||||
}
|
||||
tt := CollectionPath(maybeCol)
|
||||
if !t.Contains(tt) {
|
||||
tt = ""
|
||||
}
|
||||
u.Path = strings.TrimRight(maybeActor, "/")
|
||||
iri := IRI(u.String())
|
||||
return iri, tt
|
||||
}
|
||||
maybeActor, maybeCol := filepath.Split(i.String())
|
||||
if len(maybeActor) == 0 {
|
||||
return i, Unknown
|
||||
}
|
||||
tt := CollectionPath(maybeCol)
|
||||
if !t.Contains(tt) {
|
||||
return i, Unknown
|
||||
}
|
||||
maybeActor = strings.TrimRight(maybeActor, "/")
|
||||
return IRI(maybeActor), tt
|
||||
}
|
||||
|
||||
// IRIf formats an IRI from an existing IRI and the CollectionPath type
|
||||
func IRIf(i IRI, t CollectionPath) IRI {
|
||||
si := i.String()
|
||||
s := strings.Builder{}
|
||||
_, _ = s.WriteString(si)
|
||||
if l := len(si); l == 0 || si[l-1] != '/' {
|
||||
_, _ = s.WriteRune('/')
|
||||
}
|
||||
_, _ = s.WriteString(string(t))
|
||||
return IRI(s.String())
|
||||
}
|
||||
|
||||
// IRI gives us the IRI of the t CollectionPath type corresponding to the i Item,
|
||||
// or generates a new one if not found.
|
||||
func (t CollectionPath) IRI(i Item) IRI {
|
||||
if IsNil(i) {
|
||||
return IRIf("", t)
|
||||
}
|
||||
if IsObject(i) {
|
||||
if it := t.Of(i); !IsNil(it) {
|
||||
return it.GetLink()
|
||||
}
|
||||
}
|
||||
return IRIf(i.GetLink(), t)
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofItemCollection(col ItemCollection) Item {
|
||||
iriCol := make(ItemCollection, len(col))
|
||||
for i, it := range col {
|
||||
iriCol[i] = t.Of(it)
|
||||
}
|
||||
return iriCol
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofObject(ob *Object) Item {
|
||||
var it Item
|
||||
switch t {
|
||||
case Likes:
|
||||
it = ob.Likes
|
||||
case Shares:
|
||||
it = ob.Shares
|
||||
case Replies:
|
||||
it = ob.Replies
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofActor(a *Actor) Item {
|
||||
var it Item
|
||||
switch t {
|
||||
case Inbox:
|
||||
it = a.Inbox
|
||||
case Outbox:
|
||||
it = a.Outbox
|
||||
case Liked:
|
||||
it = a.Liked
|
||||
case Following:
|
||||
it = a.Following
|
||||
case Followers:
|
||||
it = a.Followers
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofIRI(iri IRI) Item {
|
||||
if len(iri) == 0 {
|
||||
return nil
|
||||
}
|
||||
return iri.AddPath(string(t))
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofItem(i Item) Item {
|
||||
if IsNil(i) {
|
||||
return nil
|
||||
}
|
||||
var it Item
|
||||
if IsIRI(i) {
|
||||
it = t.ofIRI(i.GetLink())
|
||||
}
|
||||
if IsItemCollection(i) {
|
||||
_ = OnItemCollection(i, func(col *ItemCollection) error {
|
||||
it = t.ofItemCollection(*col)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if OfActor.Contains(t) && ActorTypes.Contains(i.GetType()) {
|
||||
_ = OnActor(i, func(a *Actor) error {
|
||||
it = t.ofActor(a)
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
_ = OnObject(i, func(o *Object) error {
|
||||
it = t.ofObject(o)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
// Of returns the property of the i [Item] that corresponds to the t [CollectionPath] type.
|
||||
// If it's missing, it returns nil.
|
||||
func (t CollectionPath) Of(i Item) Item {
|
||||
return t.ofItem(i)
|
||||
}
|
||||
|
||||
// OfActor returns the base IRI of received i, if i represents an IRI matching CollectionPath type t
|
||||
func (t CollectionPath) OfActor(i IRI) (IRI, error) {
|
||||
maybeActor, maybeCol := filepath.Split(i.String())
|
||||
if strings.EqualFold(maybeCol, string(t)) {
|
||||
maybeActor = strings.TrimRight(maybeActor, "/")
|
||||
return IRI(maybeActor), nil
|
||||
}
|
||||
return EmptyIRI, errors.Newf("IRI does not represent a valid %s CollectionPath", t)
|
||||
}
|
||||
|
||||
// Split returns the base IRI of received i, if i represents an IRI matching CollectionPath type t
|
||||
func Split(i IRI) (IRI, CollectionPath) {
|
||||
return ActivityPubCollections.Split(i)
|
||||
}
|
||||
|
||||
func getValidActivityCollection(t CollectionPath) CollectionPath {
|
||||
if validActivityCollection.Contains(t) {
|
||||
return t
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Activities
|
||||
func ValidActivityCollection(typ CollectionPath) bool {
|
||||
return getValidActivityCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
var validObjectCollection = []CollectionPath{
|
||||
Following,
|
||||
Followers,
|
||||
Liked,
|
||||
}
|
||||
|
||||
func getValidObjectCollection(typ CollectionPath) CollectionPath {
|
||||
for _, t := range validObjectCollection {
|
||||
if strings.EqualFold(string(typ), string(t)) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Objects
|
||||
func ValidObjectCollection(typ CollectionPath) bool {
|
||||
return getValidObjectCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
func getValidCollection(typ CollectionPath) CollectionPath {
|
||||
if typ := getValidActivityCollection(typ); typ != Unknown {
|
||||
return typ
|
||||
}
|
||||
if typ := getValidObjectCollection(typ); typ != Unknown {
|
||||
return typ
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
func ValidCollection(typ CollectionPath) bool {
|
||||
return getValidCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
func ValidCollectionIRI(i IRI) bool {
|
||||
_, t := Split(i)
|
||||
return getValidCollection(t) != Unknown
|
||||
}
|
||||
|
||||
// AddTo adds CollectionPath type IRI on the corresponding property of the i Item
|
||||
func (t CollectionPath) AddTo(i Item) (IRI, bool) {
|
||||
if IsNil(i) || !i.IsObject() {
|
||||
return NilIRI, false
|
||||
}
|
||||
status := false
|
||||
var iri IRI
|
||||
if OfActor.Contains(t) {
|
||||
OnActor(i, func(a *Actor) error {
|
||||
if status = t == Inbox && IsNil(a.Inbox); status {
|
||||
a.Inbox = IRIf(a.GetLink(), t)
|
||||
iri = a.Inbox.GetLink()
|
||||
} else if status = t == Outbox && IsNil(a.Outbox); status {
|
||||
a.Outbox = IRIf(a.GetLink(), t)
|
||||
iri = a.Outbox.GetLink()
|
||||
} else if status = t == Liked && IsNil(a.Liked); status {
|
||||
a.Liked = IRIf(a.GetLink(), t)
|
||||
iri = a.Liked.GetLink()
|
||||
} else if status = t == Following && IsNil(a.Following); status {
|
||||
a.Following = IRIf(a.GetLink(), t)
|
||||
iri = a.Following.GetLink()
|
||||
} else if status = t == Followers && IsNil(a.Followers); status {
|
||||
a.Followers = IRIf(a.GetLink(), t)
|
||||
iri = a.Followers.GetLink()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else if OfObject.Contains(t) {
|
||||
OnObject(i, func(o *Object) error {
|
||||
if status = t == Likes && IsNil(o.Likes); status {
|
||||
o.Likes = IRIf(o.GetLink(), t)
|
||||
iri = o.Likes.GetLink()
|
||||
} else if status = t == Shares && IsNil(o.Shares); status {
|
||||
o.Shares = IRIf(o.GetLink(), t)
|
||||
iri = o.Shares.GetLink()
|
||||
} else if status = t == Replies && IsNil(o.Replies); status {
|
||||
o.Replies = IRIf(o.GetLink(), t)
|
||||
iri = o.Replies.GetLink()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
iri = IRIf(i.GetLink(), t)
|
||||
}
|
||||
return iri, status
|
||||
}
|
532
typer_test.go
Normal file
532
typer_test.go
Normal file
|
@ -0,0 +1,532 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPathTyper_Type(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestValidActivityCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestValidCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestValidObjectCollection(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestValidCollectionIRI(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestSplit(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionTypes_Of(t *testing.T) {
|
||||
type args struct {
|
||||
o Item
|
||||
t CollectionPath
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want Item
|
||||
}{
|
||||
{
|
||||
name: "nil from nil object",
|
||||
args: args{
|
||||
o: nil,
|
||||
t: "likes",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil from invalid CollectionPath type",
|
||||
args: args{
|
||||
o: Object{
|
||||
Likes: IRI("test"),
|
||||
},
|
||||
t: "like",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "nil from nil CollectionPath type",
|
||||
args: args{
|
||||
o: Object{
|
||||
Likes: nil,
|
||||
},
|
||||
t: "likes",
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "get likes iri",
|
||||
args: args{
|
||||
o: Object{
|
||||
Likes: IRI("test"),
|
||||
},
|
||||
t: "likes",
|
||||
},
|
||||
want: IRI("test"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if ob := test.args.t.Of(test.args.o); ob != test.want {
|
||||
t.Errorf("Object received %#v is different, expected #%v", ob, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionType_IRI(t *testing.T) {
|
||||
type args struct {
|
||||
o Item
|
||||
t CollectionPath
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want IRI
|
||||
}{
|
||||
{
|
||||
name: "just path from nil object",
|
||||
args: args{
|
||||
o: nil,
|
||||
t: "likes",
|
||||
},
|
||||
want: IRI("/likes"),
|
||||
},
|
||||
{
|
||||
name: "emptyIRI from invalid CollectionPath type",
|
||||
args: args{
|
||||
o: Object{
|
||||
Likes: IRI("test"),
|
||||
},
|
||||
t: "like",
|
||||
},
|
||||
want: "/like",
|
||||
},
|
||||
{
|
||||
name: "just path from object without ID",
|
||||
args: args{
|
||||
o: Object{},
|
||||
t: "likes",
|
||||
},
|
||||
want: IRI("/likes"),
|
||||
},
|
||||
{
|
||||
name: "likes iri on object",
|
||||
args: args{
|
||||
o: Object{
|
||||
ID: "http://example.com",
|
||||
Likes: IRI("test"),
|
||||
},
|
||||
t: "likes",
|
||||
},
|
||||
want: IRI("test"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if ob := test.args.t.IRI(test.args.o); ob != test.want {
|
||||
t.Errorf("IRI received %q is different, expected %q", ob, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionType_OfActor(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestCollectionTypes_Contains(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
||||
|
||||
func TestIRIf(t *testing.T) {
|
||||
type args struct {
|
||||
i IRI
|
||||
t CollectionPath
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want IRI
|
||||
}{
|
||||
{
|
||||
name: "nil iri",
|
||||
args: args{
|
||||
i: Object{}.ID,
|
||||
t: "inbox",
|
||||
},
|
||||
want: "/inbox",
|
||||
},
|
||||
{
|
||||
name: "empty iri",
|
||||
args: args{
|
||||
i: "",
|
||||
t: "inbox",
|
||||
},
|
||||
want: "/inbox",
|
||||
},
|
||||
{
|
||||
name: "plain concat",
|
||||
args: args{
|
||||
i: "https://example.com",
|
||||
t: "inbox",
|
||||
},
|
||||
want: "https://example.com/inbox",
|
||||
},
|
||||
{
|
||||
name: "strip root from iri",
|
||||
args: args{
|
||||
i: "https://example.com/",
|
||||
t: "inbox",
|
||||
},
|
||||
want: "https://example.com/inbox",
|
||||
},
|
||||
{
|
||||
name: "invalid iri",
|
||||
args: args{
|
||||
i: "example.com",
|
||||
t: "test",
|
||||
},
|
||||
want: "example.com/test",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IRIf(tt.args.i, tt.args.t); got != tt.want {
|
||||
t.Errorf("IRIf() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionType_AddTo(t *testing.T) {
|
||||
type args struct {
|
||||
i Item
|
||||
}
|
||||
var i Item
|
||||
var o *Object
|
||||
tests := []struct {
|
||||
name string
|
||||
t CollectionPath
|
||||
args args
|
||||
want IRI
|
||||
want1 bool
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
t: "test",
|
||||
args: args{
|
||||
i: &Object{ID: "http://example.com/addTo"},
|
||||
},
|
||||
want: "http://example.com/addTo/test",
|
||||
want1: false, // this seems to always be false
|
||||
},
|
||||
{
|
||||
name: "on-nil-item",
|
||||
t: "test",
|
||||
args: args{
|
||||
i: i,
|
||||
},
|
||||
want: NilIRI,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "on-nil",
|
||||
t: "test",
|
||||
args: args{
|
||||
i: nil,
|
||||
},
|
||||
want: NilIRI,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "on-nil-object",
|
||||
t: "test",
|
||||
args: args{
|
||||
i: o,
|
||||
},
|
||||
want: NilIRI,
|
||||
want1: false,
|
||||
},
|
||||
{
|
||||
name: "on-nil-item",
|
||||
t: "test",
|
||||
args: args{
|
||||
i: i,
|
||||
},
|
||||
want: NilIRI,
|
||||
want1: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1 := tt.t.AddTo(tt.args.i)
|
||||
if got != tt.want {
|
||||
t.Errorf("AddTo() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
if got1 != tt.want1 {
|
||||
t.Errorf("AddTo() got1 = %v, want %v", got1, tt.want1)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPaths_Split(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
t CollectionPaths
|
||||
given IRI
|
||||
maybeActor IRI
|
||||
maybeCol CollectionPath
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
t: nil,
|
||||
given: "",
|
||||
maybeActor: "",
|
||||
maybeCol: "",
|
||||
},
|
||||
{
|
||||
name: "nil with example.com",
|
||||
t: nil,
|
||||
given: "example.com",
|
||||
maybeActor: "example.com",
|
||||
maybeCol: "",
|
||||
},
|
||||
{
|
||||
name: "nil with https://example.com",
|
||||
t: nil,
|
||||
given: "https://example.com/",
|
||||
maybeActor: "https://example.com",
|
||||
maybeCol: Unknown,
|
||||
},
|
||||
{
|
||||
name: "outbox with https://example.com/outbox",
|
||||
t: CollectionPaths{Outbox},
|
||||
given: "https://example.com/outbox",
|
||||
maybeActor: "https://example.com",
|
||||
maybeCol: Outbox,
|
||||
},
|
||||
{
|
||||
name: "{outbox,inbox} with https://example.com/inbox",
|
||||
t: CollectionPaths{Outbox, Inbox},
|
||||
given: "https://example.com/inbox",
|
||||
maybeActor: "https://example.com",
|
||||
maybeCol: Inbox,
|
||||
},
|
||||
{
|
||||
// TODO(marius): This feels wrong.
|
||||
name: "outbox with https://example.com/inbox",
|
||||
t: CollectionPaths{Outbox},
|
||||
given: "https://example.com/inbox",
|
||||
maybeActor: "https://example.com",
|
||||
maybeCol: Unknown,
|
||||
},
|
||||
{
|
||||
name: "invalid url",
|
||||
t: CollectionPaths{Inbox},
|
||||
given: "127.0.0.1:666/inbox",
|
||||
maybeActor: "127.0.0.1:666",
|
||||
maybeCol: Inbox,
|
||||
},
|
||||
{
|
||||
name: "invalid url - collection doesn't match",
|
||||
t: CollectionPaths{Outbox},
|
||||
given: "127.0.0.1:666/inbox",
|
||||
maybeActor: "127.0.0.1:666/inbox",
|
||||
maybeCol: Unknown,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ma, mc := tt.t.Split(tt.given)
|
||||
if ma != tt.maybeActor {
|
||||
t.Errorf("Split() got Actor = %q, want %q", ma, tt.maybeActor)
|
||||
}
|
||||
if mc != tt.maybeCol {
|
||||
t.Errorf("Split() got Colletion Path = %q, want %q", mc, tt.maybeCol)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionPath_Of(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
t CollectionPath
|
||||
arg Item
|
||||
want Item
|
||||
}{
|
||||
{
|
||||
name: "all-nil",
|
||||
t: "",
|
||||
},
|
||||
{
|
||||
name: "inbox-nil",
|
||||
t: Inbox,
|
||||
},
|
||||
{
|
||||
name: "outbox-nil",
|
||||
t: Outbox,
|
||||
},
|
||||
{
|
||||
name: "followers-nil",
|
||||
t: Followers,
|
||||
},
|
||||
{
|
||||
name: "following-nil",
|
||||
t: Following,
|
||||
},
|
||||
{
|
||||
name: "liked-nil",
|
||||
t: Liked,
|
||||
},
|
||||
{
|
||||
name: "likes-nil",
|
||||
t: Likes,
|
||||
},
|
||||
{
|
||||
name: "shares-nil",
|
||||
t: Shares,
|
||||
},
|
||||
{
|
||||
name: "replies-nil",
|
||||
t: Replies,
|
||||
},
|
||||
{
|
||||
name: "inbox-empty",
|
||||
t: Inbox,
|
||||
arg: &Actor{},
|
||||
},
|
||||
{
|
||||
name: "outbox-empty",
|
||||
t: Outbox,
|
||||
arg: &Actor{},
|
||||
},
|
||||
{
|
||||
name: "followers-empty",
|
||||
t: Followers,
|
||||
arg: &Actor{},
|
||||
},
|
||||
{
|
||||
name: "following-empty",
|
||||
t: Following,
|
||||
arg: &Actor{},
|
||||
},
|
||||
{
|
||||
name: "liked-empty",
|
||||
t: Liked,
|
||||
arg: &Actor{},
|
||||
},
|
||||
{
|
||||
name: "likes-empty",
|
||||
t: Likes,
|
||||
arg: &Object{},
|
||||
},
|
||||
{
|
||||
name: "shares-empty",
|
||||
t: Shares,
|
||||
arg: &Object{},
|
||||
},
|
||||
{
|
||||
name: "replies-empty",
|
||||
t: Replies,
|
||||
arg: &Object{},
|
||||
},
|
||||
//
|
||||
{
|
||||
name: "inbox",
|
||||
t: Inbox,
|
||||
arg: &Actor{
|
||||
Type: PersonType,
|
||||
Inbox: IRI("https://example.com/inbox"),
|
||||
},
|
||||
want: IRI("https://example.com/inbox"),
|
||||
},
|
||||
{
|
||||
name: "outbox",
|
||||
t: Outbox,
|
||||
arg: &Actor{
|
||||
Type: PersonType,
|
||||
Outbox: IRI("https://example.com/outbox"),
|
||||
},
|
||||
want: IRI("https://example.com/outbox"),
|
||||
},
|
||||
{
|
||||
name: "followers",
|
||||
t: Followers,
|
||||
arg: &Actor{
|
||||
Type: GroupType,
|
||||
Followers: IRI("https://example.com/c132-333"),
|
||||
},
|
||||
want: IRI("https://example.com/c132-333"),
|
||||
},
|
||||
{
|
||||
name: "following",
|
||||
t: Following,
|
||||
arg: &Actor{
|
||||
Type: GroupType,
|
||||
Following: IRI("https://example.com/c666-333"),
|
||||
},
|
||||
want: IRI("https://example.com/c666-333"),
|
||||
},
|
||||
{
|
||||
name: "liked",
|
||||
t: Liked,
|
||||
arg: &Actor{
|
||||
Type: ApplicationType,
|
||||
Liked: IRI("https://example.com/l666"),
|
||||
},
|
||||
want: IRI("https://example.com/l666"),
|
||||
},
|
||||
{
|
||||
name: "likes",
|
||||
t: Likes,
|
||||
arg: &Object{
|
||||
Type: NoteType,
|
||||
Likes: IRI("https://example.com/l166"),
|
||||
},
|
||||
want: IRI("https://example.com/l166"),
|
||||
},
|
||||
{
|
||||
name: "shares",
|
||||
t: Shares,
|
||||
arg: &Object{
|
||||
Type: PageType,
|
||||
Shares: IRI("https://example.com/s266"),
|
||||
},
|
||||
want: IRI("https://example.com/s266"),
|
||||
},
|
||||
{
|
||||
name: "replies",
|
||||
t: Replies,
|
||||
arg: &Object{
|
||||
Type: ArticleType,
|
||||
Replies: IRI("https://example.com/r466"),
|
||||
},
|
||||
want: IRI("https://example.com/r466"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.t.Of(tt.arg); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Of() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
types.go
Normal file
66
types.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package activitypub
|
||||
|
||||
// ActivityVocabularyTypes is a type alias for a slice of ActivityVocabularyType elements
|
||||
type ActivityVocabularyTypes []ActivityVocabularyType
|
||||
|
||||
// Types contains all valid types in the ActivityPub vocabulary
|
||||
var Types = ActivityVocabularyTypes{
|
||||
LinkType,
|
||||
MentionType,
|
||||
|
||||
ArticleType,
|
||||
AudioType,
|
||||
DocumentType,
|
||||
EventType,
|
||||
ImageType,
|
||||
NoteType,
|
||||
PageType,
|
||||
PlaceType,
|
||||
ProfileType,
|
||||
RelationshipType,
|
||||
TombstoneType,
|
||||
VideoType,
|
||||
|
||||
QuestionType,
|
||||
|
||||
CollectionType,
|
||||
OrderedCollectionType,
|
||||
CollectionPageType,
|
||||
OrderedCollectionPageType,
|
||||
|
||||
ApplicationType,
|
||||
GroupType,
|
||||
OrganizationType,
|
||||
PersonType,
|
||||
ServiceType,
|
||||
|
||||
AcceptType,
|
||||
AddType,
|
||||
AnnounceType,
|
||||
BlockType,
|
||||
CreateType,
|
||||
DeleteType,
|
||||
DislikeType,
|
||||
FlagType,
|
||||
FollowType,
|
||||
IgnoreType,
|
||||
InviteType,
|
||||
JoinType,
|
||||
LeaveType,
|
||||
LikeType,
|
||||
ListenType,
|
||||
MoveType,
|
||||
OfferType,
|
||||
RejectType,
|
||||
ReadType,
|
||||
RemoveType,
|
||||
TentativeRejectType,
|
||||
TentativeAcceptType,
|
||||
UndoType,
|
||||
UpdateType,
|
||||
ViewType,
|
||||
|
||||
ArriveType,
|
||||
TravelType,
|
||||
QuestionType,
|
||||
}
|
16
validation.go
Normal file
16
validation.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package activitypub
|
||||
|
||||
// ValidationErrors is an aggregated error interface that allows
|
||||
// a Validator implementation to return all possible errors.
|
||||
type ValidationErrors interface {
|
||||
error
|
||||
Errors() []error
|
||||
Add(error)
|
||||
}
|
||||
|
||||
// Validator is the interface that needs to be implemented by objects that
|
||||
// provide a validation mechanism for incoming ActivityPub Objects or IRIs
|
||||
// against an external set of rules.
|
||||
type Validator interface {
|
||||
Validate(receiver IRI, incoming Item) (bool, ValidationErrors)
|
||||
}
|
7
validation_test.go
Normal file
7
validation_test.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package activitypub
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultValidator_Validate(t *testing.T) {
|
||||
t.Skipf("TODO")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue