1
0
Fork 0

Adding upstream version 0.0~git20250501.71edba4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-18 22:06:39 +02:00
parent c6ff472a6d
commit c8085bda34
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
87 changed files with 24009 additions and 0 deletions

15
.build.yml Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,80 @@
# About GoActivityPub: Vocabulary
[![MIT Licensed](https://img.shields.io/github/license/go-ap/activitypub.svg)](https://raw.githubusercontent.com/go-ap/activitypub/master/LICENSE)
[![Build Status](https://builds.sr.ht/~mariusor/activitypub.svg)](https://builds.sr.ht/~mariusor/activitypub)
[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/activitypub.svg)](https://codecov.io/gh/go-ap/activitypub)
[![Go Report Card](https://goreportcard.com/badge/github.com/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
View 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

File diff suppressed because it is too large Load diff

615
actor.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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))
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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)
}
})
}
}

1008
object.go Normal file

File diff suppressed because it is too large Load diff

20
object_id.go Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

428
ordered_collection.go Normal file
View 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
View 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
})
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}
}

View 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"
]
}
]
}

View 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"
}
}

View 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"
}
}

View 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
View file

@ -0,0 +1 @@
{}

View 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
}

View 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"
}

View file

@ -0,0 +1,6 @@
{
"-": "\n\t\t\n",
"en": "Ana got apples ⓐ",
"fr": "Aná a des pommes ⒜",
"ro": "Ana are mere"
}

View 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"
}

View 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"
}

View 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"
]
}

View 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"
}
]
}
}

View 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"
}
]
}

View file

@ -0,0 +1,4 @@
{
"@context":"https://www.w3.org/ns/activitystreams",
"url":"http://littr.git/api/accounts/system"
}

View file

@ -0,0 +1,7 @@
{
"@context":"https://www.w3.org/ns/activitystreams",
"url": [
"http://littr.git/api/accounts/system",
"http://littr.git/~system"
]
}

View 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"
}
]
}

View 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"
}
]
}

View 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"
}

View 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"
}
}

View 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)
}
*/

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
package activitypub
import "testing"
func TestDefaultValidator_Validate(t *testing.T) {
t.Skipf("TODO")
}