Add 'streams/' from commit 'f968addfe0e35bbee21ed1233ecacf65391e3f59'

git-subtree-dir: streams
git-subtree-mainline: 0ad2174ed0
git-subtree-split: f968addfe0
This commit is contained in:
Marius Orcsik 2019-12-03 16:33:20 +01:00
commit 72a9ade82d
59 changed files with 9691 additions and 0 deletions

24
streams/.build.yml Normal file
View file

@ -0,0 +1,24 @@
image: archlinux
secrets:
- 72c9ab0b-ed5f-4291-bab7-30b361be94a6
packages:
- go
sources:
- https://github.com/go-ap/activitystreams
environment:
GO111MODULE: 'on'
tasks:
- setup: |
cd activitystreams && go mod download
- tests: |
cd activitystreams && make test
- coverage: |
set -a +x
cd activitystreams && make coverage
GIT_SHA=$(git rev-parse --verify HEAD)
GIT_BRANCH=$(git name-rev --name-only HEAD)
source ~/.code-cov.sh
curl -X POST \
--data-binary @activitystreams.coverprofile \
-H 'Accept: application/json' \
"https://codecov.io/upload/v2?commit=${GIT_SHA}&token=${STREAMS_TOKEN}&branch=${GIT_BRANCH}&service=custom" || true

14
streams/.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
streams/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Marius Orcsik
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.

18
streams/Makefile Normal file
View file

@ -0,0 +1,18 @@
TEST := go test
TEST_FLAGS ?= -v
TEST_TARGET ?= ./...
GO111MODULE=on
PROJECT_NAME := $(shell basename $(PWD))
.PHONY: test coverage clean
test:
$(TEST) $(TEST_FLAGS) $(TEST_TARGET)
coverage: TEST_TARGET := .
coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile
coverage: test
clean:
$(RM) -v *.coverprofile

17
streams/README.md Normal file
View file

@ -0,0 +1,17 @@
# Activity Streams vocabulary for Go
[![MIT Licensed](https://img.shields.io/github/license/go-ap/activitystreams.svg)](https://raw.githubusercontent.com/go-ap/activitystreams/master/LICENSE)
[![Build Status](https://builds.sr.ht/~mariusor/activitystreams.svg)](https://builds.sr.ht/~mariusor/activitystreams)
[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/activitystreams.svg)](https://codecov.io/gh/go-ap/activitystreams)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/activitystreams)](https://goreportcard.com/report/github.com/go-ap/activitystreams)
<!-- [![Codacy Badge](https://api.codacy.com/project/badge/Grade/29664f7ae6c643bca76700143e912cd3)](https://www.codacy.com/app/go-ap/activitystreams/dashboard) -->
Basic package for using [activity pub](https://www.w3.org/TR/activitypub/#Overview) API in Go.
Please see issue [#1](https://github.com/go-ap/activitypub.go/issues/1) before considering this project for real life applications.
## Usage
```go
import "github.com/go-ap/activitystreams"
```

736
streams/activity.go Normal file
View file

@ -0,0 +1,736 @@
package activitystreams
import (
"errors"
"time"
"unsafe"
)
// 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"
)
type ActivityVocabularyTypes []ActivityVocabularyType
func (a ActivityVocabularyTypes) Contains(typ ActivityVocabularyType) bool {
for _, v := range a {
if v == typ {
return true
}
}
return false
}
// ContentManagementActivityTypes use case primarily deals with activities that involve the creation, modification or deletion of content.
// 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.
// 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.
// 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.
var EventRSVPActivityTypes = ActivityVocabularyTypes{
AcceptType,
IgnoreType,
InviteType,
RejectType,
TentativeAcceptType,
TentativeRejectType,
}
// GroupManagementActivityTypes use case primarily deals with management of groups.
// 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.
// 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.
// 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.
var NotificationActivityTypes = ActivityVocabularyTypes{
AnnounceType,
}
// QuestionActivityTypes use case primarily deals with representing inquiries of any type.
// See 5.4 Representing Questions for more information.
var QuestionActivityTypes = ActivityVocabularyTypes{
QuestionType,
}
// RelationshipManagementActivityTypes use case primarily deals with representing activities involving the management of interpersonal and social relationships
// (e.g. friend requests, management of social network, etc). See 5.2 Representing Relationships Between Entities for more information.
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.
// See 5.5 Inverse Activities and "Undo" for more information.
var NegatingActivityTypes = ActivityVocabularyTypes{
UndoType,
}
// OffersActivityTypes use case deals with activities involving offering one object to another.
// 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
type HasRecipients interface {
// Recipients is a method that should do a recipients de-duplication step and then return
// the remaining recipients
Recipients() ItemCollection
Clean()
}
// 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 ObjectID `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 LinkOrIRI `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"`
// Actor 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 ObjectID corresponding to the Activity object
func (a Activity) GetID() ObjectID {
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
}
// Recipients performs recipient de-duplication on the Activity's To, Bto, CC and BCC properties
func (a *Activity) Recipients() ItemCollection {
actor := make(ItemCollection, 0)
actor.Append(a.Actor)
rec, _ := ItemCollectionDeduplication(&actor, &a.To, &a.Bto, &a.CC, &a.BCC, &a.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (a *Activity) Clean() {
a.BCC = nil
a.Bto = nil
}
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 ObjectID, ob Item) *Accept {
a := ActivityNew(id, AcceptType, ob)
o := Accept(*a)
return &o
}
// AddNew initializes an Add activity
func AddNew(id ObjectID, ob Item, trgt Item) *Add {
a := ActivityNew(id, AddType, ob)
o := Add(*a)
o.Target = trgt
return &o
}
// AnnounceNew initializes an Announce activity
func AnnounceNew(id ObjectID, ob Item) *Announce {
a := ActivityNew(id, AnnounceType, ob)
o := Announce(*a)
return &o
}
// BlockNew initializes a Block activity
func BlockNew(id ObjectID, ob Item) *Block {
a := ActivityNew(id, BlockType, ob)
o := Block(*a)
return &o
}
// CreateNew initializes a Create activity
func CreateNew(id ObjectID, ob Item) *Create {
a := ActivityNew(id, CreateType, ob)
o := Create(*a)
return &o
}
// DeleteNew initializes a Delete activity
func DeleteNew(id ObjectID, ob Item) *Delete {
a := ActivityNew(id, DeleteType, ob)
o := Delete(*a)
return &o
}
// DislikeNew initializes a Dislike activity
func DislikeNew(id ObjectID, ob Item) *Dislike {
a := ActivityNew(id, DislikeType, ob)
o := Dislike(*a)
return &o
}
// FlagNew initializes a Flag activity
func FlagNew(id ObjectID, ob Item) *Flag {
a := ActivityNew(id, FlagType, ob)
o := Flag(*a)
return &o
}
// FollowNew initializes a Follow activity
func FollowNew(id ObjectID, ob Item) *Follow {
a := ActivityNew(id, FollowType, ob)
o := Follow(*a)
return &o
}
// IgnoreNew initializes an Ignore activity
func IgnoreNew(id ObjectID, ob Item) *Ignore {
a := ActivityNew(id, IgnoreType, ob)
o := Ignore(*a)
return &o
}
// InviteNew initializes an Invite activity
func InviteNew(id ObjectID, ob Item) *Invite {
a := ActivityNew(id, InviteType, ob)
o := Invite(*a)
return &o
}
// JoinNew initializes a Join activity
func JoinNew(id ObjectID, ob Item) *Join {
a := ActivityNew(id, JoinType, ob)
o := Join(*a)
return &o
}
// LeaveNew initializes a Leave activity
func LeaveNew(id ObjectID, ob Item) *Leave {
a := ActivityNew(id, LeaveType, ob)
o := Leave(*a)
return &o
}
// LikeNew initializes a Like activity
func LikeNew(id ObjectID, ob Item) *Like {
a := ActivityNew(id, LikeType, ob)
o := Like(*a)
return &o
}
// ListenNew initializes a Listen activity
func ListenNew(id ObjectID, ob Item) *Listen {
a := ActivityNew(id, ListenType, ob)
o := Listen(*a)
return &o
}
// MoveNew initializes a Move activity
func MoveNew(id ObjectID, ob Item) *Move {
a := ActivityNew(id, MoveType, ob)
o := Move(*a)
return &o
}
// OfferNew initializes an Offer activity
func OfferNew(id ObjectID, ob Item) *Offer {
a := ActivityNew(id, OfferType, ob)
o := Offer(*a)
return &o
}
// RejectNew initializes a Reject activity
func RejectNew(id ObjectID, ob Item) *Reject {
a := ActivityNew(id, RejectType, ob)
o := Reject(*a)
return &o
}
// ReadNew initializes a Read activity
func ReadNew(id ObjectID, ob Item) *Read {
a := ActivityNew(id, ReadType, ob)
o := Read(*a)
return &o
}
// RemoveNew initializes a Remove activity
func RemoveNew(id ObjectID, ob Item, trgt Item) *Remove {
a := ActivityNew(id, RemoveType, ob)
o := Remove(*a)
o.Target = trgt
return &o
}
// TentativeRejectNew initializes a TentativeReject activity
func TentativeRejectNew(id ObjectID, ob Item) *TentativeReject {
a := ActivityNew(id, TentativeRejectType, ob)
o := TentativeReject(*a)
return &o
}
// TentativeAcceptNew initializes a TentativeAccept activity
func TentativeAcceptNew(id ObjectID, ob Item) *TentativeAccept {
a := ActivityNew(id, TentativeAcceptType, ob)
o := TentativeAccept(*a)
return &o
}
// UndoNew initializes an Undo activity
func UndoNew(id ObjectID, ob Item) *Undo {
a := ActivityNew(id, UndoType, ob)
o := Undo(*a)
return &o
}
// UpdateNew initializes an Update activity
func UpdateNew(id ObjectID, ob Item) *Update {
a := ActivityNew(id, UpdateType, ob)
u := Update(*a)
return &u
}
// ViewNew initializes a View activity
func ViewNew(id ObjectID, ob Item) *View {
a := ActivityNew(id, ViewType, ob)
o := View(*a)
return &o
}
// ActivityNew initializes a basic activity
func ActivityNew(id ObjectID, 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
func (a *Activity) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
a.ID = JSONGetObjectID(data)
a.Type = JSONGetType(data)
a.Name = JSONGetNaturalLanguageField(data, "name")
a.Content = JSONGetNaturalLanguageField(data, "content")
a.Summary = JSONGetNaturalLanguageField(data, "summary")
a.Context = JSONGetItem(data, "context")
a.URL = JSONGetURIItem(data, "url")
a.MediaType = MimeType(JSONGetString(data, "mediaType"))
a.Generator = JSONGetItem(data, "generator")
a.AttributedTo = JSONGetItem(data, "attributedTo")
a.Attachment = JSONGetItem(data, "attachment")
a.Location = JSONGetItem(data, "location")
a.Published = JSONGetTime(data, "published")
a.StartTime = JSONGetTime(data, "startTime")
a.EndTime = JSONGetTime(data, "endTime")
a.Duration = JSONGetDuration(data, "duration")
a.Icon = JSONGetItem(data, "icon")
a.Preview = JSONGetItem(data, "preview")
a.Image = JSONGetItem(data, "image")
a.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
a.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
a.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
a.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
a.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
a.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
a.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
a.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
a.Tag = tag
}
a.Actor = JSONGetItem(data, "actor")
a.Target = JSONGetItem(data, "target")
a.Instrument = JSONGetItem(data, "instrument")
a.Origin = JSONGetItem(data, "origin")
a.Result = JSONGetItem(data, "result")
a.Object = JSONGetItem(data, "object")
return nil
}
// ToActivity
func ToActivity(it Item) (*Activity, error) {
switch i := it.(type) {
case *Activity:
return i, nil
case Activity:
return &i, nil
case *IntransitiveActivity:
return (*Activity)(unsafe.Pointer(i)), nil
case IntransitiveActivity:
return (*Activity)(unsafe.Pointer(&i)), nil
case *Question:
return (*Activity)(unsafe.Pointer(i)), nil
case Question:
return (*Activity)(unsafe.Pointer(&i)), nil
}
return nil, errors.New("unable to convert activity")
}
// FlattenActivityProperties flattens the Activity's properties from Object type to IRI
func FlattenActivityProperties(act *Activity) *Activity {
act.Object = Flatten(act.Object)
act.Actor = Flatten(act.Actor)
act.Target = Flatten(act.Target)
act.Result = Flatten(act.Result)
act.Origin = Flatten(act.Origin)
act.Result = Flatten(act.Result)
act.Instrument = Flatten(act.Instrument)
return act
}

971
streams/activity_test.go Normal file
View file

@ -0,0 +1,971 @@
package activitystreams
import (
"fmt"
"testing"
)
func TestActivityNew(t *testing.T) {
var testValue = ObjectID("test")
var testType ActivityVocabularyType = "Accept"
a := ActivityNew(testValue, testType, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != testType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, testType)
}
g := ActivityNew(testValue, "", nil)
if g.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", g.ID, testValue)
}
if g.Type != ActivityType {
t.Errorf("Activity Type '%v' different than expected '%v'", g.Type, ActivityType)
}
}
func TestAcceptNew(t *testing.T) {
var testValue = ObjectID("test")
a := AcceptNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != AcceptType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, AcceptType)
}
}
func TestAddNew(t *testing.T) {
var testValue = ObjectID("test")
a := AddNew(testValue, nil, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != AddType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, AddType)
}
}
func TestAnnounceNew(t *testing.T) {
var testValue = ObjectID("test")
a := AnnounceNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != AnnounceType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, AnnounceType)
}
}
func TestBlockNew(t *testing.T) {
var testValue = ObjectID("test")
a := BlockNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != BlockType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, BlockType)
}
}
func TestCreateNew(t *testing.T) {
var testValue = ObjectID("test")
a := CreateNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != CreateType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, CreateType)
}
}
func TestDeleteNew(t *testing.T) {
var testValue = ObjectID("test")
a := DeleteNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != DeleteType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, DeleteType)
}
}
func TestDislikeNew(t *testing.T) {
var testValue = ObjectID("test")
a := DislikeNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != DislikeType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, DislikeType)
}
}
func TestFlagNew(t *testing.T) {
var testValue = ObjectID("test")
a := FlagNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != FlagType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, FlagType)
}
}
func TestFollowNew(t *testing.T) {
var testValue = ObjectID("test")
a := FollowNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != FollowType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, FollowType)
}
}
func TestIgnoreNew(t *testing.T) {
var testValue = ObjectID("test")
a := IgnoreNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != IgnoreType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, IgnoreType)
}
}
func TestInviteNew(t *testing.T) {
var testValue = ObjectID("test")
a := InviteNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != InviteType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, InviteType)
}
}
func TestJoinNew(t *testing.T) {
var testValue = ObjectID("test")
a := JoinNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != JoinType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, JoinType)
}
}
func TestLeaveNew(t *testing.T) {
var testValue = ObjectID("test")
a := LeaveNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != LeaveType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, LeaveType)
}
}
func TestLikeNew(t *testing.T) {
var testValue = ObjectID("test")
a := LikeNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != LikeType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, LikeType)
}
}
func TestListenNew(t *testing.T) {
var testValue = ObjectID("test")
a := ListenNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != ListenType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, ListenType)
}
}
func TestMoveNew(t *testing.T) {
var testValue = ObjectID("test")
a := MoveNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != MoveType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, MoveType)
}
}
func TestOfferNew(t *testing.T) {
var testValue = ObjectID("test")
a := OfferNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != OfferType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, OfferType)
}
}
func TestRejectNew(t *testing.T) {
var testValue = ObjectID("test")
a := RejectNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != RejectType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, RejectType)
}
}
func TestReadNew(t *testing.T) {
var testValue = ObjectID("test")
a := ReadNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != ReadType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, ReadType)
}
}
func TestRemoveNew(t *testing.T) {
var testValue = ObjectID("test")
a := RemoveNew(testValue, nil, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != RemoveType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, RemoveType)
}
}
func TestTentativeRejectNew(t *testing.T) {
var testValue = ObjectID("test")
a := TentativeRejectNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != TentativeRejectType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, TentativeRejectType)
}
}
func TestTentativeAcceptNew(t *testing.T) {
var testValue = ObjectID("test")
a := TentativeAcceptNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != TentativeAcceptType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, TentativeAcceptType)
}
}
func TestUndoNew(t *testing.T) {
var testValue = ObjectID("test")
a := UndoNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != UndoType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, UndoType)
}
}
func TestUpdateNew(t *testing.T) {
var testValue = ObjectID("test")
a := UpdateNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != UpdateType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, UpdateType)
}
}
func TestViewNew(t *testing.T) {
var testValue = ObjectID("test")
a := ViewNew(testValue, nil)
if a.ID != testValue {
t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue)
}
if a.Type != ViewType {
t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, ViewType)
}
}
func TestActivityRecipients(t *testing.T) {
bob := PersonNew("bob")
alice := PersonNew("alice")
foo := OrganizationNew("foo")
bar := GroupNew("bar")
a := ActivityNew("t", "test", nil)
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) != 8 {
t.Errorf("%T.To should have exactly 8(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))
}
}
func TestBlockRecipients(t *testing.T) {
bob := PersonNew("bob")
alice := PersonNew("alice")
foo := OrganizationNew("foo")
bar := GroupNew("bar")
a := BlockNew("bbb", bob)
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) != 8 {
t.Errorf("%T.To should have exactly 8(eight) elements, not %d", a, len(a.To))
}
a.Recipients()
if len(a.To) != 4 {
t.Errorf("%T.To should have exactly 3(four) elements, not %d", a, len(a.To))
}
b := BlockNew("t", bob)
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([]ObjectID, 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 TestCreate_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
c := CreateNew("act", o)
c.To.Append(to)
c.CC.Append(cc)
c.BCC.Append(cc)
c.Recipients()
var err error
recIds := make([]ObjectID, 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 TestDislike_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
d := DislikeNew("act", o)
d.To.Append(to)
d.CC.Append(cc)
d.BCC.Append(cc)
d.Recipients()
var err error
recIds := make([]ObjectID, 0)
err = checkDedup(d.To, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(d.Bto, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(d.CC, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(d.BCC, &recIds)
if err != nil {
t.Error(err)
}
}
func TestLike_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
l := LikeNew("act", o)
l.To.Append(to)
l.CC.Append(cc)
l.BCC.Append(cc)
l.Recipients()
var err error
recIds := make([]ObjectID, 0)
err = checkDedup(l.To, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(l.Bto, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(l.CC, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(l.BCC, &recIds)
if err != nil {
t.Error(err)
}
}
func TestUpdate_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
u := UpdateNew("act", o)
u.To.Append(to)
u.CC.Append(cc)
u.BCC.Append(cc)
u.Recipients()
var err error
recIds := make([]ObjectID, 0)
err = checkDedup(u.To, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(u.Bto, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(u.CC, &recIds)
if err != nil {
t.Error(err)
}
err = checkDedup(u.BCC, &recIds)
if err != nil {
t.Error(err)
}
}
func TestActivity_GetID(t *testing.T) {
a := ActivityNew("test", ActivityType, Person{})
if a.GetID() != "test" {
t.Errorf("%T should return an empty %T object. Received %#v", a, a.GetID(), a.GetID())
}
}
func TestActivity_GetIDGetType(t *testing.T) {
a := ActivityNew("test", ActivityType, Person{})
if a.GetID() != "test" || a.GetType() != ActivityType {
t.Errorf("%T should not return an empty %T object. Received %#v", a, a.GetID(), a.GetID())
}
}
func TestActivity_IsLink(t *testing.T) {
a := ActivityNew("test", ActivityType, Person{})
if a.IsLink() {
t.Errorf("%T should not respond true to IsLink", a)
}
}
func TestActivity_IsObject(t *testing.T) {
a := ActivityNew("test", ActivityType, Person{})
if !a.IsObject() {
t.Errorf("%T should respond true to IsObject", a)
}
}
func checkDedup(list ItemCollection, recIds *[]ObjectID) 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
}
func TestActivity_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
c := ActivityNew("act", ActivityType, o)
c.To.Append(to)
c.CC.Append(cc)
c.BCC.Append(cc)
c.Recipients()
var err error
recIds := make([]ObjectID, 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 TestBlock_Recipients(t *testing.T) {
to := PersonNew("bob")
o := ObjectNew(ArticleType)
cc := PersonNew("alice")
o.ID = "something"
b := BlockNew("act", o)
b.To.Append(to)
b.CC.Append(cc)
b.BCC.Append(cc)
b.Recipients()
var err error
recIds := make([]ObjectID, 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 TestActivity_UnmarshalJSON(t *testing.T) {
a := Activity{}
dataEmpty := []byte("{}")
a.UnmarshalJSON(dataEmpty)
if a.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", a, a.ID)
}
if a.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", a, a.Type)
}
if a.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", a, a.AttributedTo)
}
if len(a.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", a, a.Name)
}
if len(a.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", a, a.Summary)
}
if len(a.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", a, a.Content)
}
if a.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", a, a.URL)
}
if !a.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", a, a.Published)
}
if !a.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", a, a.StartTime)
}
if !a.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", a, a.Updated)
}
}
func TestCreate_UnmarshalJSON(t *testing.T) {
c := Create{}
dataEmpty := []byte("{}")
c.UnmarshalJSON(dataEmpty)
if c.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", c, c.ID)
}
if c.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", c, c.Type)
}
if c.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", c, c.AttributedTo)
}
if len(c.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", c, c.Name)
}
if len(c.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", c, c.Summary)
}
if len(c.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", c, c.Content)
}
if c.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", c, c.URL)
}
if !c.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", c, c.Published)
}
if !c.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", c, c.StartTime)
}
if !c.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", c, c.Updated)
}
}
func TestDislike_UnmarshalJSON(t *testing.T) {
d := Dislike{}
dataEmpty := []byte("{}")
d.UnmarshalJSON(dataEmpty)
if d.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", d, d.ID)
}
if d.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", d, d.Type)
}
if d.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", d, d.AttributedTo)
}
if len(d.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", d, d.Name)
}
if len(d.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", d, d.Summary)
}
if len(d.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", d, d.Content)
}
if d.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", d, d.URL)
}
if !d.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", d, d.Published)
}
if !d.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", d, d.StartTime)
}
if !d.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", d, d.Updated)
}
}
func TestLike_UnmarshalJSON(t *testing.T) {
l := Like{}
dataEmpty := []byte("{}")
l.UnmarshalJSON(dataEmpty)
if l.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", l, l.ID)
}
if l.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", l, l.Type)
}
if l.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", l, l.AttributedTo)
}
if len(l.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", l, l.Name)
}
if len(l.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", l, l.Summary)
}
if len(l.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", l, l.Content)
}
if l.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", l, l.URL)
}
if !l.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", l, l.Published)
}
if !l.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", l, l.StartTime)
}
if !l.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", l, l.Updated)
}
}
func TestUpdate_UnmarshalJSON(t *testing.T) {
u := Update{}
dataEmpty := []byte("{}")
u.UnmarshalJSON(dataEmpty)
if u.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", u, u.ID)
}
if u.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", u, u.Type)
}
if u.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", u, u.AttributedTo)
}
if len(u.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", u, u.Name)
}
if len(u.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", u, u.Summary)
}
if len(u.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", u, u.Content)
}
if u.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", u, u.URL)
}
if !u.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", u, u.Published)
}
if !u.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", u, u.StartTime)
}
if !u.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", u, u.Updated)
}
}
func TestToActivity(t *testing.T) {
var it Item
act := ActivityNew(ObjectID("test"), CreateType, nil)
it = act
a, err := ToActivity(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 := ToActivity(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 TestFlattenActivityProperties(t *testing.T) {
t.Skipf("TODO")
}
func TestValidEventRSVPActivityType(t *testing.T) {
t.Skipf("TODO")
}
func TestValidGroupManagementActivityType(t *testing.T) {
t.Skipf("TODO")
}
func TestActivity_Clean(t *testing.T) {
t.Skipf("TODO")
}
func TestActivity_IsCollection(t *testing.T) {
t.Skipf("TODO")
}

90
streams/actors.go Normal file
View file

@ -0,0 +1,90 @@
package activitystreams
// Actor Types
const (
ApplicationType ActivityVocabularyType = "Application"
GroupType ActivityVocabularyType = "Group"
OrganizationType ActivityVocabularyType = "Organization"
PersonType ActivityVocabularyType = "Person"
ServiceType ActivityVocabularyType = "Service"
)
var ActorTypes = ActivityVocabularyTypes{
ApplicationType,
GroupType,
OrganizationType,
PersonType,
ServiceType,
}
// 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 Item
type (
// Application describes a software application.
Application = Object
// Group represents a formal or informal collective of Actors.
Group = Object
// Organization represents an organization.
Organization = Object
// Person represents an individual person.
Person = Object
// Service represents a service of any kind.
Service = Object
)
// ActorNew initializes an Actor type actor
func ActorNew(id ObjectID, typ ActivityVocabularyType) *Object {
if !ActorTypes.Contains(typ) {
typ = ActorType
}
a := Object{ID: id, Type: typ}
a.Name = NaturalLanguageValuesNew()
a.Content = NaturalLanguageValuesNew()
a.Summary = NaturalLanguageValuesNew()
return &a
}
// ApplicationNew initializes an Application type actor
func ApplicationNew(id ObjectID) *Application {
a := ActorNew(id, ApplicationType)
o := Application(*a)
return &o
}
// GroupNew initializes a Group type actor
func GroupNew(id ObjectID) *Group {
a := ActorNew(id, GroupType)
o := Group(*a)
return &o
}
// OrganizationNew initializes an Organization type actor
func OrganizationNew(id ObjectID) *Organization {
a := ActorNew(id, OrganizationType)
o := Organization(*a)
return &o
}
// PersonNew initializes a Person type actor
func PersonNew(id ObjectID) *Person {
a := ActorNew(id, PersonType)
o := Person(*a)
return &o
}
// ServiceNew initializes a Service type actor
func ServiceNew(id ObjectID) *Service {
a := ActorNew(id, ServiceType)
o := Service(*a)
return &o
}

335
streams/actors_test.go Normal file
View file

@ -0,0 +1,335 @@
package activitystreams
import (
"reflect"
"testing"
)
func TestActorNew(t *testing.T) {
var testValue = ObjectID("test")
var 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) {
var testValue = ObjectID("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) {
var testValue = ObjectID("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) {
var testValue = ObjectID("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) {
var testValue = ObjectID("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) {
var testValue = ObjectID("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(ObjectID(""), 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")
}

271
streams/collection_page.go Normal file
View file

@ -0,0 +1,271 @@
package activitystreams
import (
"errors"
"time"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// 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 preceeding 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 ObjectID corresponding to the CollectionPage object
func (c CollectionPage) GetID() ObjectID {
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.TotalItems > 0 {
return c.TotalItems
}
return uint(len(c.Items))
}
// Append adds an element to a CollectionPage
func (c *CollectionPage) Append(ob Item) error {
c.Items = append(c.Items, ob)
return nil
}
// Contains verifies if CollectionPage array contains the received one
func (c CollectionPage) Contains(r IRI) bool {
if len(c.Items) == 0 {
return false
}
for _, iri := range c.Items {
if r.Equals(iri.GetLink(), false) {
return true
}
}
return false
}
// UnmarshalJSON
func (c *CollectionPage) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
c.ID = JSONGetObjectID(data)
c.Type = JSONGetType(data)
c.Name = JSONGetNaturalLanguageField(data, "name")
c.Content = JSONGetNaturalLanguageField(data, "content")
c.Summary = JSONGetNaturalLanguageField(data, "summary")
c.Context = JSONGetItem(data, "context")
c.URL = JSONGetURIItem(data, "url")
c.MediaType = MimeType(JSONGetString(data, "mediaType"))
c.Generator = JSONGetItem(data, "generator")
c.AttributedTo = JSONGetItem(data, "attributedTo")
c.Attachment = JSONGetItem(data, "attachment")
c.Location = JSONGetItem(data, "location")
c.Published = JSONGetTime(data, "published")
c.StartTime = JSONGetTime(data, "startTime")
c.EndTime = JSONGetTime(data, "endTime")
c.Duration = JSONGetDuration(data, "duration")
c.Icon = JSONGetItem(data, "icon")
c.Preview = JSONGetItem(data, "preview")
c.Image = JSONGetItem(data, "image")
c.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
c.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
c.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
c.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
c.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
c.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
c.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
c.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
c.Tag = tag
}
c.TotalItems = uint(JSONGetInt(data, "totalItems"))
c.Items = JSONGetItems(data, "items")
c.Current = JSONGetItem(data, "current")
c.First = JSONGetItem(data, "first")
c.Last = JSONGetItem(data, "last")
c.Next = JSONGetItem(data, "next")
c.Prev = JSONGetItem(data, "prev")
c.PartOf = JSONGetItem(data, "partOf")
return nil
}
// 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.ID = c.ID
return nil
}
// ToCollectionPage
func ToCollectionPage(it Item) (*CollectionPage, error) {
switch i := it.(type) {
case *CollectionPage:
return i, nil
case CollectionPage:
return &i, nil
}
return nil, errors.New("unable to convert to collection page")
}

View file

@ -0,0 +1,145 @@
package activitystreams
import (
"reflect"
"testing"
)
func TestCollectionPageNew(t *testing.T) {
var testValue = ObjectID("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 := ObjectID("test")
val := Object{ID: ObjectID("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 := ObjectID("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 := ObjectID("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) {
t.Skipf("TODO")
}
func TestCollectionPage_Contains(t *testing.T) {
t.Skipf("TODO")
}

312
streams/collections.go Normal file
View file

@ -0,0 +1,312 @@
package activitystreams
import (
"errors"
"time"
)
const CollectionOfItems ActivityVocabularyType = "ItemCollection"
var CollectionTypes = ActivityVocabularyTypes{
CollectionOfItems,
CollectionType,
OrderedCollectionType,
CollectionPageType,
OrderedCollectionPageType,
}
type CollectionInterface interface {
ObjectOrLink
Collection() ItemCollection
Append(ob Item) error
Count() uint
Contains(IRI) 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 ObjectID `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 LinkOrIRI `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"`
// 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 preceeding 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"`
}
// CollectionNew initializes a new Collection
func CollectionNew(id ObjectID) *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 ObjectID) *OrderedCollection {
o := OrderedCollection{ID: id, Type: OrderedCollectionType}
o.Name = NaturalLanguageValuesNew()
o.Content = NaturalLanguageValuesNew()
return &o
}
// GetID returns the ObjectID corresponding to the Collection object
func (c Collection) GetID() ObjectID {
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(ob Item) error {
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.TotalItems > 0 {
return c.TotalItems
}
return uint(len(c.Items))
}
// Contains verifies if Collection array contains the received one
func (c Collection) Contains(r IRI) bool {
if len(c.Items) == 0 {
return false
}
for _, iri := range c.Items {
if r.Equals(iri.GetLink(), false) {
return true
}
}
return false
}
// UnmarshalJSON
func (c *Collection) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
c.ID = JSONGetObjectID(data)
c.Type = JSONGetType(data)
c.Name = JSONGetNaturalLanguageField(data, "name")
c.Content = JSONGetNaturalLanguageField(data, "content")
c.Summary = JSONGetNaturalLanguageField(data, "summary")
c.Context = JSONGetItem(data, "context")
c.URL = JSONGetURIItem(data, "url")
c.MediaType = MimeType(JSONGetString(data, "mediaType"))
c.Generator = JSONGetItem(data, "generator")
c.AttributedTo = JSONGetItem(data, "attributedTo")
c.Attachment = JSONGetItem(data, "attachment")
c.Location = JSONGetItem(data, "location")
c.Published = JSONGetTime(data, "published")
c.StartTime = JSONGetTime(data, "startTime")
c.EndTime = JSONGetTime(data, "endTime")
c.Duration = JSONGetDuration(data, "duration")
c.Icon = JSONGetItem(data, "icon")
c.Preview = JSONGetItem(data, "preview")
c.Image = JSONGetItem(data, "image")
c.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
c.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
c.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
c.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
c.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
c.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
c.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
c.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
c.Tag = tag
}
c.TotalItems = uint(JSONGetInt(data, "totalItems"))
c.Items = JSONGetItems(data, "items")
c.Current = JSONGetItem(data, "current")
c.First = JSONGetItem(data, "first")
c.Last = JSONGetItem(data, "last")
return nil
}
// Flatten checks if Item can be flatten to an IRI or array of IRIs and returns it if so
func Flatten(it Item) Item {
if it.IsCollection() {
if c, ok := it.(CollectionInterface); ok {
it = FlattenItemCollection(c.Collection())
}
}
if it != nil && len(it.GetLink()) > 0 {
return it.GetLink()
}
return it
}
// FlattenItemCollection flattens the Collection's properties from Object type to IRI
func FlattenItemCollection(c ItemCollection) ItemCollection {
if c != nil && len(c) > 0 {
for i, it := range c {
c[i] = FlattenToIRI(it)
}
}
return c
}
// ToItemCollection
func ToItemCollection(it Item) (*ItemCollection, error) {
switch i := it.(type) {
case *ItemCollection:
return i, nil
case ItemCollection:
return &i, nil
}
return nil, errors.New("unable to convert to item collection")
}
// ToCollection
func ToCollection(it Item) (*Collection, error) {
switch i := it.(type) {
case *Collection:
return i, nil
case Collection:
return &i, nil
}
return nil, errors.New("unable to convert to collection")
}

172
streams/collections_test.go Normal file
View file

@ -0,0 +1,172 @@
package activitystreams
import (
"reflect"
"testing"
)
func TestCollectionNew(t *testing.T) {
var testValue = ObjectID("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 := ObjectID("test")
val := Object{ID: ObjectID("grrr")}
c := CollectionNew(id)
c.Append(val)
if c.Count() != 1 {
t.Errorf("Inbox collection 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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 TestToCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestCollection_Contains(t *testing.T) {
t.Skipf("TODO")
}

8
streams/go.mod Normal file
View file

@ -0,0 +1,8 @@
module github.com/go-ap/activitystreams
go 1.13
require (
github.com/buger/jsonparser v0.0.0-20181023193515-52c6e1462ebd
github.com/go-ap/jsonld v0.0.0-20191123195936-1e43eac08b0c
)

View file

@ -0,0 +1,278 @@
package activitystreams
import (
"errors"
"time"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// Actor 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 Actor `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 Activity's To, Bto, CC and BCC properties
func (i *IntransitiveActivity) Recipients() ItemCollection {
actor := make(ItemCollection, 0)
actor.Append(i.Actor)
rec, _ := ItemCollectionDeduplication(&actor, &i.To, &i.Bto, &i.CC, &i.BCC, &i.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (i *IntransitiveActivity) Clean() {
i.BCC = nil
i.Bto = 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 ObjectID corresponding to the IntransitiveActivity object
func (i IntransitiveActivity) GetID() ObjectID {
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
func (i *IntransitiveActivity) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
i.ID = JSONGetObjectID(data)
i.Type = JSONGetType(data)
i.Name = JSONGetNaturalLanguageField(data, "name")
i.Content = JSONGetNaturalLanguageField(data, "content")
i.Summary = JSONGetNaturalLanguageField(data, "summary")
i.Context = JSONGetItem(data, "context")
i.URL = JSONGetURIItem(data, "url")
i.MediaType = MimeType(JSONGetString(data, "mediaType"))
i.Generator = JSONGetItem(data, "generator")
i.AttributedTo = JSONGetItem(data, "attributedTo")
i.Attachment = JSONGetItem(data, "attachment")
i.Location = JSONGetItem(data, "location")
i.Published = JSONGetTime(data, "published")
i.StartTime = JSONGetTime(data, "startTime")
i.EndTime = JSONGetTime(data, "endTime")
i.Duration = JSONGetDuration(data, "duration")
i.Icon = JSONGetItem(data, "icon")
i.Preview = JSONGetItem(data, "preview")
i.Image = JSONGetItem(data, "image")
i.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
i.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
i.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
i.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
i.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
i.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
i.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
i.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
i.Tag = tag
}
i.Actor = JSONGetItem(data, "actor")
i.Target = JSONGetItem(data, "target")
i.Result = JSONGetItem(data, "result")
i.Origin = JSONGetItem(data, "origin")
i.Instrument = JSONGetItem(data, "instrument")
return nil
}
// IntransitiveActivityNew initializes a intransitive activity
func IntransitiveActivityNew(id ObjectID, typ ActivityVocabularyType) *IntransitiveActivity {
if !IntransitiveActivityTypes.Contains(typ) {
typ = IntransitiveActivityType
}
i := IntransitiveActivity{ID: id, Type: typ}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
return &i
}
// ToIntransitiveActivity
func ToIntransitiveActivity(it Item) (*IntransitiveActivity, error) {
switch i := it.(type) {
case *IntransitiveActivity:
return i, nil
case IntransitiveActivity:
return &i, nil
}
return nil, errors.New("unable to convert to intransitive activity")
}
// FlattenIntransitiveActivityProperties flattens the IntransitiveActivity's properties from Object type to IRI
func FlattenIntransitiveActivityProperties(act *IntransitiveActivity) *IntransitiveActivity {
act.Actor = Flatten(act.Actor)
act.Target = Flatten(act.Target)
act.Result = Flatten(act.Result)
act.Origin = Flatten(act.Origin)
act.Result = Flatten(act.Result)
act.Instrument = Flatten(act.Instrument)
return act
}
// ArriveNew initializes an Arrive activity
func ArriveNew(id ObjectID) *Arrive {
a := IntransitiveActivityNew(id, ArriveType)
o := Arrive(*a)
return &o
}
// TravelNew initializes a Travel activity
func TravelNew(id ObjectID) *Travel {
a := IntransitiveActivityNew(id, TravelType)
o := Travel(*a)
return &o
}

View file

@ -0,0 +1,264 @@
package activitystreams
import "testing"
func TestIntransitiveActivityNew(t *testing.T) {
var testValue = ObjectID("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) != 8 {
t.Errorf("%T.To should have exactly 8(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([]ObjectID, 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([]ObjectID, 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 TestFlattenIntransitiveActivityProperties(t *testing.T) {
t.Skipf("TODO")
}
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) {
var testValue = ObjectID("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) {
var testValue = ObjectID("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)
}
}

157
streams/iri.go Normal file
View file

@ -0,0 +1,157 @@
package activitystreams
import (
"net/url"
"path"
"strings"
)
const PublicNS = IRI("https://www.w3.org/ns/activitystreams#Public")
type (
// IRI is a Internationalized Resource Identifiers (IRIs) RFC3987
IRI string
IRIs []IRI
)
// 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) {
return url.Parse(i.String())
}
// UnmarshalJSON
func (i *IRI) UnmarshalJSON(s []byte) error {
*i = IRI(strings.Trim(string(s), "\""))
return nil
}
// GetID
func (i IRI) GetID() ObjectID {
return ObjectID(i)
}
// GetType
func (i IRI) GetType() ActivityVocabularyType {
return LinkType
}
// 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 it != nil && it.IsObject() && len(it.GetLink()) > 0 {
return it.GetLink()
}
return it
}
// Contains verifies if IRIs array contains the received one
func (i IRIs) Contains(r IRI) bool {
if len(i) == 0 {
return false
}
for _, iri := range i {
if r.Equals(iri, false) {
return true
}
}
return false
}
// Equals verifies if our receiver IRI is equals with the "with" IRI
// It ignores the protocol
// It tries to use the URL representation if possible and fallback to string comparison if unable to convert
// In URL representation checks hostname in a case insensitive way and the path and
func (i IRI) Equals(with IRI, checkScheme bool) bool {
u, e := i.URL()
uw, ew := with.URL()
if e != nil || ew != nil {
return strings.ToLower(i.String()) == strings.ToLower(with.String())
}
if checkScheme {
if strings.ToLower(u.Scheme) != strings.ToLower(uw.Scheme) {
return false
}
}
if strings.ToLower(u.Host) != strings.ToLower(uw.Host) {
return false
}
if path.Clean(u.Path) != path.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
}
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
}
}
if u.Host != uw.Host {
return false
}
p := u.Path
if p != "" {
p = path.Clean(p)
}
pw := uw.Path
if pw != "" {
pw = path.Clean(pw)
}
return strings.Contains(p, pw)
}

212
streams/iri_test.go Normal file
View file

@ -0,0 +1,212 @@
package activitystreams
import "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 != ObjectID(i) {
t.Errorf("ObjectID %q (%T) should equal %q (%T)", id, id, i, ObjectID(i))
}
}
func TestIRI_GetType(t *testing.T) {
i := IRI("http://example.com")
if i.GetType() != LinkType {
t.Errorf("Invalid type for %T object %s, expected %s", i, i.GetType(), LinkType)
}
}
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() != false {
t.Errorf("%T.IsObject() returned %t, expected %t", i, i.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 TestFlattenToIRI(t *testing.T) {
t.Skipf("TODO")
}
func TestIRI_URL(t *testing.T) {
t.Skipf("TODO")
}
func TestIRIs_Contains(t *testing.T) {
t.Skipf("TODO")
}
func TestIRI_Equals(t *testing.T) {
{
i1 := IRI("http://example.com")
i2 := IRI("http://example.com")
// same host same scheme
if !i1.Equals(i2, true) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/are/mere")
i2 := IRI("http://example.com/ana/are/mere")
// same host, same scheme and same path
if !i1.Equals(i2, true) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com")
i2 := IRI("http://example.com")
// same host different scheme
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/are/mere")
i2 := IRI("https://example.com/ana/are/mere")
// same host, different scheme and same path
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com?ana=mere")
i2 := IRI("http://example.com?ana=mere")
// same host different scheme, same query
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com?ana=mere&foo=bar")
i2 := IRI("http://example.com?foo=bar&ana=mere")
// same host different scheme, same query - different order
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/are/mere?foo=bar&ana=mere")
i2 := IRI("https://example.com/ana/are/mere?ana=mere&foo=bar")
// same host, different scheme and same path, same query different order
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com?ana=mere")
i2 := IRI("http://example.com?ana=mere")
// same host different scheme, same query
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com?ana=mere&foo=bar")
i2 := IRI("http://example.com?foo=bar&ana=mere")
// same host different scheme, same query - different order
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/are/mere?foo=bar&ana=mere")
i2 := IRI("https://example.com/ana/are/mere?ana=mere&foo=bar")
// same host, different scheme and same path, same query different order
if !i1.Equals(i2, false) {
t.Errorf("%s should equal %s", i1, i2)
}
}
///
{
i1 := IRI("http://example.com")
i2 := IRI("https://example.com")
// same host different scheme
if i1.Equals(i2, true) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("http://example1.com")
i2 := IRI("http://example.com")
// different host same scheme
if i1.Equals(i2, true) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/1are/mere")
i2 := IRI("http://example.com/ana/are/mere")
// same host, same scheme and different path
if i1.Equals(i2, true) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com?ana1=mere")
i2 := IRI("http://example.com?ana=mere")
// same host same scheme, different query key
if i1.Equals(i2, false) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com?ana=mere")
i2 := IRI("http://example.com?ana=mere1")
// same host same scheme, different query value
if i1.Equals(i2, false) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("https://example.com?ana=mere&foo=bar")
i2 := IRI("http://example.com?foo=bar1&ana=mere")
// same host different scheme, different query value - different order
if i1.Equals(i2, false) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
{
i1 := IRI("http://example.com/ana/are/mere?foo=bar&ana=mere")
i2 := IRI("https://example.com/ana/are/mere?ana=mere&foo1=bar")
// same host, different scheme and same path, differnt query key different order
if i1.Equals(i2, false) {
t.Errorf("%s should not equal %s", i1, i2)
}
}
}

84
streams/item.go Normal file
View file

@ -0,0 +1,84 @@
package activitystreams
// ItemCollection represents an array of items
type ItemCollection []Item
// Item struct
type Item ObjectOrLink
const EmptyObjectID = ObjectID("")
const EmptyIRI = IRI("")
// GetID returns the ObjectID corresponding to ItemCollection
func (i ItemCollection) GetID() ObjectID {
return EmptyObjectID
}
// 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
}
// Append facilitates adding elements to Item arrays
// and ensures ItemCollection implements the Collection interface
func (i *ItemCollection) Append(o Item) error {
oldLen := len(*i)
d := make(ItemCollection, oldLen+1)
for k, it := range *i {
d[k] = it
}
d[oldLen] = o
*i = d
return nil
}
// Count returns the length of Items in the item collection
func (i *ItemCollection) Count() uint {
return uint(len(*i))
}
// First returns the ObjectID corresponding to ItemCollection
func (i ItemCollection) First() Item {
if len(i) == 0 {
return nil
}
return i[0]
}
// 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 IRI) bool {
if len(i) == 0 {
return false
}
for _, iri := range i {
if r.Equals(iri.GetLink(), false) {
return true
}
}
return false
}

47
streams/item_test.go Normal file
View file

@ -0,0 +1,47 @@
package activitystreams
import "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 TestFlattenItemCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestItemCollection_Count(t *testing.T) {
t.Skipf("TODO")
}
func TestItemCollection_Contains(t *testing.T) {
t.Skipf("TODO")
}

122
streams/link.go Normal file
View file

@ -0,0 +1,122 @@
package activitystreams
var validLinkTypes = [...]ActivityVocabularyType{
MentionType,
}
// 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 ObjectID `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
// ValidLinkType validates a type against the valid link types
func ValidLinkType(typ ActivityVocabularyType) bool {
for _, v := range validLinkTypes {
if v == typ {
return true
}
}
return false
}
// LinkNew initializes a new Link
func LinkNew(id ObjectID, typ ActivityVocabularyType) *Link {
if !ValidLinkType(typ) {
typ = LinkType
}
return &Link{ID: id, Type: typ}
}
// MentionNew initializes a new Mention
func MentionNew(id ObjectID) *Mention {
return &Mention{ID: id, Type: MentionType}
}
// IsLink validates if current Link is a Link
func (l Link) IsLink() bool {
return l.Type == LinkType || ValidLinkType(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 ObjectID corresponding to the Link object
func (l Link) GetID() ObjectID {
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
}
// UnmarshalJSON
func (l *Link) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
l.ID = JSONGetObjectID(data)
l.Type = JSONGetType(data)
l.MediaType = JSONGetMimeType(data)
l.Preview = JSONGetItem(data, "preview")
l.Height = uint(JSONGetInt(data, "height"))
l.Width = uint(JSONGetInt(data, "width"))
l.Name = JSONGetNaturalLanguageField(data, "name")
l.HrefLang = JSONGetLangRefField(data, "hrefLang")
href := JSONGetURIItem(data, "href")
if href != nil && !href.IsObject() {
l.Href = href.GetLink()
}
rel := JSONGetURIItem(data, "rel")
if rel != nil && !rel.IsObject() {
l.Rel = rel.GetLink()
}
//fmt.Printf("%s\n %#v", data, l)
return nil
}

81
streams/link_test.go Normal file
View file

@ -0,0 +1,81 @@
package activitystreams
import (
"testing"
)
func TestLinkNew(t *testing.T) {
var testValue = ObjectID("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 TestValidLinkType(t *testing.T) {
var invalidType ActivityVocabularyType = "RandomType"
if ValidLinkType(LinkType) {
t.Errorf("Generic Link Type '%v' should not be valid", LinkType)
}
if ValidLinkType(invalidType) {
t.Errorf("Link Type '%v' should not be valid", invalidType)
}
for _, validType := range validLinkTypes {
if !ValidLinkType(validType) {
t.Errorf("Link Type '%v' should be valid", validType)
}
}
}
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")
}

746
streams/object.go Normal file
View file

@ -0,0 +1,746 @@
package activitystreams
import (
"bytes"
"fmt"
"github.com/buger/jsonparser"
json "github.com/go-ap/jsonld"
"sort"
"strconv"
"strings"
"time"
"unsafe"
)
// ObjectID designates an 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 dereferenceable 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 ObjectID IRI
const (
// ActivityBaseURI the basic URI for the activity streams namespaces
ActivityBaseURI = IRI("https://www.w3.org/ns/activitystreams")
ObjectType ActivityVocabularyType = "Object"
LinkType ActivityVocabularyType = "Link"
ActivityType ActivityVocabularyType = "Activity"
IntransitiveActivityType ActivityVocabularyType = "IntransitiveActivity"
ActorType ActivityVocabularyType = "Actor"
CollectionType ActivityVocabularyType = "Collection"
OrderedCollectionType ActivityVocabularyType = "OrderedCollection"
CollectionPageType ActivityVocabularyType = "CollectionPage"
OrderedCollectionPageType ActivityVocabularyType = "OrderedCollectionPage"
// Activity Pub Object Types
ArticleType ActivityVocabularyType = "Article"
AudioType ActivityVocabularyType = "Audio"
DocumentType ActivityVocabularyType = "Document"
EventType ActivityVocabularyType = "Event"
ImageType ActivityVocabularyType = "Image"
NoteType ActivityVocabularyType = "Note"
PageType ActivityVocabularyType = "Page"
PlaceType ActivityVocabularyType = "Place"
ProfileType ActivityVocabularyType = "Profile"
RelationshipType ActivityVocabularyType = "Relationship"
TombstoneType ActivityVocabularyType = "Tombstone"
VideoType ActivityVocabularyType = "Video"
// MentionType is a link type for @mentions
MentionType ActivityVocabularyType = "Mention"
)
const (
NilLangRef LangRef = "-"
)
var GenericObjectTypes = ActivityVocabularyTypes{
ActivityType,
IntransitiveActivityType,
ObjectType,
ActorType,
CollectionType,
OrderedCollectionType,
}
var GenericLinkTypes = ActivityVocabularyTypes{
LinkType,
}
var GenericTypes = append(GenericObjectTypes[:], GenericLinkTypes[:]...)
var ObjectTypes = ActivityVocabularyTypes{
ArticleType,
AudioType,
DocumentType,
EventType,
ImageType,
NoteType,
PageType,
PlaceType,
ProfileType,
RelationshipType,
TombstoneType,
VideoType,
}
type (
// ActivityVocabularyType is the data type for an Activity type object
ActivityVocabularyType string
// ActivityObject is a subtype of Object that describes some form of action that may happen,
// is currently happening, or has already happened
ActivityObject interface {
// GetID returns the dereferenceable ActivityStreams object id
GetID() ObjectID
// GetType returns the ActivityStreams type
GetType() ActivityVocabularyType
}
// LinkOrIRI is an interface that Object and Link structs implement, and at the same time
// they are kept disjointed
LinkOrIRI interface {
// GetLink returns the object id in IRI type
GetLink() IRI
}
// Item describes the requirements of an ActivityStreams object
ObjectOrLink interface {
ActivityObject
LinkOrIRI
// IsLink shows if current item represents a Link object or an IRI
IsLink() bool
// IsObject shows if current item represents an ActivityStreams object
IsObject() bool
// IsCollection shows if the current item represents an ItemCollection
IsCollection() bool
}
// Mapper interface allows external objects to implement their own mechanism for loading information
// from an ActivityStreams vocabulary object
Mapper interface {
// FromActivityStreams maps an ActivityStreams object to another struct representation
FromActivityStreams(Item) error
}
// MimeType is the type for representing MIME types in certain ActivityStreams properties
MimeType string
// LangRef is the type for a language reference code, should be an ISO639-1 language specifier.
LangRef string
// LangRefValue is a type for storing per language values
LangRefValue struct {
Ref LangRef
Value string
}
// NaturalLanguageValues is a mapping for multiple language values
NaturalLanguageValues []LangRefValue
)
func NaturalLanguageValuesNew() NaturalLanguageValues {
return make(NaturalLanguageValues, 0)
}
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) string {
for _, val := range n {
if val.Ref == ref {
return val.Value
}
}
return ""
}
// Set sets a language, value pair in a NaturalLanguageValues array
func (n *NaturalLanguageValues) Set(ref LangRef, v string) 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
}
// MarshalJSON serializes the NaturalLanguageValues into JSON
func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) {
if len(n) == 0 {
return json.Marshal(nil)
}
if len(n) == 1 {
v := n[0]
if v.Ref == NilLangRef {
return v.MarshalJSON()
}
}
mm := make(map[LangRef]string)
for _, val := range n {
mm[val.Ref] = val.Value
}
return json.Marshal(mm)
}
// 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
}
// Append is syntactic sugar for resizing the NaturalLanguageValues map
// and appending an element
func (n *NaturalLanguageValues) Append(lang LangRef, value string) error {
var t NaturalLanguageValues
if len(*n) == 0 {
t = make(NaturalLanguageValues, 0)
} else {
t = *n
}
t = append(*n, LangRefValue{lang, value})
*n = t
return nil
}
// Count returns the length of Items in the item collection
func (n *NaturalLanguageValues) Count() uint {
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
}
return fmt.Sprintf("%s[%s]", l.Value, l.Ref)
}
// UnmarshalJSON implements the JsonEncoder interface
func (l *LangRefValue) UnmarshalJSON(data []byte) error {
_, typ, _, err := jsonparser.Get(data)
if err != nil {
l.Ref = NilLangRef
l.Value = string(unescape(data))
return nil
}
switch typ {
case jsonparser.Object:
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
l.Ref = LangRef(key)
l.Value = string(unescape(value))
return err
})
case jsonparser.String:
l.Ref = NilLangRef
l.Value = string(unescape(data))
}
return nil
}
// UnmarshalText implements the TextEncoder interface
func (l *LangRefValue) UnmarshalText(data []byte) error {
return nil
}
// MarshalJSON serializes the LangRefValue into JSON
func (l LangRefValue) MarshalJSON() ([]byte, error) {
buf := bytes.Buffer{}
if l.Ref != NilLangRef {
if l.Value == "" {
return nil, nil
}
buf.WriteByte('"')
buf.WriteString(string(l.Ref))
buf.Write([]byte{'"', ':'})
}
l.Value = string(unescape([]byte(l.Value)))
buf.WriteString(strconv.Quote(l.Value))
return buf.Bytes(), nil
}
// MarshalText serializes the LangRefValue into JSON
func (l LangRefValue) MarshalText() ([]byte, error) {
if l.Ref != NilLangRef && l.Value == "" {
return nil, nil
}
buf := bytes.Buffer{}
buf.WriteString(l.Value)
if l.Ref != NilLangRef {
buf.WriteByte('[')
buf.WriteString(string(l.Ref))
buf.WriteByte(']')
}
return buf.Bytes(), nil
}
// UnmarshalJSON implements the JsonEncoder interface
func (l *LangRef) UnmarshalJSON(data []byte) error {
return l.UnmarshalText(data)
}
// UnmarshalText implements the TextEncoder interface
func (l *LangRef) UnmarshalText(data []byte) error {
*l = LangRef("")
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 unescape(b []byte) []byte {
// FIMXE(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 tries to load the NaturalLanguage array from the incoming json value
func (n *NaturalLanguageValues) UnmarshalJSON(data []byte) error {
val, typ, _, err := jsonparser.Get(data)
if err != nil {
// try our luck if data contains an unquoted string
n.Append(NilLangRef, string(unescape(data)))
return nil
}
switch typ {
case jsonparser.Object:
jsonparser.ObjectEach(data, func(key []byte, val []byte, dataType jsonparser.ValueType, offset int) error {
n.Append(LangRef(key), string(unescape(val)))
return err
})
case jsonparser.String:
n.Append(NilLangRef, string(unescape(val)))
case jsonparser.Array:
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
l := LangRefValue{}
l.UnmarshalJSON(value)
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), string(data[1:len(data)-1]))
}
return nil
}
// Describes an object of any kind.
// The Activity Pub Object type serves as the base type for most of the other kinds of objects defined in the Activity
// Vocabulary, including other Core types such as Activity, IntransitiveActivity, Collection and OrderedCollection.
type Object struct {
// ID provides the globally unique identifier for anActivity Pub Object or Link.
ID ObjectID `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 LinkOrIRI `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"`
}
// ObjectNew initializes a new Object
func ObjectNew(typ ActivityVocabularyType) *Object {
if !(ObjectTypes.Contains(typ)) {
typ = ObjectType
}
o := Object{Type: typ}
o.Name = NaturalLanguageValuesNew()
o.Content = NaturalLanguageValuesNew()
return &o
}
// GetID returns the ObjectID corresponding to the current Object
func (o Object) GetID() ObjectID {
return o.ID
}
// GetLink returns the IRI corresponding to the current Object
func (o Object) GetLink() IRI {
return IRI(o.ID)
}
// GetType returns the type of the current Object
func (o Object) GetType() ActivityVocabularyType {
return o.Type
}
// IsLink validates if currentActivity Pub Object is a Link
func (o Object) IsLink() bool {
return false
}
// IsObject validates if currentActivity Pub Object is an Object
func (o Object) IsObject() bool {
return true
}
// IsCollection returns false for Object objects
func (o Object) IsCollection() bool {
return false
}
// UnmarshalJSON
func (o *Object) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
o.ID = JSONGetObjectID(data)
o.Type = JSONGetType(data)
o.Name = JSONGetNaturalLanguageField(data, "name")
o.Content = JSONGetNaturalLanguageField(data, "content")
o.Summary = JSONGetNaturalLanguageField(data, "summary")
o.Context = JSONGetItem(data, "context")
o.URL = JSONGetURIItem(data, "url")
o.MediaType = MimeType(JSONGetString(data, "mediaType"))
o.Generator = JSONGetItem(data, "generator")
o.AttributedTo = JSONGetItem(data, "attributedTo")
o.Attachment = JSONGetItem(data, "attachment")
o.Location = JSONGetItem(data, "location")
o.Published = JSONGetTime(data, "published")
o.StartTime = JSONGetTime(data, "startTime")
o.EndTime = JSONGetTime(data, "endTime")
o.Duration = JSONGetDuration(data, "duration")
o.Icon = JSONGetItem(data, "icon")
o.Preview = JSONGetItem(data, "preview")
o.Image = JSONGetItem(data, "image")
o.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
o.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
o.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
o.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
o.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
o.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
o.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
o.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
o.Tag = tag
}
return nil
}
// Recipients performs recipient de-duplication on the Object's To, Bto, CC and BCC properties
func (o *Object) Recipients() ItemCollection {
var aud ItemCollection
rec, _ := ItemCollectionDeduplication(&aud, &o.To, &o.Bto, &o.CC, &o.BCC, &o.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (o *Object) Clean() {
o.BCC = nil
o.Bto = nil
}
type (
// Article represents any kind of multi-paragraph written work.
Article = Object
// Audio represents an audio document of any kind.
Audio = Document
// Document represents a document of any kind.
Document = Object
// Event represents any kind of event.
Event = Object
// Image An image document of any kind
Image = Document
// Note represents a short written work typically less than a single paragraph in length.
Note = Object
// Page represents a Web Page.
Page = Document
// Video represents a video document of any kind
Video = Document
)
// ItemCollectionDeduplication normalizes the received arguments lists into a single unified one
func ItemCollectionDeduplication(recCols ...*ItemCollection) (ItemCollection, error) {
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 Item
if cur.IsObject() {
testIt = cur
} else if cur.IsLink() {
testIt = cur.GetLink()
} else {
continue
}
for _, it := range rec {
if testIt == it {
// 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, nil
}
// UnmarshalJSON
func (i *ObjectID) UnmarshalJSON(data []byte) error {
*i = ObjectID(strings.Trim(string(data), "\""))
return nil
}
func (i *ObjectID) IsValid() bool {
return i != nil && len(*i) > 0
}
// UnmarshalJSON
func (c *MimeType) UnmarshalJSON(data []byte) error {
*c = MimeType(strings.Trim(string(data), "\""))
return nil
}
// ToObject returns an Object pointer to the data in the current Item
// It relies on the fact that all the types in this package have a data layout compatible with Object.
func ToObject(it Item) (*Object, error) {
switch i := it.(type) {
case *Object:
return i, nil
case Object:
return &i, nil
case *Place:
return (*Object)(unsafe.Pointer(i)), nil
case Place:
return (*Object)(unsafe.Pointer(&i)), nil
case *Profile:
return (*Object)(unsafe.Pointer(i)), nil
case Profile:
return (*Object)(unsafe.Pointer(&i)), nil
case *Relationship:
return (*Object)(unsafe.Pointer(i)), nil
case Relationship:
return (*Object)(unsafe.Pointer(&i)), nil
case *Tombstone:
return (*Object)(unsafe.Pointer(i)), nil
case Tombstone:
return (*Object)(unsafe.Pointer(&i)), nil
case *Activity:
return (*Object)(unsafe.Pointer(i)), nil
case Activity:
return (*Object)(unsafe.Pointer(&i)), nil
case *IntransitiveActivity:
return (*Object)(unsafe.Pointer(i)), nil
case IntransitiveActivity:
return (*Object)(unsafe.Pointer(&i)), nil
case *Question:
return (*Object)(unsafe.Pointer(i)), nil
case Question:
return (*Object)(unsafe.Pointer(&i)), nil
case *Collection:
return (*Object)(unsafe.Pointer(i)), nil
case Collection:
return (*Object)(unsafe.Pointer(&i)), nil
case *CollectionPage:
return (*Object)(unsafe.Pointer(i)), nil
case CollectionPage:
return (*Object)(unsafe.Pointer(&i)), nil
case *OrderedCollection:
return (*Object)(unsafe.Pointer(i)), nil
case OrderedCollection:
return (*Object)(unsafe.Pointer(&i)), nil
case *OrderedCollectionPage:
return (*Object)(unsafe.Pointer(i)), nil
case OrderedCollectionPage:
return (*Object)(unsafe.Pointer(&i)), nil
}
return nil, fmt.Errorf("unable to convert %q", it.GetType())
}
// FlattenObjectProperties flattens the Object's properties from Object types to IRI
func FlattenObjectProperties(o *Object) *Object {
o.Replies = Flatten(o.Replies)
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 ActivityTypes.Contains(it.GetType()) {
act, err := ToActivity(it)
if err == nil {
return FlattenActivityProperties(act)
}
}
if ActorTypes.Contains(it.GetType()) || ObjectTypes.Contains(it.GetType()) {
ob, err := ToObject(it)
if err == nil {
return FlattenObjectProperties(ob)
}
}
return it
}

931
streams/object_test.go Normal file
View file

@ -0,0 +1,931 @@
package activitystreams
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"testing"
)
func TestObjectNew(t *testing.T) {
var testValue = ObjectID("test")
var testType = ArticleType
o := ObjectNew(testType)
o.ID = testValue
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 := ObjectNew("")
n.ID = testValue
if n.ID != testValue {
t.Errorf("APObject Id '%v' different than expected '%v'", n.ID, testValue)
}
if n.Type != ObjectType {
t.Errorf("APObject Type '%v' different than expected '%v'", n.Type, ObjectType)
}
}
func TestActivityVocabularyTypes_Contains(t *testing.T) {
{
var invalidType ActivityVocabularyType = "RandomType"
if ActivityTypes.Contains(ActivityType) {
t.Errorf("Generic Activity Type '%v' should not be valid", ActivityType)
}
for _, inValidType := range ObjectTypes {
if ActivityTypes.Contains(inValidType) {
t.Errorf("APObject Type '%v' should be invalid", inValidType)
}
}
if ActivityTypes.Contains(invalidType) {
t.Errorf("Activity Type '%v' should not be valid", invalidType)
}
for _, validType := range ActivityTypes {
if !ActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if IntransitiveActivityTypes.Contains(ActivityType) {
t.Errorf("Generic Activity Type '%v' should not be valid", ActivityType)
}
for _, inValidType := range ActivityTypes {
if IntransitiveActivityTypes.Contains(inValidType) {
t.Errorf("APObject Type '%v' should be invalid", inValidType)
}
}
if IntransitiveActivityTypes.Contains(invalidType) {
t.Errorf("Activity Type '%v' should not be valid", invalidType)
}
for _, validType := range IntransitiveActivityTypes {
if !IntransitiveActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if ActivityTypes.Contains(ActivityType) {
t.Errorf("Generic Activity Type '%v' should not be valid", ActivityType)
}
for _, inValidType := range CollectionManagementActivityTypes {
if !CollectionManagementActivityTypes.Contains(inValidType) {
t.Errorf("APObject Type '%v' should be valid", inValidType)
}
}
if CollectionManagementActivityTypes.Contains(invalidType) {
t.Errorf("Activity Type '%v' should not be valid", invalidType)
}
for _, validType := range ContentManagementActivityTypes {
if CollectionManagementActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
for _, validType := range ReactionsActivityTypes {
if CollectionManagementActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if ActivityTypes.Contains(ActivityType) {
t.Errorf("Generic Activity Type '%v' should not be valid", ActivityType)
}
for _, inValidType := range ContentManagementActivityTypes {
if !ContentManagementActivityTypes.Contains(inValidType) {
t.Errorf("APObject Type '%v' should be valid", inValidType)
}
}
if ContentManagementActivityTypes.Contains(invalidType) {
t.Errorf("Activity Type '%v' should not be valid", invalidType)
}
for _, validType := range CollectionManagementActivityTypes {
if ContentManagementActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
for _, validType := range ReactionsActivityTypes {
if ContentManagementActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if ReactionsActivityTypes.Contains(ActivityType) {
t.Errorf("Generic Activity Type '%v' should not be valid", ActivityType)
}
for _, inValidType := range ReactionsActivityTypes {
if !ReactionsActivityTypes.Contains(inValidType) {
t.Errorf("APObject Type '%v' should be valid", inValidType)
}
}
if ReactionsActivityTypes.Contains(invalidType) {
t.Errorf("Activity Type '%v' should not be valid", invalidType)
}
for _, validType := range CollectionManagementActivityTypes {
if ReactionsActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
for _, validType := range ContentManagementActivityTypes {
if ReactionsActivityTypes.Contains(validType) {
t.Errorf("Activity Type '%v' should not be valid", validType)
}
}
}
{
for _, validType := range CollectionTypes {
if !CollectionTypes.Contains(validType) {
t.Errorf("Generic Type '%#v' should be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if ActorTypes.Contains(invalidType) {
t.Errorf("APObject Type '%v' should not be valid", invalidType)
}
for _, validType := range ActorTypes {
if !ActorTypes.Contains(validType) {
t.Errorf("APObject Type '%v' should be valid", validType)
}
}
}
{
for _, validType := range GenericObjectTypes {
if !GenericObjectTypes.Contains(validType) {
t.Errorf("Generic Type '%v' should be valid", validType)
}
}
}
{
var invalidType ActivityVocabularyType = "RandomType"
if ObjectTypes.Contains(invalidType) {
t.Errorf("APObject Type '%v' should not be valid", invalidType)
}
for _, validType := range ObjectTypes {
if !ObjectTypes.Contains(validType) {
t.Errorf("APObject Type '%v' should be valid", validType)
}
}
}
}
func TestNaturalLanguageValue_MarshalJSON(t *testing.T) {
p := NaturalLanguageValues{
{
"en", "the test",
},
{
"fr", "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", "the test",
},
}
out1, err1 := p1.MarshalJSON()
if err1 != nil {
t.Errorf("Error: '%s'", err1)
}
txt := `{"en":"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: "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: "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: "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: "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: "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 TestObject_IsLink(t *testing.T) {
o := ObjectNew(ObjectType)
o.ID = "test"
if o.IsLink() {
t.Errorf("%#v should not be a valid link", o.Type)
}
m := ObjectNew(AcceptType)
m.ID = "test"
if m.IsLink() {
t.Errorf("%#v should not be a valid link", m.Type)
}
}
func TestObject_IsObject(t *testing.T) {
o := ObjectNew(ObjectType)
o.ID = "test"
if !o.IsObject() {
t.Errorf("%#v should be a valid object", o.Type)
}
m := ObjectNew(AcceptType)
m.ID = "test"
if !m.IsObject() {
t.Errorf("%#v should be a valid object", m.Type)
}
}
func TestObjectsArr_Append(t *testing.T) {
d := make(ItemCollection, 0)
val := Object{ID: ObjectID("grrr")}
d.Append(val)
if len(d) != 1 {
t.Errorf("Objects array should have exactly an element")
}
if !reflect.DeepEqual(d[0], val) {
t.Errorf("First item in object array does not match %q", val.ID)
}
}
func TestRecipients(t *testing.T) {
bob := PersonNew("bob")
alice := PersonNew("alice")
foo := OrganizationNew("foo")
bar := GroupNew("bar")
first := make(ItemCollection, 0)
if len(first) != 0 {
t.Errorf("Objects array should have exactly an element")
}
first.Append(bob)
first.Append(alice)
first.Append(foo)
first.Append(bar)
if len(first) != 4 {
t.Errorf("Objects array should have exactly 4(four) elements, not %d", len(first))
}
first.Append(bar)
first.Append(alice)
first.Append(foo)
first.Append(bob)
if len(first) != 8 {
t.Errorf("Objects array should have exactly 8(eight) elements, not %d", len(first))
}
ItemCollectionDeduplication(&first)
if len(first) != 4 {
t.Errorf("Objects array should have exactly 4(four) elements, not %d", len(first))
}
second := make(ItemCollection, 0)
second.Append(bar)
second.Append(foo)
ItemCollectionDeduplication(&first, &second)
if len(first) != 4 {
t.Errorf("First Objects array should have exactly 8(eight) elements, not %d", len(first))
}
if len(second) != 0 {
t.Errorf("Second Objects array should have exactly 0(zero) elements, not %d", len(second))
}
_, err := ItemCollectionDeduplication(&first, &second, nil)
if err != nil {
t.Errorf("Deduplication with empty array failed")
}
}
func TestNaturalLanguageValue_Get(t *testing.T) {
testVal := "test"
a := NaturalLanguageValues{{NilLangRef, testVal}}
if a.Get(NilLangRef) != testVal {
t.Errorf("Invalid Get result. Expected %s received %s", testVal, a.Get(NilLangRef))
}
}
func TestNaturalLanguageValue_Set(t *testing.T) {
testVal := "test"
a := NaturalLanguageValues{{NilLangRef, "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 := "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) != 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 := "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) != 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) != 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 := "random"
langDe := "de-DE"
valDe := "zufällig\n"
//m := make(map[string]string)
//m[langEn] = valEn
//m[langDe] = valDe
json := `{
"` + langEn + `": "` + valEn + `",
"` + langDe + `": "` + valDe + `"
}`
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 != valEn {
t.Errorf("Invalid json unmarshal for %T. Expected value %q, found %q", a, valEn, val)
}
if val.Ref == LangRef(langDe) && val.Value != valDe {
t.Errorf("Invalid json unmarshal for %T. Expected value %q, found %q", a, valDe, val)
}
}
}
func validateEmptyObject(o Object, t *testing.T) {
if o.ID != "" {
t.Errorf("Unmarshaled object %T should have empty ID, received %q", o, o.ID)
}
if o.Type != "" {
t.Errorf("Unmarshaled object %T should have empty Type, received %q", o, o.Type)
}
if o.AttributedTo != nil {
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", o, o.AttributedTo)
}
if len(o.Name) != 0 {
t.Errorf("Unmarshaled object %T should have empty Name, received %q", o, o.Name)
}
if len(o.Summary) != 0 {
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", o, o.Summary)
}
if len(o.Content) != 0 {
t.Errorf("Unmarshaled object %T should have empty Content, received %q", o, o.Content)
}
if o.URL != nil {
t.Errorf("Unmarshaled object %T should have empty URL, received %v", o, o.URL)
}
if o.Icon != nil {
t.Errorf("Unmarshaled object %T should have empty Icon, received %v", o, o.Icon)
}
if o.Image != nil {
t.Errorf("Unmarshaled object %T should have empty Image, received %v", o, o.Image)
}
if !o.Published.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Published, received %q", o, o.Published)
}
if !o.StartTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", o, o.StartTime)
}
if !o.Updated.IsZero() {
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", o, o.Updated)
}
if !o.EndTime.IsZero() {
t.Errorf("Unmarshaled object %T should have empty EndTime, received %q", o, o.EndTime)
}
if o.Duration != 0 {
t.Errorf("Unmarshaled object %T should have empty Duration, received %q", o, o.Duration)
}
if len(o.To) > 0 {
t.Errorf("Unmarshaled object %T should have empty To, received %q", o, o.To)
}
if len(o.Bto) > 0 {
t.Errorf("Unmarshaled object %T should have empty Bto, received %q", o, o.Bto)
}
if len(o.CC) > 0 {
t.Errorf("Unmarshaled object %T should have empty CC, received %q", o, o.CC)
}
if len(o.BCC) > 0 {
t.Errorf("Unmarshaled object %T should have empty BCC, received %q", o, o.BCC)
}
}
func TestObject_UnmarshalJSON(t *testing.T) {
o := Object{}
dataEmpty := []byte("{}")
o.UnmarshalJSON(dataEmpty)
validateEmptyObject(o, t)
}
func TestMimeType_UnmarshalJSON(t *testing.T) {
m := MimeType("")
dataEmpty := []byte("")
m.UnmarshalJSON(dataEmpty)
if m != "" {
t.Errorf("Unmarshaled object %T should be an empty string, received %q", m, m)
}
}
func TestLangRefValue_String(t *testing.T) {
t.Skipf("TODO")
}
func TestLangRefValue_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}
func TestLangRefValue_UnmarshalText(t *testing.T) {
t.Skipf("TODO")
}
func TestLangRef_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 TestObjectID_UnmarshalJSON(t *testing.T) {
o := ObjectID("")
dataEmpty := []byte("")
o.UnmarshalJSON(dataEmpty)
if o != "" {
t.Errorf("Unmarshaled object %T should be an empty string, received %q", o, o)
}
}
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 TestObject_GetID(t *testing.T) {
a := Object{}
testVal := "crash$"
a.ID = ObjectID(testVal)
if string(a.GetID()) != testVal {
t.Errorf("%T should return %q, Received %q", a.GetID, testVal, a.GetID())
}
}
func TestObject_GetLink(t *testing.T) {
a := Object{}
testVal := "crash$"
a.ID = ObjectID(testVal)
if string(a.GetLink()) != testVal {
t.Errorf("%T should return %q, Received %q", a.GetLink, testVal, a.GetLink())
}
}
func TestObject_GetType(t *testing.T) {
a := Object{}
a.Type = ActorType
if a.GetType() != ActorType {
t.Errorf("%T should return %q, Received %q", a.GetType, ActorType, a.GetType())
}
}
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: "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", "test",
},
{
"de", "test",
},
}
result, err := m.MarshalJSON()
if err != nil {
t.Errorf("Failed marshaling '%v'", err)
}
mRes := "{\"de\":\"test\",\"en\":\"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", "test",
}}
result1, err1 := n1.MarshalJSON()
if err1 != nil {
t.Errorf("Failed marshaling '%v'", err1)
}
mRes1 := `{"en":"test"}`
if string(result1) != mRes1 {
t.Errorf("Different results '%v' vs. '%v'", string(result1), mRes1)
}
}
{
nlv := LangRefValue{
Ref: NilLangRef,
Value: "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: "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\":\"%s\"}", nlv.Ref, nlv.Value)
if string(j) != expected {
t.Errorf("Wrong value: %s, expected %s", j, expected)
}
}
{
nlvEn := LangRefValue{
Ref: "en",
Value: "test",
}
nlvFr := LangRefValue{
Ref: "fr",
Value: "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: "test\nwith new line",
}
nlvFr := LangRefValue{
Ref: "fr",
Value: "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), nlvFr.Ref, strconv.Quote(nlvFr.Value))
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 != "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 != "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 TestToObject(t *testing.T) {
var it Item
ob := ObjectNew(ArticleType)
it = ob
o, err := ToObject(it)
if err != nil {
t.Error(err)
}
if o != ob {
t.Errorf("Invalid activity returned by ToObject #%v", ob)
}
act := ActivityNew("test", CreateType, nil)
it = act
a, err := ToObject(it)
if err != nil {
t.Errorf("Error returned when calling ToObject with activity should be nil, received %s", err)
}
if a == nil {
t.Errorf("Invalid return by ToObject #%v, should have not been nil", a)
}
}
func TestFlattenObjectProperties(t *testing.T) {
t.Skipf("TODO")
}
func TestFlattenProperties(t *testing.T) {
t.Skipf("TODO")
}
func TestToTombstone(t *testing.T) {
t.Skipf("TODO")
}
func TestToRelationship(t *testing.T) {
t.Skipf("TODO")
}
func TestObject_Recipients(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_Recipients(t *testing.T) {
t.Skipf("TODO")
}
func TestTombstone_Recipients(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 TestItemCollectionDeduplication(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -0,0 +1,260 @@
package activitystreams
import (
"errors"
"time"
)
// 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 anActivity Pub Object or Link.
ID ObjectID `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 LinkOrIRI `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"`
// 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 preceeding 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"`
}
// 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 ObjectID corresponding to the OrderedCollection
func (o OrderedCollection) GetID() ObjectID {
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 one
func (o OrderedCollection) Contains(r IRI) bool {
if len(o.OrderedItems) == 0 {
return false
}
for _, iri := range o.OrderedItems {
if r.Equals(iri.GetLink(), false) {
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.TotalItems > 0 {
return o.TotalItems
}
return uint(len(o.OrderedItems))
}
// Append adds an element to an OrderedCollection
func (o *OrderedCollection) Append(ob Item) error {
o.OrderedItems = append(o.OrderedItems, ob)
return nil
}
// UnmarshalJSON
func (o *OrderedCollection) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
o.ID = JSONGetObjectID(data)
o.Type = JSONGetType(data)
o.Name = JSONGetNaturalLanguageField(data, "name")
o.Content = JSONGetNaturalLanguageField(data, "content")
o.Summary = JSONGetNaturalLanguageField(data, "summary")
o.Context = JSONGetItem(data, "context")
o.URL = JSONGetURIItem(data, "url")
o.MediaType = MimeType(JSONGetString(data, "mediaType"))
o.Generator = JSONGetItem(data, "generator")
o.AttributedTo = JSONGetItem(data, "attributedTo")
o.Attachment = JSONGetItem(data, "attachment")
o.Location = JSONGetItem(data, "location")
o.Published = JSONGetTime(data, "published")
o.StartTime = JSONGetTime(data, "startTime")
o.EndTime = JSONGetTime(data, "endTime")
o.Duration = JSONGetDuration(data, "duration")
o.Icon = JSONGetItem(data, "icon")
o.Preview = JSONGetItem(data, "preview")
o.Image = JSONGetItem(data, "image")
o.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
o.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
o.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
o.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
o.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
o.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
o.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
o.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
o.Tag = tag
}
o.TotalItems = uint(JSONGetInt(data, "totalItems"))
o.OrderedItems = JSONGetItems(data, "orderedItems")
o.Current = JSONGetItem(data, "current")
o.First = JSONGetItem(data, "first")
o.Last = JSONGetItem(data, "last")
return nil
}
// 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
}
return nil, errors.New("unable to convert to ordered collection")
}
func copyOrderedCollectionToPage(c *OrderedCollection, p *OrderedCollectionPage) error {
p.ID = c.ID
return nil
}

View file

@ -0,0 +1,261 @@
package activitystreams
import (
"errors"
"github.com/buger/jsonparser"
"time"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// 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 preceeding 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 ObjectID corresponding to the OrderedCollectionPage object
func (o OrderedCollectionPage) GetID() ObjectID {
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.TotalItems > 0 {
return o.TotalItems
}
return uint(len(o.OrderedItems))
}
// Append adds an element to an OrderedCollectionPage
func (o *OrderedCollectionPage) Append(ob Item) error {
o.OrderedItems = append(o.OrderedItems, ob)
return nil
}
// Contains verifies if OrderedCollectionPage array contains the received one
func (o OrderedCollectionPage) Contains(r IRI) bool {
if len(o.OrderedItems) == 0 {
return false
}
for _, iri := range o.OrderedItems {
if r.Equals(iri.GetLink(), false) {
return true
}
}
return false
}
// UnmarshalJSON
func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
o.ID = JSONGetObjectID(data)
o.Type = JSONGetType(data)
o.Name = JSONGetNaturalLanguageField(data, "name")
o.Content = JSONGetNaturalLanguageField(data, "content")
o.Summary = JSONGetNaturalLanguageField(data, "summary")
o.Context = JSONGetItem(data, "context")
o.URL = JSONGetURIItem(data, "url")
o.MediaType = MimeType(JSONGetString(data, "mediaType"))
o.Generator = JSONGetItem(data, "generator")
o.AttributedTo = JSONGetItem(data, "attributedTo")
o.Attachment = JSONGetItem(data, "attachment")
o.Location = JSONGetItem(data, "location")
o.Published = JSONGetTime(data, "published")
o.StartTime = JSONGetTime(data, "startTime")
o.EndTime = JSONGetTime(data, "endTime")
o.Duration = JSONGetDuration(data, "duration")
o.Icon = JSONGetItem(data, "icon")
o.Preview = JSONGetItem(data, "preview")
o.Image = JSONGetItem(data, "image")
o.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
o.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
o.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
o.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
o.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
o.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
o.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
o.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
o.Tag = tag
}
o.TotalItems = uint(JSONGetInt(data, "totalItems"))
o.OrderedItems = JSONGetItems(data, "orderedItems")
o.Current = JSONGetItem(data, "current")
o.First = JSONGetItem(data, "first")
o.Last = JSONGetItem(data, "last")
o.Next = JSONGetItem(data, "next")
o.Prev = JSONGetItem(data, "prev")
o.PartOf = JSONGetItem(data, "partOf")
if si, err := jsonparser.GetInt(data, "startIndex"); err != nil {
o.StartIndex = uint(si)
}
return nil
}
// ToOrderedCollectionPage
func ToOrderedCollectionPage(it Item) (*OrderedCollectionPage, error) {
switch i := it.(type) {
case *OrderedCollectionPage:
return i, nil
case OrderedCollectionPage:
return &i, nil
}
return nil, errors.New("unable to convert to ordered collection page")
}

View file

@ -0,0 +1,120 @@
package activitystreams
import (
"reflect"
"testing"
)
func TestOrderedCollectionPageNew(t *testing.T) {
var testValue = ObjectID("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 := ObjectID("test")
val := Object{ID: ObjectID("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 := ObjectID("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 TestToOrderedCollectionPage(t *testing.T) {
t.Skipf("TODO")
}
func TestOrderedCollectionPage_Contains(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -0,0 +1,218 @@
package activitystreams
import (
"reflect"
"testing"
)
func TestOrderedCollectionNew(t *testing.T) {
var testValue = ObjectID("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 := ObjectID("test")
val := Object{ID: ObjectID("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 := ObjectID("test")
val := Object{ID: ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 := ObjectID("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 TestToOrderedCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestOrderedCollection_Contains(t *testing.T) {
t.Skipf("TODO")
}

239
streams/place.go Normal file
View file

@ -0,0 +1,239 @@
package activitystreams
import (
"fmt"
"time"
"unsafe"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// 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
// 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
// Latitude the latitude of a place
Latitude float64
// Longitude the longitude of a place
Longitude float64
// 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
// 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
}
// 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() ObjectID {
return p.ID
}
// UnmarshalJSON
func (p *Place) UnmarshalJSON(data []byte) error {
// TODO(marius): this is a candidate of using OnObject() for loading the common properties
// and then loading the extra ones
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
p.ID = JSONGetObjectID(data)
p.Type = JSONGetType(data)
p.Name = JSONGetNaturalLanguageField(data, "name")
p.Content = JSONGetNaturalLanguageField(data, "content")
p.Summary = JSONGetNaturalLanguageField(data, "summary")
p.Context = JSONGetItem(data, "context")
p.URL = JSONGetURIItem(data, "url")
p.MediaType = MimeType(JSONGetString(data, "mediaType"))
p.Generator = JSONGetItem(data, "generator")
p.AttributedTo = JSONGetItem(data, "attributedTo")
p.Attachment = JSONGetItem(data, "attachment")
p.Location = JSONGetItem(data, "location")
p.Published = JSONGetTime(data, "published")
p.StartTime = JSONGetTime(data, "startTime")
p.EndTime = JSONGetTime(data, "endTime")
p.Duration = JSONGetDuration(data, "duration")
p.Icon = JSONGetItem(data, "icon")
p.Preview = JSONGetItem(data, "preview")
p.Image = JSONGetItem(data, "image")
p.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
p.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
p.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
p.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
p.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
p.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
p.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
p.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
p.Tag = tag
}
p.Accuracy = JSONGetFloat(data, "accuracy")
p.Altitude = JSONGetFloat(data, "altitude")
p.Latitude = JSONGetFloat(data, "latitude")
p.Longitude = JSONGetFloat(data, "longitude")
p.Radius = JSONGetInt(data, "radius")
p.Units = JSONGetString(data, "units")
return nil
}
// Recipients performs recipient de-duplication on the Place object's To, Bto, CC and BCC properties
func (p *Place) Recipients() ItemCollection {
var aud ItemCollection
rec, _ := ItemCollectionDeduplication(&aud, &p.To, &p.Bto, &p.CC, &p.BCC, &p.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (p *Place) Clean(){
p.BCC = nil
p.Bto = nil
}
// ToPlace
func ToPlace(it Item) (*Place, error) {
switch i := it.(type) {
case *Place:
return i, nil
case Place:
return &i, nil
case *Object:
// FIXME(marius): **memory_safety** Place has extra properties which will point to invalid memory
// we need a safe version for converting from smaller objects to larger ones
return (*Place)(unsafe.Pointer(i)), nil
case Object:
// FIXME(marius): **memory_safety** Place has extra properties which will point to invalid memory
// we need a safe version for converting from smaller objects to larger ones
return (*Place)(unsafe.Pointer(&i)), nil
}
return nil, fmt.Errorf("unable to convert %q", it.GetType())
}

43
streams/place_test.go Normal file
View file

@ -0,0 +1,43 @@
package activitystreams
import "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")
}

216
streams/profile.go Normal file
View file

@ -0,0 +1,216 @@
package activitystreams
import (
"fmt"
"time"
"unsafe"
)
// Profile a Profile is a content object that describes another Object,
// typically used to describe Actor 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 ObjectID `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 LinkOrIRI `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"`
// 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() ObjectID {
return p.ID
}
// UnmarshalJSON
func (p *Profile) UnmarshalJSON(data []byte) error {
// TODO(marius): this is a candidate of using OnObject() for loading the common properties
// and then loading the extra ones
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
p.ID = JSONGetObjectID(data)
p.Type = JSONGetType(data)
p.Name = JSONGetNaturalLanguageField(data, "name")
p.Content = JSONGetNaturalLanguageField(data, "content")
p.Summary = JSONGetNaturalLanguageField(data, "summary")
p.Context = JSONGetItem(data, "context")
p.URL = JSONGetURIItem(data, "url")
p.MediaType = MimeType(JSONGetString(data, "mediaType"))
p.Generator = JSONGetItem(data, "generator")
p.AttributedTo = JSONGetItem(data, "attributedTo")
p.Attachment = JSONGetItem(data, "attachment")
p.Location = JSONGetItem(data, "location")
p.Published = JSONGetTime(data, "published")
p.StartTime = JSONGetTime(data, "startTime")
p.EndTime = JSONGetTime(data, "endTime")
p.Duration = JSONGetDuration(data, "duration")
p.Icon = JSONGetItem(data, "icon")
p.Preview = JSONGetItem(data, "preview")
p.Image = JSONGetItem(data, "image")
p.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
p.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
p.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
p.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
p.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
p.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
p.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
p.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
p.Tag = tag
}
p.Describes = JSONGetItem(data, "describes")
return nil
}
// Recipients performs recipient de-duplication on the Profile object's To, Bto, CC and BCC properties
func (p *Profile) Recipients() ItemCollection {
var aud ItemCollection
rec, _ := ItemCollectionDeduplication(&aud, &p.To, &p.Bto, &p.CC, &p.BCC, &p.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (p *Profile) Clean(){
p.BCC = nil
p.Bto = nil
}
// ToProfile
func ToProfile(it Item) (*Profile, error) {
switch i := it.(type) {
case *Profile:
return i, nil
case Profile:
return &i, nil
case *Object:
return (*Profile)(unsafe.Pointer(i)), nil
case Object:
return (*Profile)(unsafe.Pointer(&i)), nil
}
return nil, fmt.Errorf("unable to convert %q", it.GetType())
}

43
streams/profile_test.go Normal file
View file

@ -0,0 +1,43 @@
package activitystreams
import "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")
}

237
streams/question.go Normal file
View file

@ -0,0 +1,237 @@
package activitystreams
import (
"errors"
"time"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// Actor 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 Actor `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 ObjectID corresponding to the Question object
func (q Question) GetID() ObjectID {
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
func (q *Question) UnmarshalJSON(data []byte) error {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
q.ID = JSONGetObjectID(data)
q.Type = JSONGetType(data)
q.Name = JSONGetNaturalLanguageField(data, "name")
q.Content = JSONGetNaturalLanguageField(data, "content")
q.Summary = JSONGetNaturalLanguageField(data, "summary")
q.Context = JSONGetItem(data, "context")
q.URL = JSONGetURIItem(data, "url")
q.MediaType = MimeType(JSONGetString(data, "mediaType"))
q.Generator = JSONGetItem(data, "generator")
q.AttributedTo = JSONGetItem(data, "attributedTo")
q.Attachment = JSONGetItem(data, "attachment")
q.Location = JSONGetItem(data, "location")
q.Published = JSONGetTime(data, "published")
q.StartTime = JSONGetTime(data, "startTime")
q.EndTime = JSONGetTime(data, "endTime")
q.Duration = JSONGetDuration(data, "duration")
q.Icon = JSONGetItem(data, "icon")
q.Preview = JSONGetItem(data, "preview")
q.Image = JSONGetItem(data, "image")
q.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
q.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
q.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
q.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
q.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
q.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
q.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
q.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
q.Tag = tag
}
q.Actor = JSONGetItem(data, "actor")
q.Target = JSONGetItem(data, "target")
q.Origin = JSONGetItem(data, "origin")
q.Result = JSONGetItem(data, "result")
q.Instrument = JSONGetItem(data, "instrument")
q.OneOf = JSONGetItem(data, "oneOf")
q.AnyOf = JSONGetItem(data, "anyOf")
q.Closed = JSONGetBoolean(data, "closed")
return nil
}
// QuestionNew initializes a Question activity
func QuestionNew(id ObjectID) *Question {
q := Question{ID: id, Type: QuestionType}
q.Name = NaturalLanguageValuesNew()
q.Content = NaturalLanguageValuesNew()
return &q
}
// ToQuestion
func ToQuestion(it Item) (*Question, error) {
switch i := it.(type) {
case *Question:
return i, nil
case Question:
return &i, nil
}
return nil, errors.New("unable to convert to question activity")
}

90
streams/question_test.go Normal file
View file

@ -0,0 +1,90 @@
package activitystreams
import "testing"
func TestQuestionNew(t *testing.T) {
var testValue = ObjectID("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")
}

229
streams/relationship.go Normal file
View file

@ -0,0 +1,229 @@
package activitystreams
import (
"fmt"
"time"
"unsafe"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// Subject Subject On a Relationship object, the 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
// Object
Object Item
// Relationship On a Relationship object, the relationship property identifies the kind
// of relationship that exists between subject and object.
Relationship Item
}
// 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() ObjectID {
return r.ID
}
// UnmarshalJSON
func (r *Relationship) UnmarshalJSON(data []byte) error {
// TODO(marius): this is a candidate of using OnObject() for loading the common properties
// and then loading the extra ones
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
r.ID = JSONGetObjectID(data)
r.Type = JSONGetType(data)
r.Name = JSONGetNaturalLanguageField(data, "name")
r.Content = JSONGetNaturalLanguageField(data, "content")
r.Summary = JSONGetNaturalLanguageField(data, "summary")
r.Context = JSONGetItem(data, "context")
r.URL = JSONGetURIItem(data, "url")
r.MediaType = MimeType(JSONGetString(data, "mediaType"))
r.Generator = JSONGetItem(data, "generator")
r.AttributedTo = JSONGetItem(data, "attributedTo")
r.Attachment = JSONGetItem(data, "attachment")
r.Location = JSONGetItem(data, "location")
r.Published = JSONGetTime(data, "published")
r.StartTime = JSONGetTime(data, "startTime")
r.EndTime = JSONGetTime(data, "endTime")
r.Duration = JSONGetDuration(data, "duration")
r.Icon = JSONGetItem(data, "icon")
r.Preview = JSONGetItem(data, "preview")
r.Image = JSONGetItem(data, "image")
r.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
r.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
r.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
r.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
r.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
r.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
r.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
r.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
r.Tag = tag
}
r.Subject = JSONGetItem(data, "subject")
r.Object = JSONGetItem(data, "object")
r.Relationship = JSONGetItem(data, "relationship")
return nil
}
// Recipients performs recipient de-duplication on the Relationship object's To, Bto, CC and BCC properties
func (r *Relationship) Recipients() ItemCollection {
var aud ItemCollection
rec, _ := ItemCollectionDeduplication(&aud, &r.To, &r.Bto, &r.CC, &r.BCC, &r.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (r *Relationship) Clean(){
r.BCC = nil
r.Bto = nil
}
// ToRelationship
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
}
return nil, fmt.Errorf("unable to convert %q", it.GetType())
}

View file

@ -0,0 +1,35 @@
package activitystreams
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")
}

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

View file

@ -0,0 +1 @@
{}

View file

@ -0,0 +1,13 @@
{
"activity":{
"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,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",
"type": "Mention",
"ID": "http://example.com/tag/my_tag"
},
{
"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.00000UTC",
"generator": "http://example.com",
"attributedTo": "http://example.com/account/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=1",
"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.00000UTC",
"generator": "http://example.com",
"attributedTo": "http://example.com/account/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,538 @@
package tests
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"unsafe"
a "github.com/go-ap/activitystreams"
j "github.com/go-ap/jsonld"
)
const dir = "./mocks"
var stopOnFailure = false
type testPair struct {
expected bool
blank interface{}
result interface{}
}
type tests map[string]testPair
type visit struct {
a1 unsafe.Pointer
a2 unsafe.Pointer
typ reflect.Type
}
type canErrorFunc func(format string, args ...interface{})
func assertDeepEquals(t canErrorFunc, x, y interface{}) bool {
if x == nil || y == nil {
return x == y
}
v1 := reflect.ValueOf(x)
//if v1.CanAddr() {
// v1 = v1.Addr()
//}
v2 := reflect.ValueOf(y)
//if v2.CanAddr() {
// v2 = v2.Addr()
//}
if v1.Type() != v2.Type() {
t("%T != %T", x, y)
return false
}
return deepValueEqual(t, v1, v2, make(map[visit]bool), 0)
}
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
}
// We want to avoid putting more in the visited map than we need to.
// For any possible reference cycle that might be encountered,
// hard(t) needs to return true for at least one of the types in the cycle.
hard := func(k reflect.Kind) bool {
switch k {
case reflect.Map, reflect.Slice, reflect.Ptr, reflect.Interface:
return true
}
//t("Invalid type for %s", k)
return false
}
if v1.CanAddr() && v2.CanAddr() && hard(v1.Kind()) {
addr1 := unsafe.Pointer(v1.UnsafeAddr())
addr2 := unsafe.Pointer(v2.UnsafeAddr())
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++ {
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, v1.Type().Field(i).Name, v1.Field(i).Type().Name(), v2.Type().Field(i).Name, v2.Field(i).Type().Name())
if v1.Field(i).CanAddr() && v2.Field(i).CanAddr() {
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
}
return true // i guess?
}
var zLoc, _ = time.LoadLocation("UTC")
var allTests = tests{
"empty": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{},
},
"link_simple": testPair{
expected: true,
blank: &a.Link{},
result: &a.Link{
Type: a.LinkType,
Href: a.IRI("http://example.org/abc"),
HrefLang: a.LangRef("en"),
MediaType: a.MimeType("text/html"),
Name: a.NaturalLanguageValues{{
a.NilLangRef, "An example link",
}},
},
},
"object_with_url": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
URL: a.IRI("http://littr.git/api/accounts/system"),
},
},
"object_with_url_collection": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
URL: a.ItemCollection{
a.IRI("http://littr.git/api/accounts/system"),
a.IRI("http://littr.git/~system"),
},
},
},
"object_simple": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
Type: a.ObjectType,
ID: a.ObjectID("http://www.test.example/object/1"),
Name: a.NaturalLanguageValues{{
a.NilLangRef, "A Simple, non-specific object",
}},
},
},
"object_with_tags": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
Type: a.ObjectType,
ID: a.ObjectID("http://www.test.example/object/1"),
Name: a.NaturalLanguageValues{{
a.NilLangRef, "A Simple, non-specific object",
}},
Tag: a.ItemCollection{
&a.Mention{
Name: a.NaturalLanguageValues{{
a.NilLangRef, "#my_tag",
}},
Type: a.MentionType,
ID: a.ObjectID("http://example.com/tag/my_tag"),
},
&a.Mention{
Name: a.NaturalLanguageValues{{
a.NilLangRef, "@ana",
}},
Type: a.MentionType,
ID: a.ObjectID("http://example.com/users/ana"),
},
},
},
},
"object_with_replies": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
Type: a.ObjectType,
ID: a.ObjectID("http://www.test.example/object/1"),
Replies: &a.Collection{
ID: a.ObjectID("http://www.test.example/object/1/replies"),
Type: a.CollectionType,
TotalItems: 1,
Items: a.ItemCollection{
&a.Object{
ID: a.ObjectID("http://www.test.example/object/1/replies/2"),
Type: a.ArticleType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "Example title"}},
},
},
},
},
},
"activity_simple": testPair{
expected: true,
blank: &a.Activity{
Actor: &a.Person{},
},
result: &a.Activity{
Type: a.ActivityType,
Summary: a.NaturalLanguageValues{{a.NilLangRef, "Sally did something to a note"}},
Actor: &a.Person{
Type: a.PersonType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "Sally"}},
},
Object: &a.Object{
Type: a.NoteType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "A Note"}},
},
},
},
"person_with_outbox": testPair{
expected: true,
blank: &a.Person{},
result: &a.Person{
ID: a.ObjectID("http://example.com/accounts/ana"),
Type: a.PersonType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "ana"}},
URL: a.IRI("http://example.com/accounts/ana"),
},
},
"ordered_collection": testPair{
expected: true,
blank: &a.OrderedCollection{},
result: &a.OrderedCollection{
ID: a.ObjectID("http://example.com/outbox"),
Type: a.OrderedCollectionType,
URL: a.IRI("http://example.com/outbox"),
TotalItems: 1,
OrderedItems: a.ItemCollection{
&a.Object{
ID: a.ObjectID("http://example.com/outbox/53c6fb47"),
Type: a.ArticleType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "Example title"}},
Content: a.NaturalLanguageValues{{a.NilLangRef, "Example content!"}},
URL: a.IRI("http://example.com/53c6fb47"),
MediaType: a.MimeType("text/markdown"),
Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
Generator: a.IRI("http://example.com"),
AttributedTo: a.IRI("http://example.com/accounts/alice"),
},
},
},
},
"ordered_collection_page": testPair{
expected: true,
blank: &a.OrderedCollectionPage{},
result: &a.OrderedCollectionPage{
PartOf: a.IRI("http://example.com/outbox"),
Next: a.IRI("http://example.com/outbox?page=3"),
Prev: a.IRI("http://example.com/outbox?page=1"),
ID: a.ObjectID("http://example.com/outbox?page=2"),
Type: a.OrderedCollectionPageType,
URL: a.IRI("http://example.com/outbox?page=2"),
Current: a.IRI("http://example.com/outbox?page=2"),
TotalItems: 1,
OrderedItems: a.ItemCollection{
&a.Object{
ID: a.ObjectID("http://example.com/outbox/53c6fb47"),
Type: a.ArticleType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "Example title"}},
Content: a.NaturalLanguageValues{{a.NilLangRef, "Example content!"}},
URL: a.IRI("http://example.com/53c6fb47"),
MediaType: a.MimeType("text/markdown"),
Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
Generator: a.IRI("http://example.com"),
AttributedTo: a.IRI("http://example.com/accounts/alice"),
},
},
},
},
"natural_language_values": {
expected: true,
blank: &a.NaturalLanguageValues{},
result: &a.NaturalLanguageValues{
{
a.NilLangRef, `
`},
{a.LangRef("en"), "Ana got apples ⓐ"},
{a.LangRef("fr"), "Aná a des pommes ⒜"},
{a.LangRef("ro"), "Ana are mere"},
},
},
"activity_create_simple": {
expected: true,
blank: &a.Create{},
result: &a.Create{
Type: a.CreateType,
Actor: a.IRI("https://littr.git/api/accounts/anonymous"),
Object: &a.Object{
Type: a.NoteType,
AttributedTo: a.IRI("https://littr.git/api/accounts/anonymous"),
InReplyTo: a.ItemCollection{a.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff")},
Content: a.NaturalLanguageValues{{a.NilLangRef, "<p>Hello world</p>"}},
To: a.ItemCollection{a.IRI("https://www.w3.org/ns/activitystreams#Public")},
},
},
},
"activity_create_multiple_objects": {
expected: true,
blank: &a.Create{},
result: &a.Create{
Type: a.CreateType,
Actor: a.IRI("https://littr.git/api/accounts/anonymous"),
Object: a.ItemCollection{
&a.Object{
Type: a.NoteType,
AttributedTo: a.IRI("https://littr.git/api/accounts/anonymous"),
InReplyTo: a.ItemCollection{a.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff")},
Content: a.NaturalLanguageValues{{a.NilLangRef, "<p>Hello world</p>"}},
To: a.ItemCollection{a.IRI("https://www.w3.org/ns/activitystreams#Public")},
},
&a.Article{
Type: a.ArticleType,
ID: a.ObjectID("http://www.test.example/article/1"),
Name: a.NaturalLanguageValues{
{
a.NilLangRef,
"This someday will grow up to be an article",
},
},
InReplyTo: a.ItemCollection{
a.IRI("http://www.test.example/object/1"),
a.IRI("http://www.test.example/object/778"),
},
},
},
},
},
"object_with_audience": testPair{
expected: true,
blank: &a.Object{},
result: &a.Object{
Type: a.ObjectType,
ID: a.ObjectID("http://www.test.example/object/1"),
To: a.ItemCollection{
a.IRI("https://www.w3.org/ns/activitystreams#Public"),
},
Bto: a.ItemCollection{
a.IRI("http://example.com/sharedInbox"),
},
CC: a.ItemCollection{
a.IRI("https://example.com/actors/ana"),
a.IRI("https://example.com/actors/bob"),
},
BCC: a.ItemCollection{
a.IRI("https://darkside.cookie/actors/darthvader"),
},
},
},
"article_with_multiple_inreplyto": {
expected: true,
blank: &a.Article{},
result: &a.Article{
Type: a.ArticleType,
ID: a.ObjectID("http://www.test.example/article/1"),
Name: a.NaturalLanguageValues{
{
a.NilLangRef,
"This someday will grow up to be an article",
},
},
InReplyTo: a.ItemCollection{
a.IRI("http://www.test.example/object/1"),
a.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
var 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)
//}
})
}
}

220
streams/tombstone.go Normal file
View file

@ -0,0 +1,220 @@
package activitystreams
import (
"fmt"
"time"
"unsafe"
)
// 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 ObjectID `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 LinkOrIRI `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"`
// 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() ObjectID {
return t.ID
}
// UnmarshalJSON
func (t *Tombstone) UnmarshalJSON(data []byte) error {
// TODO(marius): this is a candidate of using OnObject() for loading the common properties
// and then loading the extra ones
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
t.ID = JSONGetObjectID(data)
t.Type = JSONGetType(data)
t.Name = JSONGetNaturalLanguageField(data, "name")
t.Content = JSONGetNaturalLanguageField(data, "content")
t.Summary = JSONGetNaturalLanguageField(data, "summary")
t.Context = JSONGetItem(data, "context")
t.URL = JSONGetURIItem(data, "url")
t.MediaType = MimeType(JSONGetString(data, "mediaType"))
t.Generator = JSONGetItem(data, "generator")
t.AttributedTo = JSONGetItem(data, "attributedTo")
t.Attachment = JSONGetItem(data, "attachment")
t.Location = JSONGetItem(data, "location")
t.Published = JSONGetTime(data, "published")
t.StartTime = JSONGetTime(data, "startTime")
t.EndTime = JSONGetTime(data, "endTime")
t.Duration = JSONGetDuration(data, "duration")
t.Icon = JSONGetItem(data, "icon")
t.Preview = JSONGetItem(data, "preview")
t.Image = JSONGetItem(data, "image")
t.Updated = JSONGetTime(data, "updated")
inReplyTo := JSONGetItems(data, "inReplyTo")
if len(inReplyTo) > 0 {
t.InReplyTo = inReplyTo
}
to := JSONGetItems(data, "to")
if len(to) > 0 {
t.To = to
}
audience := JSONGetItems(data, "audience")
if len(audience) > 0 {
t.Audience = audience
}
bto := JSONGetItems(data, "bto")
if len(bto) > 0 {
t.Bto = bto
}
cc := JSONGetItems(data, "cc")
if len(cc) > 0 {
t.CC = cc
}
bcc := JSONGetItems(data, "bcc")
if len(bcc) > 0 {
t.BCC = bcc
}
replies := JSONGetItem(data, "replies")
if replies != nil {
t.Replies = replies
}
tag := JSONGetItems(data, "tag")
if len(tag) > 0 {
t.Tag = tag
}
t.FormerType = ActivityVocabularyType(JSONGetString(data, "formerType"))
t.Deleted = JSONGetTime(data, "deleted")
return nil
}
// Recipients performs recipient de-duplication on the Tombstone object's To, Bto, CC and BCC properties
func (t *Tombstone) Recipients() ItemCollection {
var aud ItemCollection
rec, _ := ItemCollectionDeduplication(&aud, &t.To, &t.Bto, &t.CC, &t.BCC, &t.Audience)
return rec
}
// Clean removes Bto and BCC properties
func (t *Tombstone) Clean(){
t.BCC = nil
t.Bto = nil
}
// 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
}
return nil, fmt.Errorf("unable to convert %q", it.GetType())
}

35
streams/tombstone_test.go Normal file
View file

@ -0,0 +1,35 @@
package activitystreams
import "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")
}

432
streams/unmarshall.go Normal file
View file

@ -0,0 +1,432 @@
package activitystreams
import (
"encoding"
"encoding/json"
"fmt"
"net/url"
"reflect"
"time"
"github.com/buger/jsonparser"
)
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 activitystreams.Item
// The default for this package is JSONGetItemByType but can be overwritten
var ItemTyperFunc TyperFn
// TyperFn is the type of the function which returns an activitystreams.Item struct instance
// for a specific ActivityVocabularyType
type TyperFn func(ActivityVocabularyType) (Item, error)
func JSONGetObjectID(data []byte) ObjectID {
i, err := jsonparser.GetString(data, "id")
if err != nil {
return ObjectID("")
}
return ObjectID(i)
}
func JSONGetType(data []byte) ActivityVocabularyType {
t, err := jsonparser.GetString(data, "type")
typ := ActivityVocabularyType(t)
if err != nil {
return ""
}
return typ
}
func JSONGetMimeType(data []byte) MimeType {
t, err := jsonparser.GetString(data, "mediaType")
if err != nil {
return MimeType("")
}
return MimeType(t)
}
func JSONGetInt(data []byte, prop string) int64 {
val, err := jsonparser.GetInt(data, prop)
if err != nil {
}
return val
}
func JSONGetFloat(data []byte, prop string) float64 {
val, err := jsonparser.GetFloat(data, prop)
if err != nil {
}
return val
}
func JSONGetString(data []byte, prop string) string {
val, err := jsonparser.GetString(data, prop)
if err != nil {
}
return val
}
func JSONGetBoolean(data []byte, prop string) bool {
val, err := jsonparser.GetBoolean(data, prop)
if err != nil {
}
return val
}
func JSONGetNaturalLanguageField(data []byte, prop string) NaturalLanguageValues {
n := NaturalLanguageValues{}
val, typ, _, err := jsonparser.Get(data, prop)
if err != nil || val == nil {
return nil
}
switch typ {
case jsonparser.Object:
jsonparser.ObjectEach(val, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
if dataType == jsonparser.String {
l := LangRefValue{}
if err := l.UnmarshalJSON(value); err == nil {
if l.Ref != NilLangRef || len(l.Value) > 0 {
n = append(n, l)
}
}
}
return err
})
case jsonparser.String:
l := LangRefValue{}
if err := l.UnmarshalJSON(val); err == nil {
n = append(n, l)
}
}
return n
}
func JSONGetTime(data []byte, prop string) time.Time {
t := time.Time{}
str, _ := jsonparser.GetUnsafeString(data, prop)
t.UnmarshalText([]byte(str))
return t
}
func JSONGetDuration(data []byte, prop string) time.Duration {
str, _ := jsonparser.GetUnsafeString(data, prop)
d, _ := time.ParseDuration(str)
return d
}
func itemFn(data []byte) (Item, error) {
if len(data) == 0 {
return nil, nil
}
i, err := ItemTyperFunc(JSONGetType(data))
if err != nil || i == nil {
if i, ok := asIRI(data); ok {
// try to see if it's an IRI
return i, nil
}
return nil, nil
}
p := reflect.PtrTo(reflect.TypeOf(i))
if reflect.TypeOf(i).Implements(unmarshalerType) || p.Implements(unmarshalerType) {
err = i.(json.Unmarshaler).UnmarshalJSON(data)
}
if reflect.TypeOf(i).Implements(textUnmarshalerType) || p.Implements(textUnmarshalerType) {
err = i.(encoding.TextUnmarshaler).UnmarshalText(data)
}
return i, err
}
func JSONUnmarshalToItem(data []byte) Item {
if len(data) == 0 {
return nil
}
if ItemTyperFunc == nil {
return nil
}
val, typ, _, err := jsonparser.Get(data)
if err != nil || len(val) == 0 {
return nil
}
var i Item
switch typ {
case jsonparser.Array:
items := make(ItemCollection, 0)
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
var it Item
it, err = itemFn(value)
if it != nil && err == nil {
items.Append(it)
}
})
if len(items) == 1 {
i = items.First()
}
if len(items) > 1 {
i = items
}
case jsonparser.Object:
i, err = itemFn(data)
case jsonparser.String:
if iri, ok := asIRI(data); ok {
// try to see if it's an IRI
i = iri
}
}
if err != nil {
return nil
}
return i
}
func asIRI(val []byte) (IRI, bool) {
u, err := url.ParseRequestURI(string(val))
if err == nil && len(u.Scheme) > 0 && len(u.Host) > 0 {
// try to see if it's an IRI
return IRI(val), true
}
return IRI(""), false
}
func JSONGetItem(data []byte, prop string) Item {
val, typ, _, err := jsonparser.Get(data, prop)
if err != nil {
return nil
}
switch typ {
case jsonparser.String:
if i, ok := asIRI(val); ok {
// try to see if it's an IRI
return i
}
case jsonparser.Array:
fallthrough
case jsonparser.Object:
return JSONUnmarshalToItem(val)
case jsonparser.Number:
fallthrough
case jsonparser.Boolean:
fallthrough
case jsonparser.Null:
fallthrough
case jsonparser.Unknown:
fallthrough
default:
return nil
}
return nil
}
func JSONGetURIItem(data []byte, prop string) Item {
val, typ, _, err := jsonparser.Get(data, prop)
if err != nil {
return nil
}
switch typ {
case jsonparser.Object:
return JSONGetItem(data, prop)
case jsonparser.Array:
it := make(ItemCollection, 0)
jsonparser.ArrayEach(val, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
if i, ok := asIRI(value); ok {
it.Append(i)
return
}
i, err := ItemTyperFunc(JSONGetType(value))
if err != nil {
return
}
err = i.(json.Unmarshaler).UnmarshalJSON(value)
if err != nil {
return
}
it.Append(i)
})
return it
case jsonparser.String:
return IRI(val)
}
return nil
}
func JSONGetItems(data []byte, prop string) ItemCollection {
if len(data) == 0 {
return nil
}
val, typ, _, err := jsonparser.Get(data, prop)
if err != nil || len(val) == 0 {
return nil
}
it := make(ItemCollection, 0)
switch typ {
case jsonparser.Array:
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
i, err := itemFn(value)
if i != nil && err == nil {
it.Append(i)
}
}, prop)
case jsonparser.Object:
// this should never happen :)
case jsonparser.String:
s, _ := jsonparser.GetString(val)
it.Append(IRI(s))
}
return it
}
func JSONGetLangRefField(data []byte, prop string) LangRef {
val, err := jsonparser.GetString(data, prop)
if err != nil {
return LangRef(err.Error())
}
return LangRef(val)
}
func JSONGetIRI(data []byte, prop string) IRI {
val, err := jsonparser.GetString(data, prop)
if err != nil {
return IRI("")
}
return IRI(val)
}
// 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) {
if ItemTyperFunc == nil {
ItemTyperFunc = JSONGetItemByType
}
return JSONUnmarshalToItem(data), nil
}
func JSONGetItemByType(typ ActivityVocabularyType) (Item, error) {
switch typ {
case ObjectType:
return ObjectNew(typ), nil
case LinkType:
return &Link{Type: typ}, nil
case ActivityType:
return &Activity{Type: typ}, nil
case IntransitiveActivityType:
return &IntransitiveActivity{Type: typ}, nil
case ActorType:
return ObjectNew(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 ArticleType:
return ObjectNew(typ), nil
case AudioType:
return ObjectNew(typ), nil
case DocumentType:
return ObjectNew(typ), nil
case EventType:
return ObjectNew(typ), nil
case ImageType:
return ObjectNew(typ), nil
case NoteType:
return ObjectNew(typ), nil
case PageType:
return ObjectNew(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 VideoType:
return ObjectNew(typ), nil
case MentionType:
return &Mention{Type: typ}, nil
case ApplicationType:
return ObjectNew(typ), nil
case GroupType:
return ObjectNew(typ), nil
case OrganizationType:
return ObjectNew(typ), nil
case PersonType:
return ObjectNew(typ), nil
case ServiceType:
return ObjectNew(typ), nil
case AcceptType:
return &Accept{Type: typ}, nil
case AddType:
return &Add{Type: typ}, nil
case AnnounceType:
return &Announce{Type: typ}, nil
case ArriveType:
return &Arrive{Type: typ}, nil
case BlockType:
return &Block{Type: typ}, nil
case CreateType:
return &Create{Type: typ}, nil
case DeleteType:
return &Delete{Type: typ}, nil
case DislikeType:
return &Dislike{Type: typ}, nil
case FlagType:
return &Flag{Type: typ}, nil
case FollowType:
return &Follow{Type: typ}, nil
case IgnoreType:
return &Ignore{Type: typ}, nil
case InviteType:
return &Invite{Type: typ}, nil
case JoinType:
return &Join{Type: typ}, nil
case LeaveType:
return &Leave{Type: typ}, nil
case LikeType:
return &Like{Type: typ}, nil
case ListenType:
return &Listen{Type: typ}, nil
case MoveType:
return &Move{Type: typ}, nil
case OfferType:
return &Offer{Type: typ}, nil
case QuestionType:
return &Question{Type: typ}, nil
case RejectType:
return &Reject{Type: typ}, nil
case ReadType:
return &Read{Type: typ}, nil
case RemoveType:
return &Remove{Type: typ}, nil
case TentativeRejectType:
return &TentativeReject{Type: typ}, nil
case TentativeAcceptType:
return &TentativeAccept{Type: typ}, nil
case TravelType:
return &Travel{Type: typ}, nil
case UndoType:
return &Undo{Type: typ}, nil
case UpdateType:
return &Update{Type: typ}, nil
case ViewType:
return &View{Type: typ}, nil
case "":
// when no type is available use a plain Object
return nil, nil
}
return nil, fmt.Errorf("unrecognized ActivityStreams type %s", typ)
}

View file

@ -0,0 +1,193 @@
package activitystreams
import (
"reflect"
"testing"
)
type testPairs map[ActivityVocabularyType]reflect.Type
var objectPtrType = reflect.TypeOf(new(*Object)).Elem()
var tombstoneType = reflect.TypeOf(new(*Tombstone)).Elem()
var profileType = reflect.TypeOf(new(*Profile)).Elem()
var placeType = reflect.TypeOf(new(*Place)).Elem()
var relationshipType = reflect.TypeOf(new(*Relationship)).Elem()
var linkPtrType = reflect.TypeOf(new(*Link)).Elem()
var mentionPtrType = reflect.TypeOf(new(*Mention)).Elem()
var activityPtrType = reflect.TypeOf(new(*Activity)).Elem()
var intransitiveActivityPtrType = reflect.TypeOf(new(*IntransitiveActivity)).Elem()
var collectionPtrType = reflect.TypeOf(new(*Collection)).Elem()
var collectionPagePtrType = reflect.TypeOf(new(*CollectionPage)).Elem()
var orderedCollectionPtrType = reflect.TypeOf(new(*OrderedCollection)).Elem()
var orderedCollectionPagePtrType = reflect.TypeOf(new(*OrderedCollectionPage)).Elem()
var applicationPtrType = reflect.TypeOf(new(*Application)).Elem()
var servicePtrType = reflect.TypeOf(new(*Service)).Elem()
var personPtrType = reflect.TypeOf(new(*Person)).Elem()
var groupPtrType = reflect.TypeOf(new(*Group)).Elem()
var organizationPtrType = reflect.TypeOf(new(*Organization)).Elem()
var acceptPtrType = reflect.TypeOf(new(*Accept)).Elem()
var addPtrType = reflect.TypeOf(new(*Add)).Elem()
var announcePtrType = reflect.TypeOf(new(*Announce)).Elem()
var arrivePtrType = reflect.TypeOf(new(*Arrive)).Elem()
var blockPtrType = reflect.TypeOf(new(*Block)).Elem()
var createPtrType = reflect.TypeOf(new(*Create)).Elem()
var deletePtrType = reflect.TypeOf(new(*Delete)).Elem()
var dislikePtrType = reflect.TypeOf(new(*Dislike)).Elem()
var flagPtrType = reflect.TypeOf(new(*Flag)).Elem()
var followPtrType = reflect.TypeOf(new(*Follow)).Elem()
var ignorePtrType = reflect.TypeOf(new(*Ignore)).Elem()
var invitePtrType = reflect.TypeOf(new(*Invite)).Elem()
var joinPtrType = reflect.TypeOf(new(*Join)).Elem()
var leavePtrType = reflect.TypeOf(new(*Leave)).Elem()
var likePtrType = reflect.TypeOf(new(*Like)).Elem()
var listenPtrType = reflect.TypeOf(new(*Listen)).Elem()
var movePtrType = reflect.TypeOf(new(*Move)).Elem()
var offerPtrType = reflect.TypeOf(new(*Offer)).Elem()
var questionPtrType = reflect.TypeOf(new(*Question)).Elem()
var rejectPtrType = reflect.TypeOf(new(*Reject)).Elem()
var readPtrType = reflect.TypeOf(new(*Read)).Elem()
var removePtrType = reflect.TypeOf(new(*Remove)).Elem()
var tentativeRejectPtrType = reflect.TypeOf(new(*TentativeReject)).Elem()
var tentativeAcceptPtrType = reflect.TypeOf(new(*TentativeAccept)).Elem()
var travelPtrType = reflect.TypeOf(new(*Travel)).Elem()
var undoPtrType = reflect.TypeOf(new(*Undo)).Elem()
var updatePtrType = reflect.TypeOf(new(*Update)).Elem()
var 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: objectPtrType,
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 := JSONGetItemByType(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) {
dataEmpty := []byte("{}")
i, err := UnmarshalJSON(dataEmpty)
if err != nil {
t.Errorf("invalid unmarshaling %s", err)
}
if i != nil {
t.Errorf("invalid unmarshaling, expected nil")
}
}
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 TestJSONGetObjectID(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")
}