Merge pull request #7 from go-ap/crossing_the_streams

Merging back the Activitystreams package
This commit is contained in:
Marius Orcsik 2019-12-03 21:12:39 +01:00 committed by GitHub
commit b8b0448aff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 9932 additions and 1784 deletions

795
activity.go Normal file
View file

@ -0,0 +1,795 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
Actor Item `jsonld:"actor,omitempty"`
// Target describes the indirect object, or target, of the activity.
// The precise meaning of the target is largely dependent on the type of action being described
// but will often be the object of the English preposition "to".
// For instance, in the activity "John added a movie to his wishlist",
// the target of the activity is John's wishlist. An activity can have more than one target.
Target Item `jsonld:"target,omitempty"`
// Result describes the result of the activity. For instance, if a particular action results in the creation
// of a new resource, the result property can be used to describe that new resource.
Result Item `jsonld:"result,omitempty"`
// Origin describes an indirect object of the activity from which the activity is directed.
// The precise meaning of the origin is the object of the English preposition "from".
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
Origin Item `jsonld:"origin,omitempty"`
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
Instrument Item `jsonld:"instrument,omitempty"`
// Object When used within an Activity, describes the direct object of the activity.
// For instance, in the activity "John added a movie to his wishlist",
// the object of the activity is the movie added.
// When used within a Relationship describes the entity to which the subject is related.
Object Item `jsonld:"object,omitempty"`
}
// GetType returns the ActivityVocabulary type of the current Activity
func (a Activity) GetType() ActivityVocabularyType {
return a.Type
}
// IsLink returns false for Activity objects
func (a Activity) IsLink() bool {
return false
}
// GetID returns the 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
}
func removeFromCollection(col ItemCollection, items ...Item) ItemCollection {
result := make(ItemCollection, 0)
if len(items) == 0 {
return col
}
for _, ob := range col {
found := false
for _, it := range items {
if IRI(ob.GetID()).Equals(IRI(it.GetID()), false) {
found = true
break
}
}
if !found {
result = append(result, ob)
}
}
return result
}
func removeFromAudience(a *Activity, items ...Item) error {
if a.To != nil {
a.To = removeFromCollection(a.To, items...)
}
if a.Bto != nil {
a.Bto = removeFromCollection(a.Bto, items...)
}
if a.CC != nil {
a.CC = removeFromCollection(a.CC, items...)
}
if a.BCC != nil {
a.BCC = removeFromCollection(a.BCC, items...)
}
if a.Audience != nil {
a.Audience = removeFromCollection(a.Audience, items...)
}
return nil
}
// Recipients performs recipient de-duplication on the Activity's To, Bto, CC and BCC properties
func (a *Activity) Recipients() ItemCollection {
var alwaysRemove ItemCollection
if a.GetType() == BlockType && a.Object != nil {
alwaysRemove = append(alwaysRemove, a.Object)
}
if a.Actor != nil {
alwaysRemove = append(alwaysRemove, a.Actor)
}
if len(alwaysRemove) > 0 {
removeFromAudience(a, alwaysRemove...)
}
rec, _ := ItemCollectionDeduplication(&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
activity_test.go Normal file
View file

@ -0,0 +1,971 @@
package activitypub
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) != 3 {
t.Errorf("%T.To should have exactly 3(three) 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) != 3 {
t.Errorf("%T.To should have exactly 3(three) 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")
}

522
actors.go
View file

@ -2,175 +2,407 @@ package activitypub
import (
"errors"
as "github.com/go-ap/activitystreams"
"github.com/buger/jsonparser"
"time"
"unsafe"
)
// CanReceiveActivities 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,
}
// CanReceiveActivities is generally one of the ActivityStreams Actor Types, but they don't have to be.
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
// Actors are retrieved like any other Object in ActivityPub.
// Like other ActivityStreams objects, actors have an id, which is a URI.
type CanReceiveActivities Item
// Actor is generally one of the ActivityStreams actor Types, but they don't have to be.
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
// Actors are retrieved like any other Object in ActivityPub.
// Like other ActivityStreams objects, actors have an id, which is a URI.
type Actor struct {
// ID provides the globally unique identifier for anActivity Pub Object or Link.
ID 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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor;
// see 5.2 Inbox.
Inbox Item `jsonld:"inbox,omitempty"`
// An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor;
// see 5.1 Outbox.
Outbox Item `jsonld:"outbox,omitempty"`
// A link to an [ActivityStreams] collection of the actors that this actor is following;
// see 5.4 Following Collection
Following Item `jsonld:"following,omitempty"`
// A link to an [ActivityStreams] collection of the actors that follow this actor;
// see 5.3 Followers Collection.
Followers Item `jsonld:"followers,omitempty"`
// A link to an [ActivityStreams] collection of objects this actor has liked;
// see 5.5 Liked Collection.
Liked Item `jsonld:"liked,omitempty"`
// A short username which may be used to refer to the actor, with no uniqueness guarantees.
PreferredUsername NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"`
// A json object which maps additional (typically server/domain-wide) endpoints which may be useful either
// for this actor or someone referencing this actor.
// This mapping may be nested inside the actor document as the value or may be a link
// to a JSON-LD document with these properties.
Endpoints *Endpoints `jsonld:"endpoints,omitempty"`
// A list of supplementary Collections which may be of interest.
Streams []ItemCollection `jsonld:"streams,omitempty"`
PublicKey PublicKey `jsonld:"publicKey,omitempty"`
}
// GetID returns the ObjectID corresponding to the current Actor
func (a Actor) GetID() ObjectID {
return a.ID
}
// GetLink returns the IRI corresponding to the current Actor
func (a Actor) GetLink() IRI {
return IRI(a.ID)
}
// GetType returns the type of the current Actor
func (a Actor) GetType() ActivityVocabularyType {
return a.Type
}
// IsLink validates if currentActivity Pub Actor is a Link
func (a Actor) IsLink() bool {
return false
}
// IsObject validates if currentActivity Pub Actor is an Object
func (a Actor) IsObject() bool {
return true
}
// IsCollection returns false for Actor Objects
func (a Actor) IsCollection() bool {
return false
}
// PublicKey holds the ActivityPub compatible public key data
type PublicKey struct {
ID ObjectID `jsonld:"id,omitempty"`
Owner ObjectOrLink `jsonld:"owner,omitempty"`
PublicKeyPem string `jsonld:"publicKeyPem,omitempty"`
}
func (p *PublicKey) UnmarshalJSON(data []byte) error {
if id, err := jsonparser.GetString(data, "id"); err == nil {
p.ID = ObjectID(id)
} else {
return err
}
if o, err := jsonparser.GetString(data, "owner"); err == nil {
p.Owner = IRI(o)
} else {
return err
}
if pub, err := jsonparser.GetString(data, "publicKeyPem"); err == nil {
p.PublicKeyPem = pub
} else {
return err
}
return nil
}
type (
// Application describes a software application.
Application = Actor
// Group represents a formal or informal collective of Actors.
Group = Actor
// Organization represents an organization.
Organization = Actor
// Person represents an individual person.
Person = Actor
// Service represents a service of any kind.
Service = Actor
)
// ActorNew initializes an CanReceiveActivities type actor
func ActorNew(id ObjectID, typ ActivityVocabularyType) *Actor {
if !ActorTypes.Contains(typ) {
typ = ActorType
}
a := Actor{ID: id, Type: typ}
a.Name = NaturalLanguageValuesNew()
a.Content = NaturalLanguageValuesNew()
a.Summary = NaturalLanguageValuesNew()
in := OrderedCollectionNew(ObjectID("test-inbox"))
out := OrderedCollectionNew(ObjectID("test-outbox"))
liked := OrderedCollectionNew(ObjectID("test-liked"))
a.Inbox = in
a.Outbox = out
a.Liked = liked
a.PreferredUsername = 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
}
func (a *Actor) 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.Likes = JSONGetItem(data, "likes")
a.Shares = JSONGetItem(data, "shares")
a.Source = GetAPSource(data)
a.PreferredUsername = JSONGetNaturalLanguageField(data, "preferredUsername")
a.Followers = JSONGetItem(data, "followers")
a.Following = JSONGetItem(data, "following")
a.Inbox = JSONGetItem(data, "inbox")
a.Outbox = JSONGetItem(data, "outbox")
a.Liked = JSONGetItem(data, "liked")
a.Endpoints = JSONGetActorEndpoints(data, "endpoints")
a.Streams = JSONGetStreams(data, "streams")
a.PublicKey = JSONGetPublicKey(data, "publicKey")
return nil
}
// Endpoints a json object which maps additional (typically server/domain-wide)
// endpoints which may be useful either for this actor or someone referencing this actor.
// This mapping may be nested inside the actor document as the value or may be a link to
// a JSON-LD document with these properties.
type Endpoints struct {
// UploadMedia Upload endpoint URI for this user for binary data.
UploadMedia as.Item `jsonld:"uploadMedia,omitempty"`
UploadMedia Item `jsonld:"uploadMedia,omitempty"`
// OauthAuthorizationEndpoint Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication
// to access. To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being
// the id of the requested ActivityStreams object.
OauthAuthorizationEndpoint as.Item `jsonld:"oauthAuthorizationEndpoint,omitempty"`
OauthAuthorizationEndpoint Item `jsonld:"oauthAuthorizationEndpoint,omitempty"`
// OauthTokenEndpoint If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
// this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant.
OauthTokenEndpoint as.Item `jsonld:"oauthTokenEndpoint,omitempty"`
OauthTokenEndpoint Item `jsonld:"oauthTokenEndpoint,omitempty"`
// ProvideClientKey If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
// this endpoint specifies a URI at which a client may acquire an access token.
ProvideClientKey as.Item `jsonld:"provideClientKey,omitempty"`
ProvideClientKey Item `jsonld:"provideClientKey,omitempty"`
// SignClientKey If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization,
// this endpoint specifies a URI at which browser-authenticated users may authorize a client's public
// key for client to server interactions.
SignClientKey as.Item `jsonld:"signClientKey,omitempty"`
SignClientKey Item `jsonld:"signClientKey,omitempty"`
// SharedInbox An optional endpoint used for wide delivery of publicly addressed activities and activities sent to followers.
// SharedInbox endpoints SHOULD also be publicly readable OrderedCollection objects containing objects addressed to the
// Public special collection. Reading from the sharedInbox endpoint MUST NOT present objects which are not addressed to the Public endpoint.
SharedInbox as.Item `jsonld:"sharedInbox,omitempty"`
}
// Actor is generally one of the ActivityStreams actor Types, but they don't have to be.
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
// Actors are retrieved like any other Object in ActivityPub.
// Like other ActivityStreams objects, actors have an id, which is a URI.
type actor struct {
Parent
// A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor;
// see 5.2 Inbox.
Inbox as.Item `jsonld:"inbox,omitempty"`
// An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor;
// see 5.1 Outbox.
Outbox as.Item `jsonld:"outbox,omitempty"`
// A link to an [ActivityStreams] collection of the actors that this actor is following;
// see 5.4 Following Collection
Following as.Item `jsonld:"following,omitempty"`
// A link to an [ActivityStreams] collection of the actors that follow this actor;
// see 5.3 Followers Collection.
Followers as.Item `jsonld:"followers,omitempty"`
// A link to an [ActivityStreams] collection of objects this actor has liked;
// see 5.5 Liked Collection.
Liked as.Item `jsonld:"liked,omitempty"`
// A short username which may be used to refer to the actor, with no uniqueness guarantees.
PreferredUsername as.NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"`
// A json object which maps additional (typically server/domain-wide) endpoints which may be useful either
// for this actor or someone referencing this actor.
// This mapping may be nested inside the actor document as the value or may be a link
// to a JSON-LD document with these properties.
Endpoints *Endpoints `jsonld:"endpoints,omitempty"`
// A list of supplementary Collections which may be of interest.
Streams []as.ItemCollection `jsonld:"streams,omitempty"`
}
type (
// Application describes a software application.
Application = actor
// Group represents a formal or informal collective of Actors.
Group = actor
// Organization represents an organization.
Organization = actor
// Person represents an individual person.
Person = actor
// Service represents a service of any kind.
Service = actor
)
// actorNew initializes an actor type actor
func actorNew(id as.ObjectID, typ as.ActivityVocabularyType) *actor {
if !as.ActorTypes.Contains(typ) {
typ = as.ActorType
}
a := actor{Parent: Object{Parent: as.Parent{ID: id, Type: typ}}}
a.Name = as.NaturalLanguageValuesNew()
a.Content = as.NaturalLanguageValuesNew()
a.Summary = as.NaturalLanguageValuesNew()
in := as.OrderedCollectionNew(as.ObjectID("test-inbox"))
out := as.OrderedCollectionNew(as.ObjectID("test-outbox"))
liked := as.OrderedCollectionNew(as.ObjectID("test-liked"))
a.Inbox = in
a.Outbox = out
a.Liked = liked
a.PreferredUsername = as.NaturalLanguageValuesNew()
return &a
}
// ApplicationNew initializes an Application type actor
func ApplicationNew(id as.ObjectID) *Application {
return actorNew(id, as.ApplicationType)
}
// GroupNew initializes a Group type actor
func GroupNew(id as.ObjectID) *Group {
return actorNew(id, as.GroupType)
}
// OrganizationNew initializes an Organization type actor
func OrganizationNew(id as.ObjectID) *Organization {
return actorNew(id, as.OrganizationType)
}
// PersonNew initializes a Person type actor
func PersonNew(id as.ObjectID) *Person {
return actorNew(id, as.PersonType)
}
// ServiceNew initializes a Service type actor
func ServiceNew(id as.ObjectID) *Service {
return actorNew(id, as.ServiceType)
}
func (a *actor) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
a.Parent.UnmarshalJSON(data)
a.PreferredUsername = as.JSONGetNaturalLanguageField(data, "preferredUsername")
a.Followers = as.JSONGetItem(data, "followers")
a.Following = as.JSONGetItem(data, "following")
a.Inbox = as.JSONGetItem(data, "inbox")
a.Outbox = as.JSONGetItem(data, "outbox")
a.Liked = as.JSONGetItem(data, "liked")
a.Endpoints = JSONGetActorEndpoints(data, "endpoints")
// TODO(marius): Streams needs custom unmarshalling
//a.Streams = as.JSONGetItems(data, "streams")
return nil
}
// ToPerson
func ToPerson(it as.Item) (*Person, error) {
switch i := it.(type) {
case *as.Object:
return &Person{Parent: Object{Parent: *i}}, nil
case as.Object:
return &Person{Parent: Object{Parent: i}}, nil
case *Object:
return &Person{Parent: *i}, nil
case Object:
return &Person{Parent: i}, nil
case *actor:
return i, nil
case actor:
return &i, nil
}
return nil, errors.New("unable to convert object")
SharedInbox Item `jsonld:"sharedInbox,omitempty"`
}
// UnmarshalJSON
func (e *Endpoints) UnmarshalJSON(data []byte) error {
e.OauthAuthorizationEndpoint = as.JSONGetItem(data, "oauthAuthorizationEndpoint")
e.OauthTokenEndpoint = as.JSONGetItem(data, "oauthTokenEndpoint")
e.UploadMedia = as.JSONGetItem(data, "uploadMedia")
e.ProvideClientKey = as.JSONGetItem(data, "provideClientKey")
e.SignClientKey = as.JSONGetItem(data, "signClientKey")
e.SharedInbox = as.JSONGetItem(data, "sharedInbox")
e.OauthAuthorizationEndpoint = JSONGetItem(data, "oauthAuthorizationEndpoint")
e.OauthTokenEndpoint = JSONGetItem(data, "oauthTokenEndpoint")
e.UploadMedia = JSONGetItem(data, "uploadMedia")
e.ProvideClientKey = JSONGetItem(data, "provideClientKey")
e.SignClientKey = JSONGetItem(data, "signClientKey")
e.SharedInbox = JSONGetItem(data, "sharedInbox")
return nil
}
// ToActor
func ToActor(it Item) (*Actor, error) {
switch i := it.(type) {
case *Actor:
return i, nil
case Actor:
return &i, nil
case *Object:
return (*Actor)(unsafe.Pointer(i)), nil
case Object:
return (*Actor)(unsafe.Pointer(&i)), nil
}
return nil, errors.New("unable to convert object")
}

View file

@ -1,15 +1,15 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
"reflect"
"testing"
)
func TestActorNew(t *testing.T) {
var testValue = as.ObjectID("test")
var testType = as.ApplicationType
var testValue = ObjectID("test")
var testType = ApplicationType
o := actorNew(testValue, testType)
o := ActorNew(testValue, testType)
if o.ID != testValue {
t.Errorf("APObject Id '%v' different than expected '%v'", o.ID, testValue)
@ -18,72 +18,114 @@ func TestActorNew(t *testing.T) {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, testType)
}
n := actorNew(testValue, "")
n := ActorNew(testValue, "")
if n.ID != testValue {
t.Errorf("APObject Id '%v' different than expected '%v'", n.ID, testValue)
}
if n.Type != as.ActorType {
t.Errorf("APObject Type '%v' different than expected '%v'", n.Type, as.ActorType)
if n.Type != ActorType {
t.Errorf("APObject Type '%v' different than expected '%v'", n.Type, ActorType)
}
}
func TestPersonNew(t *testing.T) {
var testValue = as.ObjectID("test")
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 != as.PersonType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, as.PersonType)
if o.Type != PersonType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, PersonType)
}
}
func TestApplicationNew(t *testing.T) {
var testValue = as.ObjectID("test")
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 != as.ApplicationType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, as.ApplicationType)
if o.Type != ApplicationType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, ApplicationType)
}
}
func TestGroupNew(t *testing.T) {
var testValue = as.ObjectID("test")
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 != as.GroupType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, as.GroupType)
if o.Type != GroupType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, GroupType)
}
}
func TestOrganizationNew(t *testing.T) {
var testValue = as.ObjectID("test")
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 != as.OrganizationType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, as.OrganizationType)
if o.Type != OrganizationType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, OrganizationType)
}
}
func TestServiceNew(t *testing.T) {
var testValue = as.ObjectID("test")
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 != as.ServiceType {
t.Errorf("APObject Type '%v' different than expected '%v'", o.Type, as.ServiceType)
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)
}
}
@ -91,36 +133,140 @@ 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("Unmarshalled object %T should have empty ID, received %q", p, p.ID)
t.Errorf("Unmarshaled object %T should have empty ID, received %q", p, p.ID)
}
if p.Type != "" {
t.Errorf("Unmarshalled object %T should have empty Type, received %q", p, p.Type)
t.Errorf("Unmarshaled object %T should have empty Type, received %q", p, p.Type)
}
if p.AttributedTo != nil {
t.Errorf("Unmarshalled object %T should have empty AttributedTo, received %q", p, p.AttributedTo)
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", p, p.AttributedTo)
}
if len(p.Name) != 0 {
t.Errorf("Unmarshalled object %T should have empty Name, received %q", p, p.Name)
t.Errorf("Unmarshaled object %T should have empty Name, received %q", p, p.Name)
}
if len(p.Summary) != 0 {
t.Errorf("Unmarshalled object %T should have empty Summary, received %q", p, p.Summary)
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", p, p.Summary)
}
if len(p.Content) != 0 {
t.Errorf("Unmarshalled object %T should have empty Content, received %q", p, p.Content)
t.Errorf("Unmarshaled object %T should have empty Content, received %q", p, p.Content)
}
if p.URL != nil {
t.Errorf("Unmarshalled object %T should have empty URL, received %v", p, p.URL)
t.Errorf("Unmarshaled object %T should have empty URL, received %v", p, p.URL)
}
if !p.Published.IsZero() {
t.Errorf("Unmarshalled object %T should have empty Published, received %q", p, p.Published)
t.Errorf("Unmarshaled object %T should have empty Published, received %q", p, p.Published)
}
if !p.StartTime.IsZero() {
t.Errorf("Unmarshalled object %T should have empty StartTime, received %q", p, p.StartTime)
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", p, p.StartTime)
}
if !p.Updated.IsZero() {
t.Errorf("Unmarshalled object %T should have empty Updated, received %q", p, p.Updated)
t.Errorf("Unmarshaled object %T should have empty Updated, received %q", p, p.Updated)
}
}

View file

@ -7,7 +7,7 @@ import (
"net/http"
"net/url"
as "github.com/go-ap/activitystreams"
pub "github.com/go-ap/activitypub"
)
type RequestSignFn func(*http.Request) error
@ -30,7 +30,7 @@ type CanSign interface {
}
type Client interface {
LoadIRI(as.IRI) (as.Item, error)
LoadIRI(pub.IRI) (pub.Item, error)
}
// UserAgent value that the client uses when performing requests
@ -48,10 +48,10 @@ var defaultSign RequestSignFn = func(r *http.Request) error { return nil }
type err struct {
msg string
iri as.IRI
iri pub.IRI
}
func errorf(i as.IRI, msg string, p ...interface{}) error {
func errorf(i pub.IRI, msg string, p ...interface{}) error {
return &err{
msg: fmt.Sprintf(msg, p...),
iri: i,
@ -82,7 +82,7 @@ func (c *client) SignFn(fn RequestSignFn) {
}
// LoadIRI tries to dereference an IRI and load the full ActivityPub object it represents
func (c *client) LoadIRI(id as.IRI) (as.Item, error) {
func (c *client) LoadIRI(id pub.IRI) (pub.Item, error) {
if len(id) == 0 {
return nil, errorf(id, "Invalid IRI, nil value")
}
@ -90,7 +90,7 @@ func (c *client) LoadIRI(id as.IRI) (as.Item, error) {
return nil, errorf(id, "Invalid IRI: %s", err)
}
var err error
var obj as.Item
var obj pub.Item
var resp *http.Response
if resp, err = c.Get(id.String()); err != nil {
@ -115,7 +115,7 @@ func (c *client) LoadIRI(id as.IRI) (as.Item, error) {
return obj, err
}
return as.UnmarshalJSON(body)
return pub.UnmarshalJSON(body)
}
func (c *client) req(method string, url string, body io.Reader) (*http.Request, error) {
@ -133,7 +133,7 @@ func (c *client) req(method string, url string, body io.Reader) (*http.Request,
req.Header.Set("Content-Type", ContentTypeJsonLD)
}
if err = c.signFn(req); err != nil {
err := errorf(as.IRI(req.URL.String()), "Unable to sign request (method %q, previous error: %s)", req.Method, err)
err := errorf(pub.IRI(req.URL.String()), "Unable to sign request (method %q, previous error: %s)", req.Method, err)
return req, err
}
return req, nil

View file

@ -4,7 +4,7 @@ import (
"strings"
"testing"
as "github.com/go-ap/activitystreams"
pub "github.com/go-ap/activitypub"
)
func TestNewClient(t *testing.T) {
@ -18,7 +18,7 @@ func TestNewClient(t *testing.T) {
func TestErr_Error(t *testing.T) {
e := err{
msg: "test",
iri: as.IRI(""),
iri: pub.IRI(""),
}
if len(e.Error()) == 0 {
@ -30,7 +30,7 @@ func TestErr_Error(t *testing.T) {
}
func TestClient_LoadIRI(t *testing.T) {
empty := as.IRI("")
empty := pub.IRI("")
c := NewClient()
var err error
@ -41,7 +41,7 @@ func TestClient_LoadIRI(t *testing.T) {
t.Logf("Valid error received: %s", err)
}
inv := as.IRI("example.com")
inv := pub.IRI("example.com")
_, err = c.LoadIRI(inv)
if err == nil {
t.Errorf("LoadIRI should have failed when using invalid http url")

35
client/internal.go Normal file
View file

@ -0,0 +1,35 @@
package client
import (
"bytes"
"fmt"
"net/http"
)
// ErrorHandlerFunc is a data type for the default ErrorHandler function of the package
type ErrorHandlerFunc func(...error) http.HandlerFunc
// ErrorHandler is the error handler callback for the ActivityPub package.
// It can be overloaded from the packages that require it
var ErrorHandler ErrorHandlerFunc = func(errors ...error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
output := bytes.Buffer{}
for i, e := range errors {
output.WriteString(fmt.Sprintf("#%d %s\n", i, e.Error()))
}
w.WriteHeader(http.StatusInternalServerError)
w.Header().Add("Content-Type", "text/plain")
w.Write(output.Bytes())
}
}
// Routes
// {actor}/ -> HandleActorGET (requires LoadActorMiddleware(r, w), HandleError(r, w))
// {actor}/{inbox} -> HandleActorInboxGET
// {actor}/{outbox} -> HandleActorOutboxGET
// S2S
// {actor}/inbox -> InboxRequest (requires LoadActorMIddleware(r, w))
// C2S
// {actor}/outbox -> OutboxRequest

283
collection_page.go Normal file
View file

@ -0,0 +1,283 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// In a paged Collection, indicates the page that contains the most recently updated member items.
Current ObjectOrLink `jsonld:"current,omitempty"`
// In a paged Collection, indicates the furthest 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")
}

145
collection_page_test.go Normal file
View file

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

324
collections.go Normal file
View file

@ -0,0 +1,324 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// In a paged Collection, indicates the page that contains the most recently updated member items.
Current ObjectOrLink `jsonld:"current,omitempty"`
// In a paged Collection, indicates the furthest 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
collections_test.go Normal file
View file

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

View file

@ -1,78 +1,9 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
)
type (
// FollowersCollection is a collection of followers
FollowersCollection = Followers
// Followers is a Collection type
Followers as.Collection
Followers = Collection
)
// FollowersNew initializes a new Followers
func FollowersNew() *Followers {
id := as.ObjectID("followers")
i := Followers{Parent: as.Parent{ID: id, Type: as.CollectionType}}
i.Name = as.NaturalLanguageValuesNew()
i.Content = as.NaturalLanguageValuesNew()
i.Summary = as.NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// Append adds an element to an Followers
func (f *Followers) Append(ob as.Item) error {
f.Items = append(f.Items, ob)
f.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to Followers
func (f Followers) GetID() *as.ObjectID {
return f.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Followers object
func (f Followers) GetLink() as.IRI {
return as.IRI(f.ID)
}
// GetType returns the Followers's type
func (f Followers) GetType() as.ActivityVocabularyType {
return f.Type
}
// IsLink returns false for an Followers object
func (f Followers) IsLink() bool {
return false
}
// IsObject returns true for a Followers object
func (f Followers) IsObject() bool {
return true
}
// UnmarshalJSON
func (f *Followers) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
c := as.Collection(*f)
err := c.UnmarshalJSON(data)
*f = Followers(c)
return err
}
// Collection returns the underlying Collection type
func (f Followers) Collection() as.CollectionInterface {
c := as.Collection(f)
return &c
}

View file

@ -2,38 +2,6 @@ package activitypub
import "testing"
func TestFollowers_Append(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowers_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowersNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -1,9 +1,5 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
)
type (
// FollowingCollection is a list of everybody that the actor has followed, added as a side effect.
// The following collection MUST be either an OrderedCollection or a Collection and MAY
@ -11,70 +7,19 @@ type (
FollowingCollection = Following
// Following is a type alias for a simple Collection
Following as.Collection
Following = Collection
)
// FollowingNew initializes a new Following
func FollowingNew() *Following {
id := as.ObjectID("following")
id := ObjectID("following")
i := Following{Parent: as.Parent{ID: id, Type: as.CollectionType}}
i.Name = as.NaturalLanguageValuesNew()
i.Content = as.NaturalLanguageValuesNew()
i.Summary = as.NaturalLanguageValuesNew()
i := Following{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.Summary = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// Append adds an element to an Following
func (f *Following) Append(ob as.Item) error {
f.Items = append(f.Items, ob)
f.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to Following
func (f Following) GetID() *as.ObjectID {
return f.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Following object
func (f Following) GetLink() as.IRI {
return as.IRI(f.ID)
}
// GetType returns the Following's type
func (f Following) GetType() as.ActivityVocabularyType {
return f.Type
}
// IsLink returns false for an Following object
func (f Following) IsLink() bool {
return false
}
// IsObject returns true for a Following object
func (f Following) IsObject() bool {
return true
}
// UnmarshalJSON
func (f *Following) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
c := as.Collection(*f)
err := c.UnmarshalJSON(data)
*f = Following(c)
return err
}
// Collection returns the underlying Collection type
func (f Following) Collection() as.CollectionInterface {
c := as.Collection(f)
return &c
}

View file

@ -2,38 +2,6 @@ package activitypub
import "testing"
func TestFollowing_Append(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowing_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}
func TestFollowingNew(t *testing.T) {
t.Skipf("TODO")
}

1
go.mod
View file

@ -4,6 +4,5 @@ go 1.13
require (
github.com/buger/jsonparser v0.0.0-20181023193515-52c6e1462ebd
github.com/go-ap/activitystreams v0.0.0-20191201184731-495abe28e08d
github.com/go-ap/jsonld v0.0.0-20191123195936-1e43eac08b0c
)

View file

@ -3,41 +3,40 @@ package activitypub
import (
"errors"
"fmt"
"github.com/go-ap/activitystreams"
)
type withObjectFn func (*Object) error
type withActivityFn func (*activitystreams.Activity) error
type withIntransitiveActivityFn func (*activitystreams.IntransitiveActivity) error
type withQuestionFn func (*activitystreams.Question) error
type withActivityFn func (*Activity) error
type withIntransitiveActivityFn func (*IntransitiveActivity) error
type withQuestionFn func (*Question) error
type withPersonFn func (*Person) error
type withCollectionInterfaceFn func (collection activitystreams.CollectionInterface) error
type withCollectionFn func (collection *activitystreams.Collection) error
type withCollectionPageFn func (*activitystreams.CollectionPage) error
type withOrderedCollectionFn func (*activitystreams.OrderedCollection) error
type withOrderedCollectionPageFn func (*activitystreams.OrderedCollectionPage) error
type withItemCollectionFn func (collection *activitystreams.ItemCollection) error
type withCollectionInterfaceFn func (collection CollectionInterface) error
type withCollectionFn func (collection *Collection) error
type withCollectionPageFn func (*CollectionPage) error
type withOrderedCollectionFn func (*OrderedCollection) error
type withOrderedCollectionPageFn func (*OrderedCollectionPage) error
type withItemCollectionFn func (collection *ItemCollection) error
// OnObject
func OnObject(it activitystreams.Item, fn withObjectFn) error {
if activitystreams.ActivityTypes.Contains(it.GetType()) {
return OnActivity(it, func(a *activitystreams.Activity) error {
ob, err := ToObject(&a.Parent)
func OnObject(it Item, fn withObjectFn) error {
if ActivityTypes.Contains(it.GetType()) {
return OnActivity(it, func(a *Activity) error {
ob, err := ToObject(a)
if err != nil {
return err
}
return fn(ob)
})
} else if activitystreams.ActorTypes.Contains(it.GetType()) {
} else if ActorTypes.Contains(it.GetType()) {
return OnPerson(it, func(p *Person) error {
ob, err := ToObject(&p.Parent)
ob, err := ToObject(p)
if err != nil {
return err
}
return fn(ob)
})
} else if it.IsCollection() {
return OnCollection(it, func(col activitystreams.CollectionInterface) error {
return OnCollection(it, func(col CollectionInterface) error {
for _, it := range col.Collection() {
err := OnObject(it, fn)
if err != nil {
@ -56,11 +55,11 @@ func OnObject(it activitystreams.Item, fn withObjectFn) error {
}
// OnActivity
func OnActivity(it activitystreams.Item, fn withActivityFn) error {
if !activitystreams.ActivityTypes.Contains(it.GetType()) {
func OnActivity(it Item, fn withActivityFn) error {
if !ActivityTypes.Contains(it.GetType()) {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to Activity", it, it.GetType()))
}
act, err := activitystreams.ToActivity(it)
act, err := ToActivity(it)
if err != nil {
return err
}
@ -68,14 +67,14 @@ func OnActivity(it activitystreams.Item, fn withActivityFn) error {
}
// OnIntransitiveActivity
func OnIntransitiveActivity(it activitystreams.Item, fn withIntransitiveActivityFn) error {
if !activitystreams.IntransitiveActivityTypes.Contains(it.GetType()) {
func OnIntransitiveActivity(it Item, fn withIntransitiveActivityFn) error {
if !IntransitiveActivityTypes.Contains(it.GetType()) {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to Activity", it, it.GetType()))
}
if it.GetType() == activitystreams.QuestionType {
if it.GetType() == QuestionType {
errors.New(fmt.Sprintf("For %T[%s] you need to use OnQuestion function", it, it.GetType()))
}
act, err := activitystreams.ToIntransitiveActivity(it)
act, err := ToIntransitiveActivity(it)
if err != nil {
return err
}
@ -83,11 +82,11 @@ func OnIntransitiveActivity(it activitystreams.Item, fn withIntransitiveActivity
}
// OnQuestion
func OnQuestion(it activitystreams.Item, fn withQuestionFn) error {
if it.GetType() != activitystreams.QuestionType {
func OnQuestion(it Item, fn withQuestionFn) error {
if it.GetType() != QuestionType {
errors.New(fmt.Sprintf("For %T[%s] can't be converted to Question", it, it.GetType()))
}
act, err := activitystreams.ToQuestion(it)
act, err := ToQuestion(it)
if err != nil {
return err
}
@ -95,11 +94,11 @@ func OnQuestion(it activitystreams.Item, fn withQuestionFn) error {
}
// OnPerson
func OnPerson(it activitystreams.Item, fn withPersonFn) error {
if !activitystreams.ActorTypes.Contains(it.GetType()) {
func OnPerson(it Item, fn withPersonFn) error {
if !ActorTypes.Contains(it.GetType()) {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to Person", it, it.GetType()))
}
pers, err := ToPerson(it)
pers, err := ToActor(it)
if err != nil {
return err
}
@ -107,27 +106,27 @@ func OnPerson(it activitystreams.Item, fn withPersonFn) error {
}
// OnCollection
func OnCollection(it activitystreams.Item, fn withCollectionInterfaceFn) error {
func OnCollection(it Item, fn withCollectionInterfaceFn) error {
switch it.GetType() {
case activitystreams.CollectionOfItems:
col, err := activitystreams.ToItemCollection(it)
case CollectionOfItems:
col, err := ToItemCollection(it)
if err != nil {
return err
}
c := activitystreams.Collection{
c := Collection{
TotalItems: uint(len(*col)),
Items: *col,
}
return fn(&c)
case activitystreams.CollectionType:
col, err := activitystreams.ToCollection(it)
case CollectionType:
col, err := ToCollection(it)
if err != nil {
return err
}
return fn(col)
case activitystreams.CollectionPageType:
return OnCollectionPage(it, func(p *activitystreams.CollectionPage) error {
col, err := activitystreams.ToCollection(&p.ParentCollection)
case CollectionPageType:
return OnCollectionPage(it, func(p *CollectionPage) error {
col, err := ToCollection(p)
if err != nil {
return err
}
@ -139,11 +138,11 @@ func OnCollection(it activitystreams.Item, fn withCollectionInterfaceFn) error {
}
// OnCollectionPage
func OnCollectionPage(it activitystreams.Item, fn withCollectionPageFn) error {
if it.GetType() != activitystreams.CollectionPageType {
func OnCollectionPage(it Item, fn withCollectionPageFn) error {
if it.GetType() != CollectionPageType {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to Collection Page", it, it.GetType()))
}
col, err := activitystreams.ToCollectionPage(it)
col, err := ToCollectionPage(it)
if err != nil {
return err
}
@ -151,17 +150,17 @@ func OnCollectionPage(it activitystreams.Item, fn withCollectionPageFn) error {
}
// OnOrderedCollection
func OnOrderedCollection(it activitystreams.Item, fn withOrderedCollectionFn) error {
func OnOrderedCollection(it Item, fn withOrderedCollectionFn) error {
switch it.GetType() {
case activitystreams.OrderedCollectionType:
col, err := activitystreams.ToOrderedCollection(it)
case OrderedCollectionType:
col, err := ToOrderedCollection(it)
if err != nil {
return err
}
return fn(col)
case activitystreams.OrderedCollectionPageType:
return OnOrderedCollectionPage(it, func(p *activitystreams.OrderedCollectionPage) error {
col, err := activitystreams.ToOrderedCollection(&p.OrderedCollection)
case OrderedCollectionPageType:
return OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error {
col, err := ToOrderedCollection(p)
if err != nil {
return err
}
@ -173,11 +172,11 @@ func OnOrderedCollection(it activitystreams.Item, fn withOrderedCollectionFn) er
}
// OnOrderedCollectionPage
func OnOrderedCollectionPage(it activitystreams.Item, fn withOrderedCollectionPageFn) error {
if it.GetType() != activitystreams.OrderedCollectionPageType {
func OnOrderedCollectionPage(it Item, fn withOrderedCollectionPageFn) error {
if it.GetType() != OrderedCollectionPageType {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to OrderedCollection Page", it, it.GetType()))
}
col, err := activitystreams.ToOrderedCollectionPage(it)
col, err := ToOrderedCollectionPage(it)
if err != nil {
return err
}
@ -185,11 +184,11 @@ func OnOrderedCollectionPage(it activitystreams.Item, fn withOrderedCollectionPa
}
// OnOrderedCollectionPage
func OnItemCOllection(it activitystreams.Item, fn withOrderedCollectionPageFn) error {
if it.GetType() != activitystreams.OrderedCollectionPageType {
func OnItemCOllection(it Item, fn withOrderedCollectionPageFn) error {
if it.GetType() != OrderedCollectionPageType {
return errors.New(fmt.Sprintf("%T[%s] can't be converted to OrderedCollection Page", it, it.GetType()))
}
col, err := activitystreams.ToOrderedCollectionPage(it)
col, err := ToOrderedCollectionPage(it)
if err != nil {
return err
}

View file

@ -1,12 +1,11 @@
package activitypub
import (
"github.com/go-ap/activitystreams"
"testing"
)
func TestOnObject(t *testing.T) {
ob := activitystreams.ObjectNew(activitystreams.ArticleType)
ob := ObjectNew(ArticleType)
err := OnObject(ob, func(o *Object) error {
return nil
@ -28,10 +27,10 @@ func TestOnObject(t *testing.T) {
}
func TestOnActivity(t *testing.T) {
ob := activitystreams.ObjectNew(activitystreams.ArticleType)
act := activitystreams.ActivityNew("test", activitystreams.CreateType, ob)
ob := ObjectNew(ArticleType)
act := ActivityNew("test", CreateType, ob)
err := OnActivity(act, func(a *activitystreams.Activity) error {
err := OnActivity(act, func(a *Activity) error {
return nil
})
@ -39,7 +38,7 @@ func TestOnActivity(t *testing.T) {
t.Errorf("Unexpected error returned %s", err)
}
err = OnActivity(act, func(a *activitystreams.Activity) error {
err = OnActivity(act, func(a *Activity) error {
if a.Type != act.Type {
t.Errorf("In function type %s different than expected, %s", a.Type, act.Type)
}
@ -57,9 +56,9 @@ func TestOnActivity(t *testing.T) {
}
func TestOnIntransitiveActivity(t *testing.T) {
act := activitystreams.IntransitiveActivityNew("test", activitystreams.ArriveType)
act := IntransitiveActivityNew("test", ArriveType)
err := OnIntransitiveActivity(act, func(a *activitystreams.IntransitiveActivity) error {
err := OnIntransitiveActivity(act, func(a *IntransitiveActivity) error {
return nil
})
@ -67,7 +66,7 @@ func TestOnIntransitiveActivity(t *testing.T) {
t.Errorf("Unexpected error returned %s", err)
}
err = OnIntransitiveActivity(act, func(a *activitystreams.IntransitiveActivity) error {
err = OnIntransitiveActivity(act, func(a *IntransitiveActivity) error {
if a.Type != act.Type {
t.Errorf("In function type %s different than expected, %s", a.Type, act.Type)
}
@ -82,9 +81,9 @@ func TestOnIntransitiveActivity(t *testing.T) {
}
func TestOnQuestion(t *testing.T) {
act := activitystreams.QuestionNew("test")
act := QuestionNew("test")
err := OnQuestion(act, func(a *activitystreams.Question) error {
err := OnQuestion(act, func(a *Question) error {
return nil
})
@ -92,7 +91,7 @@ func TestOnQuestion(t *testing.T) {
t.Errorf("Unexpected error returned %s", err)
}
err = OnQuestion(act, func(a *activitystreams.Question) error {
err = OnQuestion(act, func(a *Question) error {
if a.Type != act.Type {
t.Errorf("In function type %s different than expected, %s", a.Type, act.Type)
}
@ -107,7 +106,7 @@ func TestOnQuestion(t *testing.T) {
}
func TestOnPerson(t *testing.T) {
pers := activitystreams.PersonNew("testPerson")
pers := PersonNew("testPerson")
err := OnPerson(pers, func(a *Person) error {
return nil
})

View file

@ -1,9 +1,5 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
)
type (
// InboxStream contains all activities received by the actor.
// The server SHOULD filter content according to the requester's permission.
@ -13,69 +9,18 @@ type (
InboxStream = Inbox
// Inbox is a type alias for an Ordered Collection
Inbox as.OrderedCollection
Inbox = OrderedCollection
)
// InboxNew initializes a new Inbox
func InboxNew() *as.OrderedCollection {
id := as.ObjectID("inbox")
func InboxNew() *OrderedCollection {
id := ObjectID("inbox")
i := as.OrderedCollection{Parent: as.Parent{ID: id, Type: as.CollectionType}}
i.Name = as.NaturalLanguageValuesNew()
i.Content = as.NaturalLanguageValuesNew()
i := OrderedCollection{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// Append adds an element to an Inbox
func (i *Inbox) Append(ob as.Item) error {
i.OrderedItems = append(i.OrderedItems, ob)
i.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to Inbox
func (i Inbox) GetID() *as.ObjectID {
return i.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Inbox object
func (i Inbox) GetLink() as.IRI {
return as.IRI(i.ID)
}
// GetType returns the Inbox's type
func (i Inbox) GetType() as.ActivityVocabularyType {
return i.Type
}
// IsLink returns false for an Inbox object
func (i Inbox) IsLink() bool {
return false
}
// IsObject returns true for a Inbox object
func (i Inbox) IsObject() bool {
return true
}
// UnmarshalJSON
func (i *Inbox) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
c := as.OrderedCollection(*i)
err := c.UnmarshalJSON(data)
*i = Inbox(c)
return err
}
// Collection returns the underlying Collection type
func (i Inbox) Collection() as.CollectionInterface {
c := as.OrderedCollection(i)
return &c
}

View file

@ -1,15 +1,13 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
"reflect"
"testing"
)
func TestInboxNew(t *testing.T) {
i := InboxNew()
id := as.ObjectID("inbox")
id := ObjectID("inbox")
if i.ID != id {
t.Errorf("%T should be initialized with %q as %T", i, id, id)
}
@ -26,83 +24,3 @@ func TestInboxNew(t *testing.T) {
t.Errorf("%T should be initialized with 0 TotalItems", i)
}
}
func TestInboxStream_GetID(t *testing.T) {
o := InboxStream{}
if *o.GetID() != "" {
t.Errorf("%T should be initialized with empty %T", o, o.GetID())
}
id := as.ObjectID("test_out_stream")
o.ID = id
if *o.GetID() != id {
t.Errorf("%T should have %T as %q", o, id, id)
}
}
func TestInboxStream_GetType(t *testing.T) {
o := InboxStream{}
if o.GetType() != "" {
t.Errorf("%T should be initialized with empty %T", o, o.GetType())
}
o.Type = as.OrderedCollectionType
if o.GetType() != as.OrderedCollectionType {
t.Errorf("%T should have %T as %q", o, o.GetType(), as.OrderedCollectionType)
}
}
func TestInboxStream_Append(t *testing.T) {
o := InboxStream{}
val := as.Object{ID: as.ObjectID("grrr")}
o.Append(val)
if o.TotalItems != 1 {
t.Errorf("%T should have exactly an element, found %d", o, o.TotalItems)
}
if !reflect.DeepEqual(o.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", o, o.OrderedItems, val.ID)
}
}
func TestInbox_Append(t *testing.T) {
i := InboxNew()
val := as.Object{ID: as.ObjectID("grrr")}
i.Append(val)
if i.TotalItems != 0 {
t.Errorf("%T should have exactly an element, found %d", i, i.TotalItems)
}
if !reflect.DeepEqual(i.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", i, i.OrderedItems, val.ID)
}
}
func TestInbox_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestInbox_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}

290
intransitive_activity.go Normal file
View file

@ -0,0 +1,290 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
Actor CanReceiveActivities `jsonld:"actor,omitempty"`
// Target describes the indirect object, or target, of the activity.
// The precise meaning of the target is largely dependent on the type of action being described
// but will often be the object of the English preposition "to".
// For instance, in the activity "John added a movie to his wishlist",
// the target of the activity is John's wishlist. An activity can have more than one target.
Target Item `jsonld:"target,omitempty"`
// Result describes the result of the activity. For instance, if a particular action results in the creation
// of a new resource, the result property can be used to describe that new resource.
Result Item `jsonld:"result,omitempty"`
// Origin describes an indirect object of the activity from which the activity is directed.
// The precise meaning of the origin is the object of the English preposition "from".
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
Origin Item `jsonld:"origin,omitempty"`
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
Instrument Item `jsonld:"instrument,omitempty"`
}
type (
// Arrive is an IntransitiveActivity that indicates that the actor has arrived at the location.
// The origin can be used to identify the context from which the actor originated.
// The target typically has no defined meaning.
Arrive = IntransitiveActivity
// Travel indicates that the actor is traveling to target from origin.
// Travel is an IntransitiveObject whose actor specifies the direct object.
// If the target or origin are not specified, either can be determined by context.
Travel = IntransitiveActivity
)
// Recipients performs recipient de-duplication on the 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 activitypub
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)
}
}

161
iri.go Normal file
View file

@ -0,0 +1,161 @@
package activitypub
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
}
func validURL(u *url.URL) bool {
return len(u.Scheme) > 0 && len(u.Host) > 0
}
// 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 || !validURL(u) || !validURL(uw) {
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
iri_test.go Normal file
View file

@ -0,0 +1,212 @@
package activitypub
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
item.go Normal file
View file

@ -0,0 +1,84 @@
package activitypub
// 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
item_test.go Normal file
View file

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

View file

@ -1,103 +1,25 @@
package activitypub
import as "github.com/go-ap/activitystreams"
type (
// LikedCollection is a list of every object from all of the actor's Like activities,
// added as a side effect. The liked collection MUST be either an OrderedCollection or
// a Collection and MAY be filtered on privileges of an authenticated user or as
// appropriate when no authentication is given.
LikedCollection Liked
LikedCollection = Liked
// Liked is a type alias for an Ordered Collection
Liked as.OrderedCollection
Liked = OrderedCollection
)
// LikedCollection initializes a new Outbox
func LikedNew() *as.OrderedCollection {
id := as.ObjectID("liked")
func LikedNew() *OrderedCollection {
id := ObjectID("liked")
l := as.OrderedCollection{Parent: as.Parent{ID: id, Type: as.CollectionType}}
l.Name = as.NaturalLanguageValuesNew()
l.Content = as.NaturalLanguageValuesNew()
l := OrderedCollection{ID: id, Type: CollectionType}
l.Name = NaturalLanguageValuesNew()
l.Content = NaturalLanguageValuesNew()
l.TotalItems = 0
return &l
}
// Append adds an element to an LikedCollection
func (l *LikedCollection) Append(o as.Item) error {
l.OrderedItems = append(l.OrderedItems, o)
l.TotalItems++
return nil
}
// Append adds an element to an Outbox
func (l *Liked) Append(ob as.Item) error {
l.OrderedItems = append(l.OrderedItems, ob)
l.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to the LikedCollection
func (l LikedCollection) GetID() *as.ObjectID {
return l.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current LikedCollection object
func (l LikedCollection) GetLink() as.IRI {
return as.IRI(l.ID)
}
// GetType returns the LikedCollection's type
func (l LikedCollection) GetType() as.ActivityVocabularyType {
return l.Type
}
// IsLink returns false for an LikedCollection object
func (l LikedCollection) IsLink() bool {
return false
}
// IsObject returns true for a LikedCollection object
func (l LikedCollection) IsObject() bool {
return true
}
// GetID returns the ObjectID corresponding to the Liked
func (l Liked) GetID() *as.ObjectID {
return l.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Liked object
func (l Liked) GetLink() as.IRI {
return as.IRI(l.ID)
}
// GetType returns the Liked's type
func (l Liked) GetType() as.ActivityVocabularyType {
return l.Type
}
// IsLink returns false for an Liked object
func (l Liked) IsLink() bool {
return false
}
// IsObject returns true for a Liked object
func (l Liked) IsObject() bool {
return true
}
// Collection returns the underlying Collection type
func (l Liked) Collection() as.CollectionInterface {
c := as.OrderedCollection(l)
return &c
}
// Collection returns the underlying Collection type
func (l LikedCollection) Collection() as.CollectionInterface {
c := as.OrderedCollection(l)
return &c
}

View file

@ -1,15 +1,13 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
"reflect"
"testing"
)
func TestLikedNew(t *testing.T) {
l := LikedNew()
id := as.ObjectID("liked")
id := ObjectID("liked")
if l.ID != id {
t.Errorf("%T should be initialized with %q as %T", l, id, id)
}
@ -26,96 +24,3 @@ func TestLikedNew(t *testing.T) {
t.Errorf("%T should be initialized with 0 TotalItems", l)
}
}
func TestLikedCollection_GetID(t *testing.T) {
l := LikedCollection{}
if *l.GetID() != "" {
t.Errorf("%T should be initialized with empty %T", l, l.GetID())
}
id := as.ObjectID("test_out_stream")
l.ID = id
if *l.GetID() != id {
t.Errorf("%T should have %T as %q", l, id, id)
}
}
func TestLikedCollection_GetType(t *testing.T) {
l := LikedCollection{}
if l.GetType() != "" {
t.Errorf("%T should be initialized with empty %T", l, l.GetType())
}
l.Type = as.OrderedCollectionType
if l.GetType() != as.OrderedCollectionType {
t.Errorf("%T should have %T as %q", l, l.GetType(), as.OrderedCollectionType)
}
}
func TestLikedCollection_Append(t *testing.T) {
l := LikedCollection{}
val := as.Object{ID: as.ObjectID("grrr")}
l.Append(val)
if l.TotalItems != 1 {
t.Errorf("%T should have exactly an element, found %d", l, l.TotalItems)
}
if !reflect.DeepEqual(l.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", l, l.OrderedItems, val.ID)
}
}
func TestLiked_Append(t *testing.T) {
l := LikedNew()
val := as.Object{ID: as.ObjectID("grrr")}
l.Append(val)
if l.TotalItems != 0 {
t.Errorf("%T should have exactly an element, found %d", l, l.TotalItems)
}
if !reflect.DeepEqual(l.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", l, l.OrderedItems, val.ID)
}
}
func TestLiked_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestLiked_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestLiked_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLiked_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestLiked_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLiked_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestLikedCollection_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestLikedCollection_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikedCollection_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikedCollection_IsObject(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -1,103 +1,25 @@
package activitypub
import as "github.com/go-ap/activitystreams"
type (
// LikesCollection is a list of all Like activities with this object as the object property,
// added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
// and MAY be filtered on privileges of an authenticated user or as appropriate when
// no authentication is given.
LikesCollection Likes
LikesCollection = Likes
// Likes is a type alias for an Ordered Collection
Likes as.OrderedCollection
Likes = OrderedCollection
)
// LikesCollection initializes a new Outbox
func LikesNew() *Likes {
id := as.ObjectID("likes")
id := ObjectID("likes")
l := Likes{Parent: as.Parent{ID: id, Type: as.CollectionType}}
l.Name = as.NaturalLanguageValuesNew()
l.Content = as.NaturalLanguageValuesNew()
l := Likes{ID: id, Type: CollectionType}
l.Name = NaturalLanguageValuesNew()
l.Content = NaturalLanguageValuesNew()
l.TotalItems = 0
return &l
}
// Append adds an element to an LikesCollection
func (l *LikesCollection) Append(o as.Item) error {
l.OrderedItems = append(l.OrderedItems, o)
l.TotalItems++
return nil
}
// Append adds an element to an Outbox
func (l *Likes) Append(ob as.Item) error {
l.OrderedItems = append(l.OrderedItems, ob)
l.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to the LikesCollection
func (l LikesCollection) GetID() *as.ObjectID {
return l.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current LikesCollection object
func (l LikesCollection) GetLink() as.IRI {
return as.IRI(l.ID)
}
// GetType returns the LikesCollection's type
func (l LikesCollection) GetType() as.ActivityVocabularyType {
return l.Type
}
// IsLink returns false for an LikesCollection object
func (l LikesCollection) IsLink() bool {
return false
}
// IsObject returns true for a LikesCollection object
func (l LikesCollection) IsObject() bool {
return true
}
// GetID returns the ObjectID corresponding to the Likes
func (l Likes) GetID() *as.ObjectID {
return l.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Likes object
func (l Likes) GetLink() as.IRI {
return as.IRI(l.ID)
}
// GetType returns the Likes's type
func (l Likes) GetType() as.ActivityVocabularyType {
return l.Type
}
// IsLink returns false for an Likes object
func (l Likes) IsLink() bool {
return false
}
// IsObject returns true for a Likes object
func (l Likes) IsObject() bool {
return true
}
// Collection returns the underlying Collection type
func (l Likes) Collection() as.CollectionInterface {
c := as.OrderedCollection(l)
return &c
}
// Collection returns the underlying Collection type
func (l LikesCollection) Collection() as.CollectionInterface {
c := as.OrderedCollection(l)
return &c
}

View file

@ -2,62 +2,6 @@ package activitypub
import "testing"
func TestLikes_Append(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikes_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_Append(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesCollection_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestLikesNew(t *testing.T) {
t.Skipf("TODO")
}

122
link.go Normal file
View file

@ -0,0 +1,122 @@
package activitypub
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
link_test.go Normal file
View file

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

808
object.go
View file

@ -1,37 +1,772 @@
package activitypub
import (
"errors"
"bytes"
"fmt"
"github.com/buger/jsonparser"
as "github.com/go-ap/activitystreams"
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
}
// 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
}
o.Likes = JSONGetItem(data, "likes")
o.Shares = JSONGetItem(data, "shares")
o.Source = GetAPSource(data)
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 IRI
if cur.IsObject() {
testIt = IRI(cur.GetID())
} else if cur.IsLink() {
testIt = cur.GetLink()
} else {
continue
}
for _, it := range rec {
if testIt.Equals(IRI(it.GetID()), false) {
// mark the element for removal
toRemove = append(toRemove, i)
save = false
}
}
if save {
rec = append(rec, testIt)
}
}
sort.Sort(sort.Reverse(sort.IntSlice(toRemove)))
for _, idx := range toRemove {
*recCol = append((*recCol)[:idx], (*recCol)[idx+1:]...)
}
}
return rec, 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
}
// Source is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
type Source struct {
// Content
Content as.NaturalLanguageValues `jsonld:"content"`
Content NaturalLanguageValues `jsonld:"content"`
// MediaType
MediaType as.MimeType `jsonld:"mediaType"`
}
type Parent = Object
// Object
type Object struct {
as.Parent
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes as.Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares as.Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
MediaType MimeType `jsonld:"mediaType"`
}
// GetAPSource
@ -53,30 +788,3 @@ func (s *Source) UnmarshalJSON(data []byte) error {
*s = GetAPSource(data)
return nil
}
// UnmarshalJSON
func (o *Object) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
o.Parent.UnmarshalJSON(data)
o.Likes = as.JSONGetItem(data, "likes")
o.Shares = as.JSONGetItem(data, "shares")
o.Source = GetAPSource(data)
return nil
}
// ToObject
func ToObject(it as.Item) (*Object, error) {
switch i := it.(type) {
case *as.Object:
return &Object{Parent: *i}, nil
case as.Object:
return &Object{Parent: i}, nil
case *Object:
return i, nil
case Object:
return &i, nil
}
return nil, errors.New("unable to convert object")
}

View file

@ -1,39 +1,547 @@
package activitypub
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("Unmarshalled object %T should have empty ID, received %q", o, o.ID)
t.Errorf("Unmarshaled object %T should have empty ID, received %q", o, o.ID)
}
if o.Type != "" {
t.Errorf("Unmarshalled object %T should have empty Type, received %q", o, o.Type)
t.Errorf("Unmarshaled object %T should have empty Type, received %q", o, o.Type)
}
if o.AttributedTo != nil {
t.Errorf("Unmarshalled object %T should have empty AttributedTo, received %q", o, o.AttributedTo)
t.Errorf("Unmarshaled object %T should have empty AttributedTo, received %q", o, o.AttributedTo)
}
if len(o.Name) != 0 {
t.Errorf("Unmarshalled object %T should have empty Name, received %q", o, o.Name)
t.Errorf("Unmarshaled object %T should have empty Name, received %q", o, o.Name)
}
if len(o.Summary) != 0 {
t.Errorf("Unmarshalled object %T should have empty Summary, received %q", o, o.Summary)
t.Errorf("Unmarshaled object %T should have empty Summary, received %q", o, o.Summary)
}
if len(o.Content) != 0 {
t.Errorf("Unmarshalled object %T should have empty Content, received %q", o, o.Content)
t.Errorf("Unmarshaled object %T should have empty Content, received %q", o, o.Content)
}
if o.URL != nil {
t.Errorf("Unmarshalled object %T should have empty URL, received %v", o, o.URL)
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("Unmarshalled object %T should have empty Published, received %q", o, o.Published)
t.Errorf("Unmarshaled object %T should have empty Published, received %q", o, o.Published)
}
if !o.StartTime.IsZero() {
t.Errorf("Unmarshalled object %T should have empty StartTime, received %q", o, o.StartTime)
t.Errorf("Unmarshaled object %T should have empty StartTime, received %q", o, o.StartTime)
}
if !o.Updated.IsZero() {
t.Errorf("Unmarshalled object %T should have empty Updated, received %q", o, o.Updated)
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)
}
validateEmptySource(o.Source, t)
}
@ -55,6 +563,384 @@ func TestObject_UnmarshalJSON(t *testing.T) {
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")
}
func TestSource_UnmarshalJSON(t *testing.T) {
s := Source{}

272
ordered_collection.go Normal file
View file

@ -0,0 +1,272 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// In a paged Collection, indicates the page that contains the most recently updated member items.
Current ObjectOrLink `jsonld:"current,omitempty"`
// In a paged Collection, indicates the furthest 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
}

273
ordered_collection_page.go Normal file
View file

@ -0,0 +1,273 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// In a paged Collection, indicates the page that contains the most recently updated member items.
Current ObjectOrLink `jsonld:"current,omitempty"`
// In a paged Collection, indicates the furthest 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 activitypub
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")
}

218
ordered_collection_test.go Normal file
View file

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

View file

@ -1,7 +1,5 @@
package activitypub
import as "github.com/go-ap/activitystreams"
type (
// OutboxStream contains activities the user has published,
// subject to the ability of the requestor to retrieve the activity (that is,
@ -9,66 +7,18 @@ type (
OutboxStream = Outbox
// Outbox is a type alias for an Ordered Collection
Outbox as.OrderedCollection
Outbox = OrderedCollection
)
// OutboxNew initializes a new Outbox
func OutboxNew() *Outbox {
id := as.ObjectID("outbox")
i := Outbox{Parent: as.Parent{ID: id, Type: as.CollectionType}}
i.Name = as.NaturalLanguageValuesNew()
i.Content = as.NaturalLanguageValuesNew()
id := ObjectID("outbox")
i := Outbox{ID: id, Type: OrderedCollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
i.OrderedItems = make(ItemCollection, 0)
return &i
}
// Append adds an element to an Outbox
func (o *Outbox) Append(ob as.Item) error {
o.OrderedItems = append(o.OrderedItems, ob)
o.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to Outbox
func (o Outbox) GetID() *as.ObjectID {
return o.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Outbox object
func (o Outbox) GetLink() as.IRI {
return as.IRI(o.ID)
}
// GetType returns the Outbox's type
func (o Outbox) GetType() as.ActivityVocabularyType {
return o.Type
}
// IsLink returns false for an Outbox object
func (o Outbox) IsLink() bool {
return false
}
// IsObject returns true for a Outbox object
func (o Outbox) IsObject() bool {
return true
}
// UnmarshalJSON
func (o *Outbox) UnmarshalJSON(data []byte) error {
c := as.OrderedCollection(*o)
err := c.UnmarshalJSON(data)
*o = Outbox(c)
return err
}
// Collection returns the underlying Collection type
func (o Outbox) Collection() as.CollectionInterface {
c := as.OrderedCollection(o)
return &c
}

View file

@ -3,14 +3,12 @@ package activitypub
import (
"reflect"
"testing"
as "github.com/go-ap/activitystreams"
)
func TestOutboxNew(t *testing.T) {
o := OutboxNew()
id := as.ObjectID("outbox")
id := ObjectID("outbox")
if o.ID != id {
t.Errorf("%T should be initialized with %q as %T", o, id, id)
}
@ -30,12 +28,12 @@ func TestOutboxNew(t *testing.T) {
func TestOutboxStream_GetID(t *testing.T) {
o := OutboxStream{}
if *o.GetID() != "" {
if o.GetID() != "" {
t.Errorf("%T should be initialized with empty %T", o, o.GetID())
}
id := as.ObjectID("test_out_stream")
id := ObjectID("test_out_stream")
o.ID = id
if *o.GetID() != id {
if o.GetID() != id {
t.Errorf("%T should have %T as %q", o, id, id)
}
}
@ -47,21 +45,18 @@ func TestOutboxStream_GetType(t *testing.T) {
t.Errorf("%T should be initialized with empty %T", o, o.GetType())
}
o.Type = as.OrderedCollectionType
if o.GetType() != as.OrderedCollectionType {
t.Errorf("%T should have %T as %q", o, o.GetType(), as.OrderedCollectionType)
o.Type = OrderedCollectionType
if o.GetType() != OrderedCollectionType {
t.Errorf("%T should have %T as %q", o, o.GetType(), OrderedCollectionType)
}
}
func TestOutboxStream_Append(t *testing.T) {
o := OutboxStream{}
val := as.Object{ID: as.ObjectID("grrr")}
val := Object{ID: ObjectID("grrr")}
o.Append(val)
if o.TotalItems != 1 {
t.Errorf("%T should have exactly an element, found %d", o, o.TotalItems)
}
if !reflect.DeepEqual(o.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", o, o.OrderedItems, val.ID)
}
@ -70,12 +65,9 @@ func TestOutboxStream_Append(t *testing.T) {
func TestOutbox_Append(t *testing.T) {
o := OutboxNew()
val := as.Object{ID: as.ObjectID("grrr")}
val := Object{ID: ObjectID("grrr")}
o.Append(val)
if o.TotalItems != 1 {
t.Errorf("%T should have exactly an element, found %d", o, o.TotalItems)
}
if !reflect.DeepEqual(o.OrderedItems[0], val) {
t.Errorf("First item in %T.%T does not match %q", o, o.OrderedItems, val.ID)
}

251
place.go Normal file
View file

@ -0,0 +1,251 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// Accuracy indicates the accuracy of position coordinates on a Place objects.
// Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
Accuracy float64
// 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
place_test.go Normal file
View file

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

228
profile.go Normal file
View file

@ -0,0 +1,228 @@
package activitypub
import (
"fmt"
"time"
"unsafe"
)
// Profile a Profile is a content object that describes another Object,
// typically used to describe CanReceiveActivities Type objects.
// The describes property is used to reference the object being described by the profile.
type Profile struct {
// ID provides the globally unique identifier for anActivity Pub Object or Link.
ID 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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// Describes On a Profile object, the describes property identifies the object described by the Profile.
Describes Item `jsonld:"describes,omitempty"`
}
// IsLink returns false for Profile objects
func (p Profile) IsLink() bool {
return false
}
// IsObject returns true for Profile objects
func (p Profile) IsObject() bool {
return true
}
// IsCollection returns false for Profile objects
func (p Profile) IsCollection() bool {
return false
}
// GetLink returns the IRI corresponding to the current Profile object
func (p Profile) GetLink() IRI {
return IRI(p.ID)
}
// GetType returns the type of the current Profile
func (p Profile) GetType() ActivityVocabularyType {
return p.Type
}
// GetID returns the ID corresponding to the current Profile
func (p Profile) GetID() 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
profile_test.go Normal file
View file

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

249
question.go Normal file
View file

@ -0,0 +1,249 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
Actor CanReceiveActivities `jsonld:"actor,omitempty"`
// Target describes the indirect object, or target, of the activity.
// The precise meaning of the target is largely dependent on the type of action being described
// but will often be the object of the English preposition "to".
// For instance, in the activity "John added a movie to his wishlist",
// the target of the activity is John's wishlist. An activity can have more than one target.
Target Item `jsonld:"target,omitempty"`
// Result describes the result of the activity. For instance, if a particular action results in the creation
// of a new resource, the result property can be used to describe that new resource.
Result Item `jsonld:"result,omitempty"`
// Origin describes an indirect object of the activity from which the activity is directed.
// The precise meaning of the origin is the object of the English preposition "from".
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
Origin Item `jsonld:"origin,omitempty"`
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
Instrument Item `jsonld:"instrument,omitempty"`
// OneOf identifies an exclusive option for a Question. Use of oneOf implies that the Question
// can have only a single answer. To indicate that a Question can have multiple answers, use anyOf.
OneOf Item `jsonld:"oneOf,omitempty"`
// AnyOf identifies an inclusive option for a Question. Use of anyOf implies that the Question can have multiple answers.
// To indicate that a Question can have only one answer, use oneOf.
AnyOf Item `jsonld:"anyOf,omitempty"`
// Closed indicates that a question has been closed, and answers are no longer accepted.
Closed bool `jsonld:"closed,omitempty"`
}
// GetID returns the 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
question_test.go Normal file
View file

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

241
relationship.go Normal file
View file

@ -0,0 +1,241 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// Subject 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())
}

35
relationship_test.go Normal file
View file

@ -0,0 +1,35 @@
package activitypub
import "testing"
func TestRelationship_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_IsCollection(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}
func TestRelationship_Clean(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -1,7 +1,5 @@
package activitypub
import as "github.com/go-ap/activitystreams"
type (
// SharesCollection is a list of all Announce activities with this object as the object property,
// added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection
@ -10,69 +8,18 @@ type (
SharesCollection = Shares
// Shares is a type alias for an Ordered Collection
Shares as.OrderedCollection
Shares = OrderedCollection
)
// SharesNew initializes a new Shares
func SharesNew() *Shares {
id := as.ObjectID("Shares")
id := ObjectID("Shares")
i := Shares{Parent: as.Parent{ID: id, Type: as.CollectionType}}
i.Name = as.NaturalLanguageValuesNew()
i.Content = as.NaturalLanguageValuesNew()
i := Shares{ID: id, Type: CollectionType}
i.Name = NaturalLanguageValuesNew()
i.Content = NaturalLanguageValuesNew()
i.TotalItems = 0
return &i
}
// Append adds an element to an Shares
func (o *Shares) Append(ob as.Item) error {
o.OrderedItems = append(o.OrderedItems, ob)
o.TotalItems++
return nil
}
// GetID returns the ObjectID corresponding to Shares
func (o Shares) GetID() *as.ObjectID {
return o.Collection().GetID()
}
// GetLink returns the IRI corresponding to the current Shares object
func (o Shares) GetLink() as.IRI {
return as.IRI(o.ID)
}
// GetType returns the Shares's type
func (o Shares) GetType() as.ActivityVocabularyType {
return o.Type
}
// IsLink returns false for an Shares object
func (o Shares) IsLink() bool {
return false
}
// IsObject returns true for a Shares object
func (o Shares) IsObject() bool {
return true
}
// UnmarshalJSON
func (o *Shares) UnmarshalJSON(data []byte) error {
if as.ItemTyperFunc == nil {
as.ItemTyperFunc = JSONGetItemByType
}
c := as.OrderedCollection(*o)
err := c.UnmarshalJSON(data)
*o = Shares(c)
return err
}
// Collection returns the underlying Collection type
func (o Shares) Collection() as.CollectionInterface {
c := as.OrderedCollection(o)
return &c
}

View file

@ -2,38 +2,6 @@ package activitypub
import "testing"
func TestShares_Append(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_Collection(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_GetID(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_GetLink(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_GetType(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_IsLink(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_IsObject(t *testing.T) {
t.Skipf("TODO")
}
func TestShares_UnmarshalJSON(t *testing.T) {
t.Skipf("TODO")
}
func TestSharesNew(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -3,15 +3,15 @@ package tests
import (
"testing"
a "github.com/go-ap/activitystreams"
j "github.com/go-ap/jsonld"
pub "github.com/go-ap/activitypub"
"strings"
)
func TestAcceptSerialization(t *testing.T) {
obj := a.AcceptNew("https://localhost/myactivity", nil)
obj.Name = make(a.NaturalLanguageValues, 1)
obj := pub.AcceptNew("https://localhost/myactivity", nil)
obj.Name = make(pub.NaturalLanguageValues, 1)
obj.Name.Set("en", "test")
obj.Name.Set("fr", "teste")
@ -41,11 +41,11 @@ func TestAcceptSerialization(t *testing.T) {
}
func TestCreateActivityHTTPSerialization(t *testing.T) {
id := a.ObjectID("test_object")
obj := a.AcceptNew(id, nil)
id := pub.ObjectID("test_object")
obj := pub.AcceptNew(id, nil)
obj.Name.Set("en", "Accept New")
uri := string(a.ActivityBaseURI)
uri := string(pub.ActivityBaseURI)
data, err := j.WithContext(j.IRI(uri)).Marshal(obj)
if err != nil {

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,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

@ -4,7 +4,7 @@ package tests
import (
"fmt"
a "github.com/go-ap/activitystreams"
pub "github.com/go-ap/activitypub"
"testing"
)
@ -22,9 +22,9 @@ S2S Server: Activities requiring the object property
`
t.Log(desc)
obj := a.MentionNew("gigel")
obj := pub.MentionNew("gigel")
add := a.AddNew("https://localhost/myactivity", obj, nil)
add := pub.AddNew("https://localhost/myactivity", obj, nil)
if add.Object == nil {
t.Errorf("Missing GetID in Add activity %#v", add.Object)
}
@ -32,7 +32,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Add.GetID different than what we initialized %#v %#v", add.Object, obj)
}
block := a.BlockNew("https://localhost/myactivity", obj)
block := pub.BlockNew("https://localhost/myactivity", obj)
if block.Object == nil {
t.Errorf("Missing GetID in Add activity %#v", block.Object)
}
@ -40,7 +40,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Block.GetID different than what we initialized %#v %#v", block.Object, obj)
}
create := a.CreateNew("https://localhost/myactivity", obj)
create := pub.CreateNew("https://localhost/myactivity", obj)
if create.Object == nil {
t.Errorf("Missing GetID in Add activity %#v", create.Object)
}
@ -48,7 +48,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Create.GetID different than what we initialized %#v %#v", create.Object, obj)
}
delete := a.DeleteNew("https://localhost/myactivity", obj)
delete := pub.DeleteNew("https://localhost/myactivity", obj)
if delete.Object == nil {
t.Errorf("Missing GetID in Delete activity %#v", delete.Object)
}
@ -56,7 +56,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Delete.GetID different than what we initialized %#v %#v", delete.Object, obj)
}
follow := a.FollowNew("https://localhost/myactivity", obj)
follow := pub.FollowNew("https://localhost/myactivity", obj)
if follow.Object == nil {
t.Errorf("Missing GetID in Follow activity %#v", follow.Object)
}
@ -64,7 +64,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Follow.GetID different than what we initialized %#v %#v", follow.Object, obj)
}
like := a.LikeNew("https://localhost/myactivity", obj)
like := pub.LikeNew("https://localhost/myactivity", obj)
if like.Object == nil {
t.Errorf("Missing GetID in Like activity %#v", like.Object)
}
@ -72,7 +72,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Like.GetID different than what we initialized %#v %#v", add.Object, obj)
}
update := a.UpdateNew("https://localhost/myactivity", obj)
update := pub.UpdateNew("https://localhost/myactivity", obj)
if update.Object == nil {
t.Errorf("Missing GetID in Update activity %#v", update.Object)
}
@ -80,7 +80,7 @@ S2S Server: Activities requiring the object property
t.Errorf("Update.GetID different than what we initialized %#v %#v", update.Object, obj)
}
undo := a.UndoNew("https://localhost/myactivity", obj)
undo := pub.UndoNew("https://localhost/myactivity", obj)
if undo.Object == nil {
t.Errorf("Missing GetID in Undo activity %#v", undo.Object)
}
@ -103,10 +103,10 @@ property: Add, Remove.
`
t.Log(desc)
obj := a.MentionNew("foo")
target := a.MentionNew("bar")
obj := pub.MentionNew("foo")
target := pub.MentionNew("bar")
add := a.AddNew("https://localhost/myactivity", obj, target)
add := pub.AddNew("https://localhost/myactivity", obj, target)
if add.Target == nil {
t.Errorf("Missing Target in Add activity %#v", add.Target)
}
@ -114,7 +114,7 @@ property: Add, Remove.
t.Errorf("Add.Target different than what we initialized %#v %#v", add.Target, target)
}
remove := a.RemoveNew("https://localhost/myactivity", obj, target)
remove := pub.RemoveNew("https://localhost/myactivity", obj, target)
if remove.Target == nil {
t.Errorf("Missing Target in Remove activity %#v", remove.Target)
}
@ -141,32 +141,32 @@ S2S Server: Deduplication of recipient list
`
t.Log(desc)
to := a.PersonNew("bob")
o := a.ObjectNew(a.ArticleType)
cc := a.PersonNew("alice")
to := pub.PersonNew("bob")
o := pub.ObjectNew(pub.ArticleType)
cc := pub.PersonNew("alice")
o.ID = "something"
c := a.CreateNew("create", o)
c := pub.CreateNew("create", o)
c.To.Append(to)
c.CC.Append(cc)
c.BCC.Append(cc)
c.Recipients()
checkDedup := func(list a.ItemCollection, recIds *[]a.ObjectID) error {
checkDedup := func(list pub.ItemCollection, recIds *[]pub.ObjectID) error {
for _, rec := range list {
for _, id := range *recIds {
if *rec.GetID() == id {
if rec.GetID() == id {
return fmt.Errorf("%T[%s] already stored in recipients list, Deduplication faild", rec, id)
}
}
*recIds = append(*recIds, *rec.GetID())
*recIds = append(*recIds, rec.GetID())
}
return nil
}
var err error
recIds := make([]a.ObjectID, 0)
recIds := make([]pub.ObjectID, 0)
err = checkDedup(c.To, &recIds)
if err != nil {
t.Error(err)
@ -197,15 +197,15 @@ Activity being notified about
`
t.Log(desc)
p := a.PersonNew("main actor")
p := pub.PersonNew("main actor")
to := a.PersonNew("bob")
o := a.ObjectNew(a.ArticleType)
cc := a.PersonNew("alice")
to := pub.PersonNew("bob")
o := pub.ObjectNew(pub.ArticleType)
cc := pub.PersonNew("alice")
o.ID = "something"
c := a.CreateNew("create", o)
c.Actor = a.Actor(*p)
c := pub.CreateNew("create", o)
c.Actor = *p
c.To.Append(p)
c.To.Append(to)
@ -216,10 +216,10 @@ Activity being notified about
c.Recipients()
checkActor := func(list a.ItemCollection, actor a.Item) error {
checkActor := func(list pub.ItemCollection, actor pub.Item) error {
for _, rec := range list {
if rec.GetID() == actor.GetID() {
return fmt.Errorf("%T[%s] Actor of activity should not be in the recipients list", rec, *actor.GetID())
return fmt.Errorf("%T[%s] Actor of activity should not be in the recipients list", rec, actor.GetID())
}
}
return nil
@ -254,13 +254,13 @@ S2S Server: Do-not-deliver considerations
`
t.Log(desc)
p := a.PersonNew("blocked")
p := pub.PersonNew("blocked")
bob := a.PersonNew("bob")
jane := a.PersonNew("jane doe")
bob := pub.PersonNew("bob")
jane := pub.PersonNew("jane doe")
b := a.BlockNew("block actor", p)
b.Actor = a.Actor(*bob)
b := pub.BlockNew("block actor", p)
b.Actor = *bob
b.To.Append(jane)
b.To.Append(p)
@ -268,10 +268,10 @@ S2S Server: Do-not-deliver considerations
b.Recipients()
checkActor := func(list a.ItemCollection, ob a.Item) error {
checkActor := func(list pub.ItemCollection, ob pub.Item) error {
for _, rec := range list {
if rec.GetID() == ob.GetID() {
return fmt.Errorf("%T[%s] of activity should not be in the recipients list", rec, *ob.GetID())
return fmt.Errorf("%T[%s] of activity should not be in the recipients list", rec, ob.GetID())
}
}
return nil

546
tests/unmarshal.go Normal file
View file

@ -0,0 +1,546 @@
package tests
import (
"bytes"
"fmt"
pub "github.com/go-ap/activitypub"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"unsafe"
j "github.com/go-ap/jsonld"
)
const dir = "./mocks"
var stopOnFailure = false
type testPair struct {
expected bool
blank interface{}
result interface{}
}
type testMaps map[string]testPair
type visit struct {
a1 unsafe.Pointer
a2 unsafe.Pointer
typ reflect.Type
}
type canErrorFunc func(format string, args ...interface{})
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 = testMaps{
"empty": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{},
},
"link_simple": testPair{
expected: true,
blank: &pub.Link{},
result: &pub.Link{
Type: pub.LinkType,
Href: pub.IRI("http://example.org/abc"),
HrefLang: pub.LangRef("en"),
MediaType: pub.MimeType("text/html"),
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "An example link",
}},
},
},
"object_with_url": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
URL: pub.IRI("http://littr.git/api/accounts/system"),
},
},
"object_with_url_collection": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
URL: pub.ItemCollection{
pub.IRI("http://littr.git/api/accounts/system"),
pub.IRI("http://littr.git/~system"),
},
},
},
"object_simple": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
Type: pub.ObjectType,
ID: pub.ObjectID("http://www.test.example/object/1"),
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "A Simple, non-specific object",
}},
},
},
"object_with_tags": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
Type: pub.ObjectType,
ID: pub.ObjectID("http://www.test.example/object/1"),
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "A Simple, non-specific object",
}},
Tag: pub.ItemCollection{
&pub.Mention{
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "#my_tag",
}},
Type: pub.MentionType,
ID: pub.ObjectID("http://example.com/tag/my_tag"),
},
&pub.Mention{
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "@ana",
}},
Type: pub.MentionType,
ID: pub.ObjectID("http://example.com/users/ana"),
},
},
},
},
"object_with_replies": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
Type: pub.ObjectType,
ID: pub.ObjectID("http://www.test.example/object/1"),
Replies: &pub.Collection{
ID: pub.ObjectID("http://www.test.example/object/1/replies"),
Type: pub.CollectionType,
TotalItems: 1,
Items: pub.ItemCollection{
&pub.Object{
ID: pub.ObjectID("http://www.test.example/object/1/replies/2"),
Type: pub.ArticleType,
Name: pub.NaturalLanguageValues{{
pub.NilLangRef, "Example title",
}},
},
},
},
},
},
"activity_simple": testPair{
expected: true,
blank: &pub.Activity{
Actor: &pub.Person{},
},
result: &pub.Activity{
Type: pub.ActivityType,
Summary: pub.NaturalLanguageValues{{pub.NilLangRef, "Sally did something to a note"}},
Actor: &pub.Person{
Type: pub.PersonType,
Name: pub.NaturalLanguageValues{{pub.NilLangRef, "Sally"}},
},
Object: &pub.Object{
Type: pub.NoteType,
Name: pub.NaturalLanguageValues{{pub.NilLangRef, "A Note"}},
},
},
},
"person_with_outbox": testPair{
expected: true,
blank: &pub.Person{},
result: &pub.Person{
ID: pub.ObjectID("http://example.com/accounts/ana"),
Type: pub.PersonType,
Name: pub.NaturalLanguageValues{{pub.NilLangRef, "ana"}},
PreferredUsername: pub.NaturalLanguageValues{{pub.NilLangRef, "ana"}},
URL: pub.IRI("http://example.com/accounts/ana"),
Outbox: &pub.OrderedCollection{
ID: "http://example.com/accounts/ana/outbox",
Type: pub.OrderedCollectionType,
URL: pub.IRI("http://example.com/outbox"),
},
},
},
"ordered_collection": testPair{
expected: true,
blank: &pub.OrderedCollection{},
result: &pub.OrderedCollection{
ID: pub.ObjectID("http://example.com/outbox"),
Type: pub.OrderedCollectionType,
URL: pub.IRI("http://example.com/outbox"),
TotalItems: 1,
OrderedItems: pub.ItemCollection{
&pub.Object{
ID: pub.ObjectID("http://example.com/outbox/53c6fb47"),
Type: pub.ArticleType,
Name: pub.NaturalLanguageValues{{pub.NilLangRef, "Example title"}},
Content: pub.NaturalLanguageValues{{pub.NilLangRef, "Example content!"}},
URL: pub.IRI("http://example.com/53c6fb47"),
MediaType: pub.MimeType("text/markdown"),
Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
Generator: pub.IRI("http://example.com"),
AttributedTo: pub.IRI("http://example.com/accounts/alice"),
},
},
},
},
"ordered_collection_page": testPair{
expected: true,
blank: &pub.OrderedCollectionPage{},
result: &pub.OrderedCollectionPage{
PartOf: pub.IRI("http://example.com/outbox"),
Next: pub.IRI("http://example.com/outbox?page=3"),
Prev: pub.IRI("http://example.com/outbox?page=1"),
ID: pub.ObjectID("http://example.com/outbox?page=2"),
Type: pub.OrderedCollectionPageType,
URL: pub.IRI("http://example.com/outbox?page=2"),
Current: pub.IRI("http://example.com/outbox?page=2"),
TotalItems: 1,
OrderedItems: pub.ItemCollection{
&pub.Object{
ID: pub.ObjectID("http://example.com/outbox/53c6fb47"),
Type: pub.ArticleType,
Name: pub.NaturalLanguageValues{{pub.NilLangRef, "Example title"}},
Content: pub.NaturalLanguageValues{{pub.NilLangRef, "Example content!"}},
URL: pub.IRI("http://example.com/53c6fb47"),
MediaType: pub.MimeType("text/markdown"),
Published: time.Date(2018, time.July, 5, 16, 46, 44, 0, zLoc),
Generator: pub.IRI("http://example.com"),
AttributedTo: pub.IRI("http://example.com/accounts/alice"),
},
},
},
},
"natural_language_values": {
expected: true,
blank: &pub.NaturalLanguageValues{},
result: &pub.NaturalLanguageValues{
{
pub.NilLangRef, `
`},
{pub.LangRef("en"), "Ana got apples ⓐ"},
{pub.LangRef("fr"), "Aná a des pommes ⒜"},
{pub.LangRef("ro"), "Ana are mere"},
},
},
"activity_create_simple": {
expected: true,
blank: &pub.Create{},
result: &pub.Create{
Type: pub.CreateType,
Actor: pub.IRI("https://littr.git/api/accounts/anonymous"),
Object: &pub.Object{
Type: pub.NoteType,
AttributedTo: pub.IRI("https://littr.git/api/accounts/anonymous"),
InReplyTo: pub.ItemCollection{pub.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff")},
Content: pub.NaturalLanguageValues{{pub.NilLangRef, "<p>Hello world</p>"}},
To: pub.ItemCollection{pub.IRI("https://www.w3.org/ns/activitystreams#Public")},
},
},
},
"activity_create_multiple_objects": {
expected: true,
blank: &pub.Create{},
result: &pub.Create{
Type: pub.CreateType,
Actor: pub.IRI("https://littr.git/api/accounts/anonymous"),
Object:pub.ItemCollection{
&pub.Object{
Type: pub.NoteType,
AttributedTo: pub.IRI("https://littr.git/api/accounts/anonymous"),
InReplyTo: pub.ItemCollection{pub.IRI("https://littr.git/api/accounts/system/outbox/7ca154ff")},
Content: pub.NaturalLanguageValues{{pub.NilLangRef, "<p>Hello world</p>"}},
To: pub.ItemCollection{pub.IRI("https://www.w3.org/ns/activitystreams#Public")},
},
&pub.Article{
Type: pub.ArticleType,
ID: pub.ObjectID("http://www.test.example/article/1"),
Name: pub.NaturalLanguageValues{
{
pub.NilLangRef,
"This someday will grow up to be an article",
},
},
InReplyTo: pub.ItemCollection{
pub.IRI("http://www.test.example/object/1"),
pub.IRI("http://www.test.example/object/778"),
},
},
},
},
},
"object_with_audience": testPair{
expected: true,
blank: &pub.Object{},
result: &pub.Object{
Type: pub.ObjectType,
ID: pub.ObjectID("http://www.test.example/object/1"),
To: pub.ItemCollection{
pub.IRI("https://www.w3.org/ns/activitystreams#Public"),
},
Bto: pub.ItemCollection{
pub.IRI("http://example.com/sharedInbox"),
},
CC: pub.ItemCollection{
pub.IRI("https://example.com/actors/ana"),
pub.IRI("https://example.com/actors/bob"),
},
BCC: pub.ItemCollection{
pub.IRI("https://darkside.cookie/actors/darthvader"),
},
},
},
"article_with_multiple_inreplyto": {
expected: true,
blank: &pub.Article{},
result: &pub.Article{
Type: pub.ArticleType,
ID: pub.ObjectID("http://www.test.example/article/1"),
Name: pub.NaturalLanguageValues{
{
pub.NilLangRef,
"This someday will grow up to be an article",
},
},
InReplyTo: pub.ItemCollection{
pub.IRI("http://www.test.example/object/1"),
pub.IRI("http://www.test.example/object/778"),
},
},
},
}
func getFileContents(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
st, err := f.Stat()
if err != nil {
return nil, err
}
data := make([]byte, st.Size())
io.ReadFull(f, data)
data = bytes.Trim(data, "\x00")
return data, nil
}
func TestUnmarshal(t *testing.T) {
var err error
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)
//}
})
}
}

View file

@ -1,520 +0,0 @@
package tests
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"unsafe"
ap "github.com/go-ap/activitypub"
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: &ap.Object{},
result: &ap.Object{},
},
"object_with_url": testPair{
expected: true,
blank: &ap.Object{},
result: &ap.Object{Parent: a.Parent{URL: a.IRI("http://littr.git/api/accounts/system")}},
},
"object_with_url_collection": testPair{
expected: true,
blank: &ap.Object{},
result: &ap.Object{
Parent: a.Parent{
URL: a.ItemCollection{
a.IRI("http://littr.git/api/accounts/system"),
a.IRI("http://littr.git/~system"),
},
},
},
},
"object_simple": testPair{
expected: true,
blank: &ap.Object{},
result: &ap.Object{
Parent: a.Parent{
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: &ap.Object{},
result: &ap.Object{
Parent: a.Parent{
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",
}},
ID: a.ObjectID("http://example.com/tag/my_tag"),
Type: a.MentionType,
},
&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: &ap.Object{},
result: &ap.Object{
Parent: a.Parent{
Type: a.ObjectType,
ID: a.ObjectID("http://www.test.example/object/1"),
Replies: &a.Collection{
Parent: a.Parent{
ID: a.ObjectID("http://www.test.example/object/1/replies"),
Type: a.CollectionType,
},
TotalItems: 1,
Items: a.ItemCollection{
&ap.Object{
Parent: a.Parent{
ID: a.ObjectID("http://www.test.example/object/1/replies/2"),
Type: a.ArticleType,
Name: a.NaturalLanguageValues{{a.NilLangRef, "Example title"}},
},
},
},
},
},
},
},
"person_with_outbox": testPair{
expected: true,
blank: &ap.Person{},
result: &ap.Person{
Parent: ap.Parent{
Parent: a.Parent{
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"),
},
},
PreferredUsername: a.NaturalLanguageValues{{a.NilLangRef, "Ana"}},
Outbox: &a.OrderedCollection{
Parent: a.Parent{
ID: a.ObjectID("http://example.com/accounts/ana/outbox"),
Type: a.OrderedCollectionType,
URL: a.IRI("http://example.com/outbox"),
},
},
},
},
"ordered_collection": testPair{
expected: true,
blank: &a.OrderedCollection{},
result: &a.OrderedCollection{
Parent: a.Parent{
ID: a.ObjectID("http://example.com/outbox"),
Type: a.OrderedCollectionType,
URL: a.IRI("http://example.com/outbox"),
},
TotalItems: 1,
OrderedItems: a.ItemCollection{
&ap.Object{
Parent: a.Parent{
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"),
OrderedCollection: a.OrderedCollection{
Parent: a.Parent{
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{
&ap.Object{
Parent: a.Parent{
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{
Parent: a.Parent{
Type: a.CreateType,
},
Actor: a.IRI("https://littr.git/api/accounts/anonymous"),
Object: &ap.Object{
Parent: a.Parent{
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")},
},
},
},
},
"like_activity_with_iri_actor": {
expected: true,
blank: &a.Like{},
result: &a.Like{
Parent: a.Parent{
Type: a.LikeType,
Published: time.Date(2018, time.September, 6, 15, 15, 9, 0, zLoc),
},
Actor: a.IRI("https://littr.git/api/accounts/24d4b96f"),
Object: &ap.Object{
Parent: a.Article{
ID: a.ObjectID("https://littr.git/api/accounts/ana/liked/7ca154ff"),
Type: a.ArticleType,
},
},
},
},
"object_with_audience": testPair{
expected: true,
blank: &ap.Object{},
result: &ap.Object{
Parent: a.Parent{
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"),
},
},
},
},
}
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 Test_ActivityPubUnmarshal(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)
//}
})
}
}
func TestMain (m *testing.M) {
a.ItemTyperFunc = ap.JSONGetItemByType
m.Run()
}

232
tombstone.go Normal file
View file

@ -0,0 +1,232 @@
package activitypub
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"`
// This is a list of all Like activities with this object as the object property, added as a side effect.
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Likes Item `jsonld:"likes,omitempty"`
// This is a list of all Announce activities with this object as the object property, added as a side effect.
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
// of an authenticated user or as appropriate when no authentication is given.
Shares Item `jsonld:"shares,omitempty"`
// Source property is intended to convey some sort of source from which the content markup was derived,
// as a form of provenance, or to support future editing by clients.
// In general, clients do the conversion from source to content, not the other way around.
Source Source `jsonld:"source,omitempty"`
// FormerType On a Tombstone object, the formerType property identifies the type of the object that was deleted.
FormerType ActivityVocabularyType `jsonld:"formerType,omitempty"`
// Deleted On a Tombstone object, the deleted property is a timestamp for when the object was deleted.
Deleted time.Time `jsonld:"deleted,omitempty"`
}
// IsLink returns false for Tombstone objects
func (t Tombstone) IsLink() bool {
return false
}
// IsObject returns true for Tombstone objects
func (t Tombstone) IsObject() bool {
return true
}
// IsCollection returns false for Tombstone objects
func (t Tombstone) IsCollection() bool {
return false
}
// GetLink returns the IRI corresponding to the current Tombstone object
func (t Tombstone) GetLink() IRI {
return IRI(t.ID)
}
// GetType returns the type of the current Tombstone
func (t Tombstone) GetType() ActivityVocabularyType {
return t.Type
}
// GetID returns the ID corresponding to the current Tombstone
func (t Tombstone) GetID() 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
tombstone_test.go Normal file
View file

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

461
unmarshal.go Normal file
View file

@ -0,0 +1,461 @@
package activitypub
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 JSONGetBytes(data []byte, prop string) []byte {
val, _, _, err := jsonparser.Get(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 JSONGetPublicKey(data []byte, prop string) PublicKey {
key := PublicKey{}
key.UnmarshalJSON(JSONGetBytes(data, prop))
return key
}
func JSONGetStreams(data []byte, prop string) []ItemCollection {
return nil
}
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 &Actor{Type:typ}, nil
case CollectionType:
return &Collection{Type: typ}, nil
case OrderedCollectionType:
return &OrderedCollection{Type: typ}, nil
case CollectionPageType:
return &CollectionPage{Type: typ}, nil
case OrderedCollectionPageType:
return &OrderedCollectionPage{Type: typ}, nil
case 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 &Application{Type:typ}, nil
case GroupType:
return &Group{Type:typ}, nil
case OrganizationType:
return &Organization{Type:typ}, nil
case PersonType:
return &Person{Type:typ}, nil
case ServiceType:
return &Service{Type: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)
}
func JSONGetActorEndpoints(data []byte, prop string) *Endpoints {
str, _ := jsonparser.GetUnsafeString(data, prop)
var e *Endpoints
if len(str) > 0 {
e = &Endpoints{}
e.UnmarshalJSON([]byte(str))
}
return e
}

198
unmarshal_test.go Normal file
View file

@ -0,0 +1,198 @@
package activitypub
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 actorPtrType = reflect.TypeOf(new(*Actor)).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: actorPtrType,
ApplicationType: applicationPtrType,
ServiceType: servicePtrType,
PersonType: personPtrType,
GroupType: groupPtrType,
OrganizationType: organizationPtrType,
ActivityType: activityPtrType,
IntransitiveActivityType: intransitiveActivityPtrType,
AcceptType: acceptPtrType,
AddType: addPtrType,
AnnounceType: announcePtrType,
ArriveType: arrivePtrType,
BlockType: blockPtrType,
CreateType: createPtrType,
DeleteType: deletePtrType,
DislikeType: dislikePtrType,
FlagType: flagPtrType,
FollowType: followPtrType,
IgnoreType: ignorePtrType,
InviteType: invitePtrType,
JoinType: joinPtrType,
LeaveType: leavePtrType,
LikeType: likePtrType,
ListenType: listenPtrType,
MoveType: movePtrType,
OfferType: offerPtrType,
QuestionType: questionPtrType,
RejectType: rejectPtrType,
ReadType: readPtrType,
RemoveType: removePtrType,
TentativeRejectType: tentativeRejectPtrType,
TentativeAcceptType: tentativeAcceptPtrType,
TravelType: travelPtrType,
UndoType: undoPtrType,
UpdateType: updatePtrType,
ViewType: viewPtrType,
}
func TestJSONGetItemByType(t *testing.T) {
for typ, test := range tests {
t.Run(string(typ), func(t *testing.T) {
v, err := 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")
}
func TestJSONGetActorEndpoints(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -1,30 +0,0 @@
package activitypub
import (
"github.com/buger/jsonparser"
as "github.com/go-ap/activitystreams"
)
func JSONGetItemByType(typ as.ActivityVocabularyType) (as.Item, error) {
obTyp := as.ActivityVocabularyTypes{as.ObjectType}
if as.ObjectTypes.Contains(typ) || obTyp.Contains(typ) {
return &Object{Parent: as.Object{Type: typ}}, nil
}
actTyp := as.ActivityVocabularyTypes{as.ActorType}
if as.ActorTypes.Contains(typ) || actTyp.Contains(typ) {
return &actor{Parent: Parent{Parent: as.Object{Type: typ}}}, nil
}
return as.JSONGetItemByType(typ)
}
func JSONGetActorEndpoints(data []byte, prop string) *Endpoints {
str, _ := jsonparser.GetUnsafeString(data, prop)
var e *Endpoints
if len(str) > 0 {
e = &Endpoints{}
e.UnmarshalJSON([]byte(str))
}
return e
}

View file

@ -1,45 +0,0 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
"reflect"
"testing"
)
type testPairs map[as.ActivityVocabularyType]reflect.Type
var objectPtrType = reflect.TypeOf(new(*Object)).Elem()
var actorPtrType = reflect.TypeOf(new(*actor)).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 tests = testPairs{
as.ObjectType: objectPtrType,
as.ActorType: actorPtrType,
as.ApplicationType: applicationPtrType,
as.ServiceType: servicePtrType,
as.PersonType: personPtrType,
as.GroupType: groupPtrType,
as.OrganizationType: organizationPtrType,
}
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 TestJSONGetActorEndpoints(t *testing.T) {
t.Skipf("TODO")
}

View file

@ -1,9 +1,5 @@
package activitypub
import (
as "github.com/go-ap/activitystreams"
)
// ValidationErrors is an aggregated error interface that allows
// a Validator implementation to return all possible errors.
type ValidationErrors interface {
@ -16,10 +12,10 @@ type ValidationErrors interface {
// provide a validation mechanism for incoming ActivityPub Objects or IRIs
// against an external set of rules.
type Validator interface {
Validate(receiver as.IRI, incoming as.Item) (bool, ValidationErrors)
Validate(receiver IRI, incoming Item) (bool, ValidationErrors)
}
func (v defaultValidator) Validate(receiver as.IRI, incoming as.Item) (bool, ValidationErrors) {
func (v defaultValidator) Validate(receiver IRI, incoming Item) (bool, ValidationErrors) {
return true, nil
}