├── .build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── activity.go ├── activity_test.go ├── actor.go ├── actor_test.go ├── collection.go ├── collection_page.go ├── collection_page_test.go ├── collection_test.go ├── copy.go ├── decoding_gob.go ├── decoding_gob_test.go ├── decoding_json.go ├── decoding_json_test.go ├── encoding_gob.go ├── encoding_gob_test.go ├── encoding_json.go ├── encoding_json_test.go ├── extractors.go ├── extractors_test.go ├── flatten.go ├── flatten_test.go ├── go.mod ├── helpers.go ├── helpers_test.go ├── intransitive_activity.go ├── intransitive_activity_test.go ├── iri.go ├── iri_test.go ├── item.go ├── item_collection.go ├── item_collection_test.go ├── item_test.go ├── link.go ├── link_test.go ├── natural_language_values.go ├── natural_language_values_test.go ├── object.go ├── object_id.go ├── object_id_test.go ├── object_test.go ├── ordered_collection.go ├── ordered_collection_page.go ├── ordered_collection_page_test.go ├── ordered_collection_test.go ├── place.go ├── place_test.go ├── profile.go ├── profile_test.go ├── question.go ├── question_test.go ├── relationship.go ├── relationship_test.go ├── tests ├── integration_test.go ├── mocks │ ├── activity_create_multiple_objects.json │ ├── activity_create_simple.json │ ├── activity_simple.json │ ├── article_with_multiple_inreplyto.json │ ├── empty.json │ ├── like_activity_with_iri_actor.json │ ├── link_simple.json │ ├── natural_language_values.json │ ├── object_no_type.json │ ├── object_simple.json │ ├── object_with_audience.json │ ├── object_with_replies.json │ ├── object_with_tags.json │ ├── object_with_url.json │ ├── object_with_url_collection.json │ ├── ordered_collection.json │ ├── ordered_collection_page.json │ ├── person_with_outbox.json │ └── travel_simple.json ├── server_common_test.go ├── server_to_server_test.go └── unmarshal_test.go ├── tombstone.go ├── tombstone_test.go ├── typer.go ├── typer_test.go ├── types.go ├── validation.go └── validation_test.go /.build.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - go 4 | sources: 5 | - https://github.com/go-ap/activitypub 6 | environment: 7 | GO111MODULE: 'on' 8 | tasks: 9 | - tests: | 10 | cd activitypub 11 | make test 12 | make TEST_TARGET=./tests test 13 | - coverage: | 14 | set -a +x 15 | cd activitypub && make coverage 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gogland 2 | .idea/ 3 | 4 | # Binaries for programs and plugins 5 | *.so 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tools 11 | *.out 12 | *.coverprofile 13 | 14 | *pkg 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Golang ActitvityPub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .ONESHELL: 3 | .SHELLFLAGS := -eu -o pipefail -c 4 | .DELETE_ON_ERROR: 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rules 7 | 8 | GO ?= go 9 | TEST := $(GO) test 10 | TEST_FLAGS ?= -v 11 | TEST_TARGET ?= . 12 | GO111MODULE = on 13 | PROJECT_NAME := $(shell basename $(PWD)) 14 | 15 | .PHONY: test coverage clean download 16 | 17 | download: 18 | $(GO) mod download all 19 | $(GO) mod tidy 20 | 21 | test: download 22 | $(TEST) $(TEST_FLAGS) $(TEST_TARGET) 23 | 24 | coverage: TEST_TARGET := . 25 | coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile 26 | coverage: test 27 | 28 | clean: 29 | $(RM) -v *.coverprofile 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About GoActivityPub: Vocabulary 2 | 3 | [](https://raw.githubusercontent.com/go-ap/activitypub/master/LICENSE) 4 | [](https://builds.sr.ht/~mariusor/activitypub) 5 | [](https://codecov.io/gh/go-ap/activitypub) 6 | [](https://goreportcard.com/report/github.com/go-ap/activitypub) 7 | 8 | This project is part of the [GoActivityPub](https://github.com/go-ap) library which helps with creating ActivityPub applications using the Go programming language. 9 | 10 | It contains data types for most of the [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) and the [ActivityPub](https://www.w3.org/TR/activitypub/) extension. 11 | They are documented accordingly with annotations from these specifications. 12 | 13 | You can find an expanded documentation about the whole library [on SourceHut](https://man.sr.ht/~mariusor/go-activitypub/go-ap/index.md). 14 | 15 | For discussions about the projects you can write to the discussions mailing list: [~mariusor/go-activitypub-discuss@lists.sr.ht](mailto:~mariusor/go-activitypub-discuss@lists.sr.ht) 16 | 17 | For patches and bug reports please use the dev mailing list: [~mariusor/go-activitypub-dev@lists.sr.ht](mailto:~mariusor/go-activitypub-dev@lists.sr.ht) 18 | 19 | ## Usage 20 | 21 | ```go 22 | import vocab "github.com/go-ap/activitypub" 23 | 24 | follow := vocab.Activity{ 25 | Type: vocab.FollowType, 26 | Actor: vocab.IRI("https://example.com/alice"), 27 | Object: vocab.IRI("https://example.com/janedoe"), 28 | } 29 | 30 | ``` 31 | 32 | ## Note about generics 33 | 34 | The module contains helper functions which make it simpler to deal with the `vocab.Item` 35 | interfaces and they come in two flavours: explicit `OnXXX` and `ToXXX` functions corresponding 36 | to each type and, a generic pair of functions `On[T]` and `To[T]`. 37 | 38 | ```go 39 | import ( 40 | "fmt" 41 | 42 | vocab "github.com/go-ap/activitypub" 43 | ) 44 | 45 | var it vocab.Item = ... // an ActivityPub object unmarshaled from a request 46 | 47 | err := vocab.OnActivity(it, func(act *vocab.Activity) error { 48 | if vocab.ContentManagementActivityTypes.Contains(act.Type) { 49 | fmt.Printf("This is a Content Management type activity: %q", act.Type) 50 | } 51 | return nil 52 | }) 53 | 54 | err := vocab.On[vocab.Activity](it, func(act *vocab.Activity) error { 55 | if vocab.ReactionsActivityTypes.Contains(act.Type) { 56 | fmt.Printf("This is a Reaction type activity: %q", act.Type) 57 | } 58 | return nil 59 | }) 60 | 61 | ``` 62 | 63 | Before using the generic versions you should consider that they come with a pretty heavy performance penalty: 64 | 65 | ``` 66 | goos: linux 67 | goarch: amd64 68 | pkg: github.com/go-ap/activitypub 69 | cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz 70 | Benchmark_OnT_vs_On_T/OnObject-8 752387791 1.633 ns/op 71 | Benchmark_OnT_vs_On_T/On_T_Object-8 4656264 261.8 ns/op 72 | Benchmark_OnT_vs_On_T/OnActor-8 739833261 1.596 ns/op 73 | Benchmark_OnT_vs_On_T/On_T_Actor-8 4035148 301.9 ns/op 74 | Benchmark_OnT_vs_On_T/OnActivity-8 751173854 1.604 ns/op 75 | Benchmark_OnT_vs_On_T/On_T_Activity-8 4062598 285.9 ns/op 76 | Benchmark_OnT_vs_On_T/OnIntransitiveActivity-8 675824500 1.640 ns/op 77 | Benchmark_OnT_vs_On_T/On_T_IntransitiveActivity-8 4372798 274.1 ns/op 78 | PASS 79 | ok github.com/go-ap/activitypub 11.350s 80 | ``` 81 | -------------------------------------------------------------------------------- /collection_page_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestCollectionPageNew(t *testing.T) { 9 | testValue := ID("test") 10 | 11 | c := CollectionNew(testValue) 12 | p := CollectionPageNew(c) 13 | if reflect.DeepEqual(p.Collection, c) { 14 | t.Errorf("Invalid collection parent '%v'", p.PartOf) 15 | } 16 | if p.PartOf != c.GetLink() { 17 | t.Errorf("Invalid collection '%v'", p.PartOf) 18 | } 19 | } 20 | 21 | func TestCollectionPage_Append(t *testing.T) { 22 | id := ID("test") 23 | 24 | val := Object{ID: ID("grrr")} 25 | 26 | c := CollectionNew(id) 27 | 28 | p := CollectionPageNew(c) 29 | p.Append(val) 30 | 31 | if p.PartOf != c.GetLink() { 32 | t.Errorf("Collection page should point to collection %q", c.GetLink()) 33 | } 34 | if p.Count() != 1 { 35 | t.Errorf("Collection page of %q should have exactly one element", p.GetID()) 36 | } 37 | if !reflect.DeepEqual(p.Items[0], val) { 38 | t.Errorf("First item in Inbox is does not match %q", val.ID) 39 | } 40 | } 41 | 42 | func TestCollectionPage_UnmarshalJSON(t *testing.T) { 43 | p := CollectionPage{} 44 | 45 | dataEmpty := []byte("{}") 46 | p.UnmarshalJSON(dataEmpty) 47 | if p.ID != "" { 48 | t.Errorf("Unmarshaled object should have empty ID, received %q", p.ID) 49 | } 50 | if p.Type != "" { 51 | t.Errorf("Unmarshaled object should have empty Type, received %q", p.Type) 52 | } 53 | if p.AttributedTo != nil { 54 | t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", p.AttributedTo) 55 | } 56 | if len(p.Name) != 0 { 57 | t.Errorf("Unmarshaled object should have empty Name, received %q", p.Name) 58 | } 59 | if len(p.Summary) != 0 { 60 | t.Errorf("Unmarshaled object should have empty Summary, received %q", p.Summary) 61 | } 62 | if len(p.Content) != 0 { 63 | t.Errorf("Unmarshaled object should have empty Content, received %q", p.Content) 64 | } 65 | if p.TotalItems != 0 { 66 | t.Errorf("Unmarshaled object should have empty TotalItems, received %d", p.TotalItems) 67 | } 68 | if len(p.Items) > 0 { 69 | t.Errorf("Unmarshaled object should have empty Items, received %v", p.Items) 70 | } 71 | if p.URL != nil { 72 | t.Errorf("Unmarshaled object should have empty URL, received %v", p.URL) 73 | } 74 | if !p.Published.IsZero() { 75 | t.Errorf("Unmarshaled object should have empty Published, received %q", p.Published) 76 | } 77 | if !p.StartTime.IsZero() { 78 | t.Errorf("Unmarshaled object should have empty StartTime, received %q", p.StartTime) 79 | } 80 | if !p.Updated.IsZero() { 81 | t.Errorf("Unmarshaled object should have empty Updated, received %q", p.Updated) 82 | } 83 | if p.PartOf != nil { 84 | t.Errorf("Unmarshaled object should have empty PartOf, received %q", p.PartOf) 85 | } 86 | if p.Current != nil { 87 | t.Errorf("Unmarshaled object should have empty Current, received %q", p.Current) 88 | } 89 | if p.First != nil { 90 | t.Errorf("Unmarshaled object should have empty First, received %q", p.First) 91 | } 92 | if p.Last != nil { 93 | t.Errorf("Unmarshaled object should have empty Last, received %q", p.Last) 94 | } 95 | if p.Next != nil { 96 | t.Errorf("Unmarshaled object should have empty Next, received %q", p.Next) 97 | } 98 | if p.Prev != nil { 99 | t.Errorf("Unmarshaled object should have empty Prev, received %q", p.Prev) 100 | } 101 | } 102 | 103 | func TestCollectionPage_Collection(t *testing.T) { 104 | id := ID("test") 105 | 106 | c := CollectionNew(id) 107 | p := CollectionPageNew(c) 108 | 109 | if !reflect.DeepEqual(p.Collection(), p.Items) { 110 | t.Errorf("Collection items should be equal %v %v", p.Collection(), p.Items) 111 | } 112 | } 113 | 114 | func TestCollectionPage_Count(t *testing.T) { 115 | id := ID("test") 116 | 117 | c := CollectionNew(id) 118 | p := CollectionPageNew(c) 119 | 120 | if p.TotalItems != 0 { 121 | t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems) 122 | } 123 | if len(p.Items) > 0 { 124 | t.Errorf("Empty object should have empty Items, received %v", p.Items) 125 | } 126 | if p.Count() != uint(len(p.Items)) { 127 | t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.Items)) 128 | } 129 | 130 | p.Append(IRI("test")) 131 | if p.TotalItems != 0 { 132 | t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems) 133 | } 134 | if p.Count() != uint(len(p.Items)) { 135 | t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.Items)) 136 | } 137 | } 138 | 139 | func TestToCollectionPage(t *testing.T) { 140 | err := func(it Item) error { return ErrorInvalidType[CollectionPage](it) } 141 | tests := map[string]struct { 142 | it Item 143 | want *CollectionPage 144 | wantErr error 145 | }{ 146 | "CollectionPage": { 147 | it: new(CollectionPage), 148 | want: new(CollectionPage), 149 | wantErr: nil, 150 | }, 151 | "OrderedCollectionPage": { 152 | it: new(OrderedCollectionPage), 153 | want: new(CollectionPage), 154 | wantErr: nil, 155 | }, 156 | "OrderedCollection": { 157 | it: new(OrderedCollection), 158 | want: new(CollectionPage), 159 | wantErr: err(new(OrderedCollection)), 160 | }, 161 | "Collection": { 162 | it: new(Collection), 163 | want: new(CollectionPage), 164 | wantErr: err(new(Collection)), 165 | }, 166 | } 167 | for name, tt := range tests { 168 | t.Run(name, func(t *testing.T) { 169 | got, err := ToCollectionPage(tt.it) 170 | if tt.wantErr != nil && err == nil { 171 | t.Errorf("ToCollectionPage() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr) 172 | return 173 | } 174 | if err != nil { 175 | if tt.wantErr == nil { 176 | t.Errorf("ToCollectionPage() returned unexpected error[%T]%s", err, err) 177 | return 178 | } 179 | if !reflect.DeepEqual(err, tt.wantErr) { 180 | t.Errorf("ToCollectionPage() received error %v, wanted error %v", err, tt.wantErr) 181 | return 182 | } 183 | return 184 | } 185 | if !reflect.DeepEqual(got, tt.want) { 186 | t.Errorf("ToCollectionPage() got %v, want %v", got, tt.want) 187 | } 188 | }) 189 | } 190 | } 191 | 192 | func TestCollectionPage_Contains(t *testing.T) { 193 | t.Skipf("TODO") 194 | } 195 | 196 | func TestCollectionPage_GetID(t *testing.T) { 197 | t.Skipf("TODO") 198 | } 199 | 200 | func TestCollectionPage_GetLink(t *testing.T) { 201 | t.Skipf("TODO") 202 | } 203 | 204 | func TestCollectionPage_GetType(t *testing.T) { 205 | t.Skipf("TODO") 206 | } 207 | 208 | func TestCollectionPage_IsCollection(t *testing.T) { 209 | t.Skipf("TODO") 210 | } 211 | 212 | func TestCollectionPage_IsLink(t *testing.T) { 213 | t.Skipf("TODO") 214 | } 215 | 216 | func TestCollectionPage_IsObject(t *testing.T) { 217 | t.Skipf("TODO") 218 | } 219 | 220 | func TestCollectionPage_MarshalJSON(t *testing.T) { 221 | t.Skipf("TODO") 222 | } 223 | 224 | func TestCollectionPage_ItemMatches(t *testing.T) { 225 | t.Skipf("TODO") 226 | } 227 | -------------------------------------------------------------------------------- /collection_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestCollectionNew(t *testing.T) { 9 | testValue := ID("test") 10 | 11 | c := CollectionNew(testValue) 12 | 13 | if c.ID != testValue { 14 | t.Errorf("APObject Id '%v' different than expected '%v'", c.ID, testValue) 15 | } 16 | if c.Type != CollectionType { 17 | t.Errorf("APObject Type '%v' different than expected '%v'", c.Type, CollectionType) 18 | } 19 | } 20 | 21 | func TestCollection_Append(t *testing.T) { 22 | id := ID("test") 23 | 24 | val := Object{ID: ID("grrr")} 25 | 26 | c := CollectionNew(id) 27 | c.Append(val) 28 | 29 | if c.Count() != 1 { 30 | t.Errorf("Inbox collectionPath of %q should have one element", c.GetID()) 31 | } 32 | if !reflect.DeepEqual(c.Items[0], val) { 33 | t.Errorf("First item in Inbox is does not match %q", val.ID) 34 | } 35 | } 36 | 37 | func TestCollection_Collection(t *testing.T) { 38 | id := ID("test") 39 | 40 | c := CollectionNew(id) 41 | 42 | if !reflect.DeepEqual(c.Collection(), c.Items) { 43 | t.Errorf("Collection items should be equal %v %v", c.Collection(), c.Items) 44 | } 45 | } 46 | 47 | func TestCollection_GetID(t *testing.T) { 48 | id := ID("test") 49 | 50 | c := CollectionNew(id) 51 | 52 | if c.GetID() != id { 53 | t.Errorf("GetID should return %s, received %s", id, c.GetID()) 54 | } 55 | } 56 | 57 | func TestCollection_GetLink(t *testing.T) { 58 | id := ID("test") 59 | link := IRI(id) 60 | 61 | c := CollectionNew(id) 62 | 63 | if c.GetLink() != link { 64 | t.Errorf("GetLink should return %q, received %q", link, c.GetLink()) 65 | } 66 | } 67 | 68 | func TestCollection_GetType(t *testing.T) { 69 | id := ID("test") 70 | 71 | c := CollectionNew(id) 72 | 73 | if c.GetType() != CollectionType { 74 | t.Errorf("Collection Type should be %q, received %q", CollectionType, c.GetType()) 75 | } 76 | } 77 | 78 | func TestCollection_IsLink(t *testing.T) { 79 | id := ID("test") 80 | 81 | c := CollectionNew(id) 82 | 83 | if c.IsLink() != false { 84 | t.Errorf("Collection should not be a link, received %t", c.IsLink()) 85 | } 86 | } 87 | 88 | func TestCollection_IsObject(t *testing.T) { 89 | id := ID("test") 90 | 91 | c := CollectionNew(id) 92 | 93 | if c.IsObject() != true { 94 | t.Errorf("Collection should be an object, received %t", c.IsObject()) 95 | } 96 | } 97 | 98 | func TestCollection_UnmarshalJSON(t *testing.T) { 99 | c := Collection{} 100 | 101 | dataEmpty := []byte("{}") 102 | c.UnmarshalJSON(dataEmpty) 103 | if c.ID != "" { 104 | t.Errorf("Unmarshaled object should have empty ID, received %q", c.ID) 105 | } 106 | if c.Type != "" { 107 | t.Errorf("Unmarshaled object should have empty Type, received %q", c.Type) 108 | } 109 | if c.AttributedTo != nil { 110 | t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", c.AttributedTo) 111 | } 112 | if len(c.Name) != 0 { 113 | t.Errorf("Unmarshaled object should have empty Name, received %q", c.Name) 114 | } 115 | if len(c.Summary) != 0 { 116 | t.Errorf("Unmarshaled object should have empty Summary, received %q", c.Summary) 117 | } 118 | if len(c.Content) != 0 { 119 | t.Errorf("Unmarshaled object should have empty Content, received %q", c.Content) 120 | } 121 | if c.TotalItems != 0 { 122 | t.Errorf("Unmarshaled object should have empty TotalItems, received %d", c.TotalItems) 123 | } 124 | if len(c.Items) > 0 { 125 | t.Errorf("Unmarshaled object should have empty Items, received %v", c.Items) 126 | } 127 | if c.URL != nil { 128 | t.Errorf("Unmarshaled object should have empty URL, received %v", c.URL) 129 | } 130 | if !c.Published.IsZero() { 131 | t.Errorf("Unmarshaled object should have empty Published, received %q", c.Published) 132 | } 133 | if !c.StartTime.IsZero() { 134 | t.Errorf("Unmarshaled object should have empty StartTime, received %q", c.StartTime) 135 | } 136 | if !c.Updated.IsZero() { 137 | t.Errorf("Unmarshaled object should have empty Updated, received %q", c.Updated) 138 | } 139 | } 140 | 141 | func TestCollection_Count(t *testing.T) { 142 | id := ID("test") 143 | 144 | c := CollectionNew(id) 145 | 146 | if c.TotalItems != 0 { 147 | t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems) 148 | } 149 | if len(c.Items) > 0 { 150 | t.Errorf("Empty object should have empty Items, received %v", c.Items) 151 | } 152 | if c.Count() != uint(len(c.Items)) { 153 | t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.Items)) 154 | } 155 | 156 | c.Append(IRI("test")) 157 | if c.TotalItems != 0 { 158 | t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems) 159 | } 160 | if c.Count() != uint(len(c.Items)) { 161 | t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.Items)) 162 | } 163 | } 164 | 165 | func TestCollection_Contains(t *testing.T) { 166 | t.Skipf("TODO") 167 | } 168 | 169 | func TestCollection_IsCollection(t *testing.T) { 170 | t.Skipf("TODO") 171 | } 172 | 173 | func TestFollowersNew(t *testing.T) { 174 | t.Skipf("TODO") 175 | } 176 | 177 | func TestFollowingNew(t *testing.T) { 178 | t.Skipf("TODO") 179 | } 180 | 181 | func TestCollection_MarshalJSON(t *testing.T) { 182 | t.Skipf("TODO") 183 | } 184 | 185 | func TestCollection_ItemMatches(t *testing.T) { 186 | t.Skipf("TODO") 187 | } 188 | 189 | func TestToCollection(t *testing.T) { 190 | //err := func(it Item) error { return ErrorInvalidType[Collection](it) } 191 | tests := map[string]struct { 192 | it Item 193 | want *Collection 194 | wantErr error 195 | }{ 196 | "Collection": { 197 | it: new(Collection), 198 | want: new(Collection), 199 | wantErr: nil, 200 | }, 201 | "CollectionPage": { 202 | it: new(CollectionPage), 203 | want: new(Collection), 204 | wantErr: nil, 205 | }, 206 | "OrderedCollectionPage": { 207 | it: new(OrderedCollectionPage), 208 | want: new(Collection), 209 | wantErr: nil, 210 | }, 211 | "OrderedCollection": { 212 | it: new(OrderedCollection), 213 | want: new(Collection), 214 | wantErr: nil, 215 | }, 216 | } 217 | for name, tt := range tests { 218 | t.Run(name, func(t *testing.T) { 219 | got, err := ToCollection(tt.it) 220 | if tt.wantErr != nil && err == nil { 221 | t.Errorf("ToCollection() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr) 222 | return 223 | } 224 | if err != nil { 225 | if tt.wantErr == nil { 226 | t.Errorf("ToCollection() returned unexpected error[%T]%s", err, err) 227 | return 228 | } 229 | if !reflect.DeepEqual(err, tt.wantErr) { 230 | t.Errorf("ToCollection() received error %v, wanted error %v", err, tt.wantErr) 231 | return 232 | } 233 | return 234 | } 235 | if !reflect.DeepEqual(got, tt.want) { 236 | t.Errorf("ToCollection() got = %v, want %v", got, tt.want) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | func TestCollection_Equals(t *testing.T) { 243 | tests := []struct { 244 | name string 245 | fields Collection 246 | item Item 247 | want bool 248 | }{ 249 | { 250 | name: "collection with two items", 251 | fields: Collection{ 252 | ID: "https://example.com/1", 253 | Type: CollectionType, 254 | First: IRI("https://example.com/1?first"), 255 | Items: ItemCollection{ 256 | Object{ID: "https://example.com/1/1", Type: NoteType}, 257 | Object{ID: "https://example.com/1/3", Type: ImageType}, 258 | }, 259 | }, 260 | item: &Collection{ 261 | ID: "https://example.com/1", 262 | Type: CollectionType, 263 | First: IRI("https://example.com/1?first"), 264 | Items: ItemCollection{ 265 | Object{ID: "https://example.com/1/1", Type: NoteType}, 266 | Object{ID: "https://example.com/1/3", Type: ImageType}, 267 | }, 268 | }, 269 | want: true, 270 | }, 271 | } 272 | for _, tt := range tests { 273 | t.Run(tt.name, func(t *testing.T) { 274 | if got := tt.fields.Equals(tt.item); got != tt.want { 275 | t.Errorf("Equals() = %v, want %v", got, tt.want) 276 | } 277 | }) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /copy.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func CopyOrderedCollectionPageProperties(to, from *OrderedCollectionPage) (*OrderedCollectionPage, error) { 8 | to.PartOf = replaceIfItem(to.PartOf, from.PartOf) 9 | to.Next = replaceIfItem(to.Next, from.Next) 10 | to.Prev = replaceIfItem(to.Prev, from.Prev) 11 | oldCol, _ := ToOrderedCollection(to) 12 | newCol, _ := ToOrderedCollection(from) 13 | _, err := CopyOrderedCollectionProperties(oldCol, newCol) 14 | if err != nil { 15 | return to, err 16 | } 17 | return to, nil 18 | } 19 | 20 | func CopyCollectionPageProperties(to, from *CollectionPage) (*CollectionPage, error) { 21 | to.PartOf = replaceIfItem(to.PartOf, from.PartOf) 22 | to.Next = replaceIfItem(to.Next, from.Next) 23 | to.Prev = replaceIfItem(to.Prev, from.Prev) 24 | toCol, _ := ToCollection(to) 25 | fromCol, _ := ToCollection(from) 26 | _, err := CopyCollectionProperties(toCol, fromCol) 27 | return to, err 28 | } 29 | 30 | func CopyOrderedCollectionProperties(to, from *OrderedCollection) (*OrderedCollection, error) { 31 | to.First = replaceIfItem(to.First, from.First) 32 | to.Last = replaceIfItem(to.Last, from.Last) 33 | to.OrderedItems = replaceIfItemCollection(to.OrderedItems, from.OrderedItems) 34 | if to.TotalItems == 0 { 35 | to.TotalItems = from.TotalItems 36 | } 37 | oldOb, _ := ToObject(to) 38 | newOb, _ := ToObject(from) 39 | _, err := CopyObjectProperties(oldOb, newOb) 40 | return to, err 41 | } 42 | 43 | func CopyCollectionProperties(to, from *Collection) (*Collection, error) { 44 | to.First = replaceIfItem(to.First, from.First) 45 | to.Last = replaceIfItem(to.Last, from.Last) 46 | to.Items = replaceIfItemCollection(to.Items, from.Items) 47 | if to.TotalItems == 0 { 48 | to.TotalItems = from.TotalItems 49 | } 50 | oldOb, _ := ToObject(to) 51 | newOb, _ := ToObject(from) 52 | _, err := CopyObjectProperties(oldOb, newOb) 53 | return to, err 54 | } 55 | 56 | // CopyObjectProperties updates the "old" object properties with the "new's" 57 | // Including ID and Type 58 | func CopyObjectProperties(to, from *Object) (*Object, error) { 59 | to.ID = from.ID 60 | to.Type = from.Type 61 | to.Name = replaceIfNaturalLanguageValues(to.Name, from.Name) 62 | to.Attachment = replaceIfItem(to.Attachment, from.Attachment) 63 | to.AttributedTo = replaceIfItem(to.AttributedTo, from.AttributedTo) 64 | to.Audience = replaceIfItemCollection(to.Audience, from.Audience) 65 | to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content) 66 | to.Context = replaceIfItem(to.Context, from.Context) 67 | if len(from.MediaType) > 0 { 68 | to.MediaType = from.MediaType 69 | } 70 | if !from.EndTime.IsZero() { 71 | to.EndTime = from.EndTime 72 | } 73 | to.Generator = replaceIfItem(to.Generator, from.Generator) 74 | to.Icon = replaceIfItem(to.Icon, from.Icon) 75 | to.Image = replaceIfItem(to.Image, from.Image) 76 | to.InReplyTo = replaceIfItem(to.InReplyTo, from.InReplyTo) 77 | to.Location = replaceIfItem(to.Location, from.Location) 78 | to.Preview = replaceIfItem(to.Preview, from.Preview) 79 | if to.Published.IsZero() && !from.Published.IsZero() { 80 | to.Published = from.Published 81 | } 82 | if to.Updated.IsZero() && !from.Updated.IsZero() { 83 | to.Updated = from.Updated 84 | } 85 | to.Replies = replaceIfItem(to.Replies, from.Replies) 86 | if !from.StartTime.IsZero() { 87 | to.StartTime = from.StartTime 88 | } 89 | to.Summary = replaceIfNaturalLanguageValues(to.Summary, from.Summary) 90 | to.Tag = replaceIfItemCollection(to.Tag, from.Tag) 91 | if from.URL != nil { 92 | to.URL = from.URL 93 | } 94 | to.To = replaceIfItemCollection(to.To, from.To) 95 | to.Bto = replaceIfItemCollection(to.Bto, from.Bto) 96 | to.CC = replaceIfItemCollection(to.CC, from.CC) 97 | to.BCC = replaceIfItemCollection(to.BCC, from.BCC) 98 | if from.Duration == 0 { 99 | to.Duration = from.Duration 100 | } 101 | to.Source = replaceIfSource(to.Source, from.Source) 102 | return to, nil 103 | } 104 | 105 | func copyAllItemProperties(to, from Item) (Item, error) { 106 | if CollectionType == to.GetType() { 107 | o, err := ToCollection(to) 108 | if err != nil { 109 | return o, err 110 | } 111 | n, err := ToCollection(from) 112 | if err != nil { 113 | return o, err 114 | } 115 | return CopyCollectionProperties(o, n) 116 | } 117 | if CollectionPageType == to.GetType() { 118 | o, err := ToCollectionPage(to) 119 | if err != nil { 120 | return o, err 121 | } 122 | n, err := ToCollectionPage(from) 123 | if err != nil { 124 | return o, err 125 | } 126 | return CopyCollectionPageProperties(o, n) 127 | } 128 | if OrderedCollectionType == to.GetType() { 129 | o, err := ToOrderedCollection(to) 130 | if err != nil { 131 | return o, err 132 | } 133 | n, err := ToOrderedCollection(from) 134 | if err != nil { 135 | return o, err 136 | } 137 | return CopyOrderedCollectionProperties(o, n) 138 | } 139 | if OrderedCollectionPageType == to.GetType() { 140 | o, err := ToOrderedCollectionPage(to) 141 | if err != nil { 142 | return o, err 143 | } 144 | n, err := ToOrderedCollectionPage(from) 145 | if err != nil { 146 | return o, err 147 | } 148 | return CopyOrderedCollectionPageProperties(o, n) 149 | } 150 | if ActorTypes.Contains(to.GetType()) { 151 | o, err := ToActor(to) 152 | if err != nil { 153 | return o, err 154 | } 155 | n, err := ToActor(from) 156 | if err != nil { 157 | return o, err 158 | } 159 | return UpdatePersonProperties(o, n) 160 | } 161 | if ObjectTypes.Contains(to.GetType()) || to.GetType() == "" { 162 | o, err := ToObject(to) 163 | if err != nil { 164 | return o, err 165 | } 166 | n, err := ToObject(from) 167 | if err != nil { 168 | return o, err 169 | } 170 | return CopyObjectProperties(o, n) 171 | } 172 | return to, fmt.Errorf("could not process objects with type %s", to.GetType()) 173 | } 174 | 175 | // CopyItemProperties delegates to the correct per type functions for copying 176 | // properties between matching Activity Objects 177 | func CopyItemProperties(to, from Item) (Item, error) { 178 | if to == nil { 179 | return to, fmt.Errorf("nil object to update") 180 | } 181 | if from == nil { 182 | return to, fmt.Errorf("nil object for update") 183 | } 184 | if !to.GetLink().Equals(from.GetLink(), false) { 185 | return to, fmt.Errorf("object IDs don't match") 186 | } 187 | if to.GetType() != "" && to.GetType() != from.GetType() { 188 | return to, fmt.Errorf("invalid object types for update %s(old) and %s(new)", from.GetType(), to.GetType()) 189 | } 190 | return copyAllItemProperties(to, from) 191 | } 192 | 193 | // UpdatePersonProperties 194 | func UpdatePersonProperties(to, from *Actor) (*Actor, error) { 195 | to.Inbox = replaceIfItem(to.Inbox, from.Inbox) 196 | to.Outbox = replaceIfItem(to.Outbox, from.Outbox) 197 | to.Following = replaceIfItem(to.Following, from.Following) 198 | to.Followers = replaceIfItem(to.Followers, from.Followers) 199 | to.Liked = replaceIfItem(to.Liked, from.Liked) 200 | to.PreferredUsername = replaceIfNaturalLanguageValues(to.PreferredUsername, from.PreferredUsername) 201 | oldOb, _ := ToObject(to) 202 | newOb, _ := ToObject(from) 203 | _, err := CopyObjectProperties(oldOb, newOb) 204 | return to, err 205 | } 206 | 207 | func replaceIfItem(old, new Item) Item { 208 | if new == nil { 209 | return old 210 | } 211 | return new 212 | } 213 | 214 | func replaceIfItemCollection(old, new ItemCollection) ItemCollection { 215 | if new == nil { 216 | return old 217 | } 218 | return new 219 | } 220 | 221 | func replaceIfNaturalLanguageValues(old, new NaturalLanguageValues) NaturalLanguageValues { 222 | if new == nil { 223 | return old 224 | } 225 | return new 226 | } 227 | 228 | func replaceIfSource(to, from Source) Source { 229 | if from.MediaType != to.MediaType { 230 | return from 231 | } 232 | to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content) 233 | return to 234 | } 235 | -------------------------------------------------------------------------------- /decoding_gob_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | /* 4 | func TestGobEncode(t *testing.T) { 5 | type args struct { 6 | it Item 7 | } 8 | tests := []struct { 9 | name string 10 | args args 11 | want []byte 12 | wantErr bool 13 | }{ 14 | // TODO: Add test cases. 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | got, err := GobEncode(tt.args.it) 19 | if (err != nil) != tt.wantErr { 20 | t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr) 21 | return 22 | } 23 | if !reflect.DeepEqual(got, tt.want) { 24 | t.Errorf("GobEncode() got = %v, want %v", got, tt.want) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | func TestUnmarshalGob(t *testing.T) { 31 | type args struct { 32 | data []byte 33 | } 34 | tests := []struct { 35 | name string 36 | args args 37 | want Item 38 | wantErr bool 39 | }{ 40 | { 41 | name: "empty", 42 | args: args{}, 43 | want: nil, 44 | wantErr: false, 45 | }, 46 | } 47 | for _, tt := range tests { 48 | t.Run(tt.name, func(t *testing.T) { 49 | got, err := UnmarshalGob(tt.args.data) 50 | if (err != nil) != tt.wantErr { 51 | t.Errorf("UnmarshalGob() error = %v, wantErr %v", err, tt.wantErr) 52 | return 53 | } 54 | if !reflect.DeepEqual(got, tt.want) { 55 | t.Errorf("UnmarshalGob() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | */ 61 | -------------------------------------------------------------------------------- /encoding_gob_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | /* 4 | func TestMarshalGob(t *testing.T) { 5 | tests := []struct { 6 | name string 7 | it Item 8 | want []byte 9 | wantErr error 10 | }{ 11 | { 12 | name: "empty object", 13 | it: &Object{ 14 | ID: "test", 15 | }, 16 | want: []byte{}, 17 | wantErr: nil, 18 | }, 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | buf := bytes.NewBuffer(make([]byte, 0)) 23 | err := gob.NewEncoder(buf).Encode(tt.it) 24 | 25 | if !errors.Is(err, tt.wantErr) { 26 | t.Errorf("MarshalGob() error = %s, wantErr %v", err, tt.wantErr) 27 | return 28 | } 29 | 30 | it := new(Object) 31 | got := buf.Bytes() 32 | if err := gob.NewDecoder(bytes.NewReader(got)).Decode(it); err != nil { 33 | t.Errorf("Gob Decoding failed for previously generated output %v", err) 34 | } 35 | if tt.wantErr == nil { 36 | if !assertDeepEquals(t.Errorf, it, tt.it) { 37 | t.Errorf("Gob Decoded value is different got = %#v, want %#v", it, tt.it) 38 | } 39 | } 40 | }) 41 | } 42 | } 43 | */ 44 | -------------------------------------------------------------------------------- /encoding_json.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "git.sr.ht/~mariusor/go-xsd-duration" 9 | "github.com/go-ap/jsonld" 10 | ) 11 | 12 | func JSONWriteComma(b *[]byte) { 13 | if len(*b) > 1 && (*b)[len(*b)-1] != ',' { 14 | *b = append(*b, ',') 15 | } 16 | } 17 | 18 | func JSONWriteProp(b *[]byte, name string, val []byte) (notEmpty bool) { 19 | if len(val) == 0 { 20 | return false 21 | } 22 | JSONWriteComma(b) 23 | success := JSONWritePropName(b, name) && JSONWriteValue(b, val) 24 | if !success { 25 | *b = (*b)[:len(*b)-1] 26 | } 27 | return success 28 | } 29 | 30 | func JSONWrite(b *[]byte, c ...byte) { 31 | *b = append(*b, c...) 32 | } 33 | 34 | func JSONWriteS(b *[]byte, s string) { 35 | *b = append(*b, s...) 36 | } 37 | 38 | func JSONWritePropName(b *[]byte, s string) (notEmpty bool) { 39 | if len(s) == 0 { 40 | return false 41 | } 42 | JSONWrite(b, '"') 43 | JSONWriteS(b, s) 44 | JSONWrite(b, '"', ':') 45 | return true 46 | } 47 | 48 | func JSONWriteValue(b *[]byte, s []byte) (notEmpty bool) { 49 | if len(s) == 0 { 50 | return false 51 | } 52 | JSONWrite(b, s...) 53 | return true 54 | } 55 | 56 | func JSONWriteNaturalLanguageProp(b *[]byte, n string, nl NaturalLanguageValues) (notEmpty bool) { 57 | l := nl.Count() 58 | if l > 1 { 59 | n += "Map" 60 | } 61 | if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { 62 | return JSONWriteProp(b, n, v) 63 | } 64 | return false 65 | } 66 | 67 | func JSONWriteStringProp(b *[]byte, n string, s string) (notEmpty bool) { 68 | return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%s"`, s))) 69 | } 70 | 71 | func JSONWriteBoolProp(b *[]byte, n string, t bool) (notEmpty bool) { 72 | return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%t"`, t))) 73 | } 74 | 75 | func JSONWriteIntProp(b *[]byte, n string, d int64) (notEmpty bool) { 76 | return JSONWriteProp(b, n, []byte(fmt.Sprintf("%d", d))) 77 | } 78 | 79 | func JSONWriteFloatProp(b *[]byte, n string, f float64) (notEmpty bool) { 80 | return JSONWriteProp(b, n, []byte(fmt.Sprintf("%f", f))) 81 | } 82 | 83 | func JSONWriteTimeProp(b *[]byte, n string, t time.Time) (notEmpty bool) { 84 | var tb []byte 85 | JSONWrite(&tb, '"') 86 | JSONWriteS(&tb, t.UTC().Format(time.RFC3339)) 87 | JSONWrite(&tb, '"') 88 | return JSONWriteProp(b, n, tb) 89 | } 90 | 91 | func JSONWriteDurationProp(b *[]byte, n string, d time.Duration) (notEmpty bool) { 92 | var tb []byte 93 | if v, err := xsd.Marshal(d); err == nil { 94 | JSONWrite(&tb, '"') 95 | JSONWrite(&tb, v...) 96 | JSONWrite(&tb, '"') 97 | } 98 | return JSONWriteProp(b, n, tb) 99 | } 100 | 101 | func JSONWriteIRIProp(b *[]byte, n string, i LinkOrIRI) (notEmpty bool) { 102 | url := i.GetLink().String() 103 | if len(url) == 0 { 104 | return false 105 | } 106 | JSONWriteStringProp(b, n, url) 107 | return true 108 | } 109 | 110 | func JSONWriteItemProp(b *[]byte, n string, i Item) (notEmpty bool) { 111 | if i == nil { 112 | return notEmpty 113 | } 114 | if im, ok := i.(json.Marshaler); ok { 115 | v, err := im.MarshalJSON() 116 | if err != nil { 117 | return false 118 | } 119 | return JSONWriteProp(b, n, v) 120 | } 121 | return notEmpty 122 | } 123 | 124 | func byteInsertAt(raw []byte, b byte, p int) []byte { 125 | return append(raw[:p], append([]byte{b}, raw[p:]...)...) 126 | } 127 | 128 | func escapeQuote(s string) string { 129 | raw := []byte(s) 130 | end := len(s) 131 | for i := 0; i < end; i++ { 132 | c := raw[i] 133 | if c == '"' && (i > 0 && s[i-1] != '\\') { 134 | raw = byteInsertAt(raw, '\\', i) 135 | i++ 136 | end++ 137 | } 138 | } 139 | return string(raw) 140 | } 141 | 142 | func JSONWriteStringValue(b *[]byte, s string) (notEmpty bool) { 143 | if len(s) == 0 { 144 | return false 145 | } 146 | JSONWrite(b, '"') 147 | JSONWriteS(b, escapeQuote(s)) 148 | JSONWrite(b, '"') 149 | return true 150 | } 151 | 152 | func JSONWriteItemCollectionValue(b *[]byte, col ItemCollection, compact bool) (notEmpty bool) { 153 | if len(col) == 0 { 154 | return notEmpty 155 | } 156 | if len(col) == 1 && compact { 157 | it := col[0] 158 | im, ok := it.(json.Marshaler) 159 | if !ok { 160 | return false 161 | } 162 | v, err := im.MarshalJSON() 163 | if err != nil { 164 | return false 165 | } 166 | if len(v) == 0 { 167 | return false 168 | } 169 | JSONWrite(b, v...) 170 | return true 171 | } 172 | writeCommaIfNotEmpty := func(notEmpty bool) { 173 | if notEmpty { 174 | JSONWrite(b, ',') 175 | } 176 | } 177 | JSONWrite(b, '[') 178 | skipComma := true 179 | for _, it := range col { 180 | im, ok := it.(json.Marshaler) 181 | if !ok { 182 | continue 183 | } 184 | v, err := im.MarshalJSON() 185 | if err != nil { 186 | return false 187 | } 188 | if len(v) == 0 { 189 | continue 190 | } 191 | writeCommaIfNotEmpty(!skipComma) 192 | JSONWrite(b, v...) 193 | skipComma = false 194 | } 195 | JSONWrite(b, ']') 196 | return true 197 | } 198 | 199 | func JSONWriteItemCollectionProp(b *[]byte, n string, col ItemCollection, compact bool) (notEmpty bool) { 200 | if len(col) == 0 { 201 | return notEmpty 202 | } 203 | JSONWriteComma(b) 204 | success := JSONWritePropName(b, n) && JSONWriteItemCollectionValue(b, col, compact) 205 | if !success { 206 | *b = (*b)[:len(*b)-1] 207 | } 208 | return success 209 | } 210 | 211 | func JSONWriteObjectValue(b *[]byte, o Object) (notEmpty bool) { 212 | if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { 213 | notEmpty = JSONWriteProp(b, "id", v) 214 | } 215 | if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { 216 | notEmpty = JSONWriteProp(b, "type", v) || notEmpty 217 | } 218 | if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { 219 | notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty 220 | } 221 | if len(o.Name) > 0 { 222 | notEmpty = JSONWriteNaturalLanguageProp(b, "name", o.Name) || notEmpty 223 | } 224 | if len(o.Summary) > 0 { 225 | notEmpty = JSONWriteNaturalLanguageProp(b, "summary", o.Summary) || notEmpty 226 | } 227 | if len(o.Content) > 0 { 228 | notEmpty = JSONWriteNaturalLanguageProp(b, "content", o.Content) || notEmpty 229 | } 230 | if o.Attachment != nil { 231 | notEmpty = JSONWriteItemProp(b, "attachment", o.Attachment) || notEmpty 232 | } 233 | if o.AttributedTo != nil { 234 | notEmpty = JSONWriteItemProp(b, "attributedTo", o.AttributedTo) || notEmpty 235 | } 236 | if o.Audience != nil { 237 | notEmpty = JSONWriteItemProp(b, "audience", o.Audience) || notEmpty 238 | } 239 | if o.Context != nil { 240 | notEmpty = JSONWriteItemProp(b, "context", o.Context) || notEmpty 241 | } 242 | if o.Generator != nil { 243 | notEmpty = JSONWriteItemProp(b, "generator", o.Generator) || notEmpty 244 | } 245 | if o.Icon != nil { 246 | notEmpty = JSONWriteItemProp(b, "icon", o.Icon) || notEmpty 247 | } 248 | if o.Image != nil { 249 | notEmpty = JSONWriteItemProp(b, "image", o.Image) || notEmpty 250 | } 251 | if o.InReplyTo != nil { 252 | notEmpty = JSONWriteItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty 253 | } 254 | if o.Location != nil { 255 | notEmpty = JSONWriteItemProp(b, "location", o.Location) || notEmpty 256 | } 257 | if o.Preview != nil { 258 | notEmpty = JSONWriteItemProp(b, "preview", o.Preview) || notEmpty 259 | } 260 | if o.Replies != nil { 261 | notEmpty = JSONWriteItemProp(b, "replies", o.Replies) || notEmpty 262 | } 263 | if o.Tag != nil { 264 | notEmpty = JSONWriteItemCollectionProp(b, "tag", o.Tag, false) || notEmpty 265 | } 266 | if o.URL != nil { 267 | notEmpty = JSONWriteItemProp(b, "url", o.URL) || notEmpty 268 | } 269 | if o.To != nil { 270 | notEmpty = JSONWriteItemCollectionProp(b, "to", o.To, false) || notEmpty 271 | } 272 | if o.Bto != nil { 273 | notEmpty = JSONWriteItemCollectionProp(b, "bto", o.Bto, false) || notEmpty 274 | } 275 | if o.CC != nil { 276 | notEmpty = JSONWriteItemCollectionProp(b, "cc", o.CC, false) || notEmpty 277 | } 278 | if o.BCC != nil { 279 | notEmpty = JSONWriteItemCollectionProp(b, "bcc", o.BCC, false) || notEmpty 280 | } 281 | if !o.Published.IsZero() { 282 | notEmpty = JSONWriteTimeProp(b, "published", o.Published) || notEmpty 283 | } 284 | if !o.Updated.IsZero() { 285 | notEmpty = JSONWriteTimeProp(b, "updated", o.Updated) || notEmpty 286 | } 287 | if !o.StartTime.IsZero() { 288 | notEmpty = JSONWriteTimeProp(b, "startTime", o.StartTime) || notEmpty 289 | } 290 | if !o.EndTime.IsZero() { 291 | notEmpty = JSONWriteTimeProp(b, "endTime", o.EndTime) || notEmpty 292 | } 293 | if o.Duration != 0 { 294 | // TODO(marius): maybe don't use 0 as a nil value for Object types 295 | // which can have a valid duration of 0 - (Video, Audio, etc) 296 | notEmpty = JSONWriteDurationProp(b, "duration", o.Duration) || notEmpty 297 | } 298 | if o.Likes != nil { 299 | notEmpty = JSONWriteItemProp(b, "likes", o.Likes) || notEmpty 300 | } 301 | if o.Shares != nil { 302 | notEmpty = JSONWriteItemProp(b, "shares", o.Shares) || notEmpty 303 | } 304 | if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { 305 | notEmpty = JSONWriteProp(b, "source", v) || notEmpty 306 | } 307 | return notEmpty 308 | } 309 | 310 | func JSONWriteActivityValue(b *[]byte, a Activity) (notEmpty bool) { 311 | _ = OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { 312 | if i == nil { 313 | return nil 314 | } 315 | notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty 316 | return nil 317 | }) 318 | if a.Object != nil { 319 | notEmpty = JSONWriteItemProp(b, "object", a.Object) || notEmpty 320 | } 321 | return notEmpty 322 | } 323 | 324 | func JSONWriteIntransitiveActivityValue(b *[]byte, i IntransitiveActivity) (notEmpty bool) { 325 | _ = OnObject(i, func(o *Object) error { 326 | if o == nil { 327 | return nil 328 | } 329 | notEmpty = JSONWriteObjectValue(b, *o) || notEmpty 330 | return nil 331 | }) 332 | if i.Actor != nil { 333 | notEmpty = JSONWriteItemProp(b, "actor", i.Actor) || notEmpty 334 | } 335 | if i.Target != nil { 336 | notEmpty = JSONWriteItemProp(b, "target", i.Target) || notEmpty 337 | } 338 | if i.Result != nil { 339 | notEmpty = JSONWriteItemProp(b, "result", i.Result) || notEmpty 340 | } 341 | if i.Origin != nil { 342 | notEmpty = JSONWriteItemProp(b, "origin", i.Origin) || notEmpty 343 | } 344 | if i.Instrument != nil { 345 | notEmpty = JSONWriteItemProp(b, "instrument", i.Instrument) || notEmpty 346 | } 347 | return notEmpty 348 | } 349 | 350 | func JSONWriteQuestionValue(b *[]byte, q Question) (notEmpty bool) { 351 | _ = OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { 352 | if i == nil { 353 | return nil 354 | } 355 | notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty 356 | return nil 357 | }) 358 | if q.OneOf != nil { 359 | notEmpty = JSONWriteItemProp(b, "oneOf", q.OneOf) || notEmpty 360 | } 361 | if q.AnyOf != nil { 362 | notEmpty = JSONWriteItemProp(b, "anyOf", q.AnyOf) || notEmpty 363 | } 364 | notEmpty = JSONWriteBoolProp(b, "closed", q.Closed) || notEmpty 365 | return notEmpty 366 | } 367 | 368 | func JSONWriteLinkValue(b *[]byte, l Link) (notEmpty bool) { 369 | if v, err := l.ID.MarshalJSON(); err == nil && len(v) > 0 { 370 | notEmpty = JSONWriteProp(b, "id", v) 371 | } 372 | if v, err := l.Type.MarshalJSON(); err == nil && len(v) > 0 { 373 | notEmpty = JSONWriteProp(b, "type", v) || notEmpty 374 | } 375 | if v, err := l.MediaType.MarshalJSON(); err == nil && len(v) > 0 { 376 | notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty 377 | } 378 | if len(l.Name) > 0 { 379 | notEmpty = JSONWriteNaturalLanguageProp(b, "name", l.Name) || notEmpty 380 | } 381 | if v, err := l.Rel.MarshalJSON(); err == nil && len(v) > 0 { 382 | notEmpty = JSONWriteProp(b, "rel", v) || notEmpty 383 | } 384 | if l.Height > 0 { 385 | notEmpty = JSONWriteIntProp(b, "height", int64(l.Height)) 386 | } 387 | if l.Width > 0 { 388 | notEmpty = JSONWriteIntProp(b, "width", int64(l.Width)) 389 | } 390 | if l.Preview != nil { 391 | notEmpty = JSONWriteItemProp(b, "rel", l.Preview) || notEmpty 392 | } 393 | if v, err := l.Href.MarshalJSON(); err == nil && len(v) > 0 { 394 | notEmpty = JSONWriteProp(b, "href", v) || notEmpty 395 | } 396 | if len(l.HrefLang) > 0 { 397 | notEmpty = JSONWriteStringProp(b, "hrefLang", string(l.HrefLang)) || notEmpty 398 | } 399 | return notEmpty 400 | } 401 | 402 | // MarshalJSON represents just a wrapper for the jsonld.Marshal function 403 | func MarshalJSON(it LinkOrIRI) ([]byte, error) { 404 | return jsonld.Marshal(it) 405 | } 406 | -------------------------------------------------------------------------------- /extractors.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | func PreferredNameOf(it Item) string { 4 | var cont string 5 | if IsObject(it) { 6 | _ = OnActor(it, func(act *Actor) error { 7 | if act.PreferredUsername != nil { 8 | cont = act.PreferredUsername.First().String() 9 | } 10 | return nil 11 | }) 12 | } 13 | return cont 14 | } 15 | 16 | func ContentOf(it Item) string { 17 | var cont string 18 | if IsObject(it) { 19 | _ = OnObject(it, func(ob *Object) error { 20 | if ob.Content != nil { 21 | cont = ob.Content.First().String() 22 | } 23 | return nil 24 | }) 25 | } 26 | return cont 27 | } 28 | 29 | func SummaryOf(it Item) string { 30 | var cont string 31 | if IsObject(it) { 32 | _ = OnObject(it, func(ob *Object) error { 33 | if ob.Summary != nil { 34 | cont = ob.Summary.First().String() 35 | } 36 | return nil 37 | }) 38 | } 39 | return cont 40 | } 41 | 42 | func NameOf(it Item) string { 43 | var name string 44 | if IsLink(it) { 45 | _ = OnLink(it, func(lnk *Link) error { 46 | if lnk.Name != nil { 47 | name = lnk.Name.First().String() 48 | } 49 | return nil 50 | }) 51 | } else { 52 | _ = OnObject(it, func(ob *Object) error { 53 | if ob.Name != nil { 54 | name = ob.Name.First().String() 55 | } 56 | return nil 57 | }) 58 | } 59 | return name 60 | } 61 | -------------------------------------------------------------------------------- /extractors_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import "testing" 4 | 5 | func TestContentOf(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | arg Item 9 | want string 10 | }{ 11 | { 12 | name: "empty", 13 | arg: nil, 14 | want: "", 15 | }, 16 | } 17 | for _, tt := range tests { 18 | t.Run(tt.name, func(t *testing.T) { 19 | if got := ContentOf(tt.arg); got != tt.want { 20 | t.Errorf("ContentOf() = %v, want %v", got, tt.want) 21 | } 22 | }) 23 | } 24 | } 25 | 26 | func TestNameOf(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | arg Item 30 | want string 31 | }{ 32 | { 33 | name: "empty", 34 | arg: nil, 35 | want: "", 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if got := NameOf(tt.arg); got != tt.want { 41 | t.Errorf("NameOf() = %v, want %v", got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestPreferredNameOf(t *testing.T) { 48 | tests := []struct { 49 | name string 50 | arg Item 51 | want string 52 | }{ 53 | { 54 | name: "empty", 55 | arg: nil, 56 | want: "", 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | if got := PreferredNameOf(tt.arg); got != tt.want { 62 | t.Errorf("PreferredNameOf() = %v, want %v", got, tt.want) 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestSummaryOf(t *testing.T) { 69 | tests := []struct { 70 | name string 71 | arg Item 72 | want string 73 | }{ 74 | { 75 | name: "empty", 76 | arg: nil, 77 | want: "", 78 | }, 79 | } 80 | for _, tt := range tests { 81 | t.Run(tt.name, func(t *testing.T) { 82 | if got := SummaryOf(tt.arg); got != tt.want { 83 | t.Errorf("SummaryOf() = %v, want %v", got, tt.want) 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /flatten.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | // FlattenActivityProperties flattens the Activity's properties from Object type to IRI 4 | func FlattenActivityProperties(act *Activity) *Activity { 5 | if act == nil { 6 | return nil 7 | } 8 | _ = OnIntransitiveActivity(act, func(in *IntransitiveActivity) error { 9 | FlattenIntransitiveActivityProperties(in) 10 | return nil 11 | }) 12 | act.Object = FlattenToIRI(act.Object) 13 | return act 14 | } 15 | 16 | // FlattenIntransitiveActivityProperties flattens the Activity's properties from Object type to IRI 17 | func FlattenIntransitiveActivityProperties(act *IntransitiveActivity) *IntransitiveActivity { 18 | if act == nil { 19 | return nil 20 | } 21 | act.Actor = FlattenToIRI(act.Actor) 22 | act.Target = FlattenToIRI(act.Target) 23 | act.Result = FlattenToIRI(act.Result) 24 | act.Origin = FlattenToIRI(act.Origin) 25 | act.Result = FlattenToIRI(act.Result) 26 | act.Instrument = FlattenToIRI(act.Instrument) 27 | _ = OnObject(act, func(o *Object) error { 28 | FlattenObjectProperties(o) 29 | return nil 30 | }) 31 | return act 32 | } 33 | 34 | // FlattenItemCollection flattens an Item Collection to their respective IRIs 35 | func FlattenItemCollection(col ItemCollection) ItemCollection { 36 | if col == nil { 37 | return col 38 | } 39 | for k, it := range ItemCollectionDeduplication(&col) { 40 | if iri := it.GetLink(); iri != "" { 41 | col[k] = iri 42 | } 43 | } 44 | return col 45 | } 46 | 47 | // FlattenCollection flattens a Collection's objects to their respective IRIs 48 | func FlattenCollection(col *Collection) *Collection { 49 | if col == nil { 50 | return col 51 | } 52 | col.Items = FlattenItemCollection(col.Items) 53 | 54 | return col 55 | } 56 | 57 | // FlattenOrderedCollection flattens an OrderedCollection's objects to their respective IRIs 58 | func FlattenOrderedCollection(col *OrderedCollection) *OrderedCollection { 59 | if col == nil { 60 | return col 61 | } 62 | col.OrderedItems = FlattenItemCollection(col.OrderedItems) 63 | 64 | return col 65 | } 66 | 67 | // FlattenActorProperties flattens the Actor's properties from Object types to IRI 68 | func FlattenActorProperties(a *Actor) *Actor { 69 | if a == nil { 70 | return nil 71 | } 72 | OnObject(a, func(o *Object) error { 73 | FlattenObjectProperties(o) 74 | return nil 75 | }) 76 | return a 77 | } 78 | 79 | // FlattenObjectProperties flattens the Object's properties from Object types to IRI 80 | func FlattenObjectProperties(o *Object) *Object { 81 | if o == nil { 82 | return nil 83 | } 84 | o.Replies = Flatten(o.Replies) 85 | o.Shares = Flatten(o.Shares) 86 | o.Likes = Flatten(o.Likes) 87 | o.AttributedTo = Flatten(o.AttributedTo) 88 | o.To = FlattenItemCollection(o.To) 89 | o.Bto = FlattenItemCollection(o.Bto) 90 | o.CC = FlattenItemCollection(o.CC) 91 | o.BCC = FlattenItemCollection(o.BCC) 92 | o.Audience = FlattenItemCollection(o.Audience) 93 | // o.Tag = FlattenItemCollection(o.Tag) 94 | return o 95 | } 96 | 97 | // FlattenProperties flattens the Item's properties from Object types to IRI 98 | func FlattenProperties(it Item) Item { 99 | if IsNil(it) { 100 | return nil 101 | } 102 | typ := it.GetType() 103 | if IntransitiveActivityTypes.Contains(typ) { 104 | _ = OnIntransitiveActivity(it, func(a *IntransitiveActivity) error { 105 | FlattenIntransitiveActivityProperties(a) 106 | return nil 107 | }) 108 | } else if ActivityTypes.Contains(typ) { 109 | _ = OnActivity(it, func(a *Activity) error { 110 | FlattenActivityProperties(a) 111 | return nil 112 | }) 113 | } 114 | if ActorTypes.Contains(typ) { 115 | OnActor(it, func(a *Actor) error { 116 | FlattenActorProperties(a) 117 | return nil 118 | }) 119 | } 120 | if ObjectTypes.Contains(typ) { 121 | OnObject(it, func(o *Object) error { 122 | FlattenObjectProperties(o) 123 | return nil 124 | }) 125 | } 126 | return it 127 | } 128 | 129 | // Flatten checks if Item can be flattened to an IRI or array of IRIs and returns it if so 130 | func Flatten(it Item) Item { 131 | if IsNil(it) { 132 | return nil 133 | } 134 | if it.IsCollection() { 135 | OnCollectionIntf(it, func(c CollectionInterface) error { 136 | it = FlattenItemCollection(c.Collection()).Normalize() 137 | return nil 138 | }) 139 | return it 140 | } 141 | return it.GetLink() 142 | } 143 | -------------------------------------------------------------------------------- /flatten_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestFlattenPersonProperties(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestFlattenProperties(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestFlattenItemCollection(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestFlattenCollection(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestFlattenOrderedCollection(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestFlattenIntransitiveActivityProperties(t *testing.T) { 29 | type args struct { 30 | act *IntransitiveActivity 31 | } 32 | tests := []struct { 33 | name string 34 | args args 35 | want *IntransitiveActivity 36 | }{ 37 | { 38 | name: "blank", 39 | args: args{&IntransitiveActivity{}}, 40 | want: &IntransitiveActivity{}, 41 | }, 42 | { 43 | name: "flatten-actor", 44 | args: args{&IntransitiveActivity{Actor: &Actor{ID: "example-actor-iri"}}}, 45 | want: &IntransitiveActivity{Actor: IRI("example-actor-iri")}, 46 | }, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | if got := FlattenIntransitiveActivityProperties(tt.args.act); !reflect.DeepEqual(got, tt.want) { 51 | t.Errorf("FlattenIntransitiveActivityProperties() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestFlattenActivityProperties(t *testing.T) { 58 | type args struct { 59 | act *Activity 60 | } 61 | tests := []struct { 62 | name string 63 | args args 64 | want *Activity 65 | }{ 66 | { 67 | name: "blank", 68 | args: args{&Activity{}}, 69 | want: &Activity{}, 70 | }, 71 | { 72 | name: "flatten-actor", 73 | args: args{&Activity{Actor: &Actor{ID: "example-actor-iri"}}}, 74 | want: &Activity{Actor: IRI("example-actor-iri")}, 75 | }, 76 | { 77 | name: "flatten-object", 78 | args: args{&Activity{Object: &Object{ID: "example-actor-iri"}}}, 79 | want: &Activity{Object: IRI("example-actor-iri")}, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | if got := FlattenActivityProperties(tt.args.act); !reflect.DeepEqual(got, tt.want) { 85 | t.Errorf("FlattenActivityProperties() = %v, want %v", got, tt.want) 86 | } 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-ap/activitypub 2 | 3 | go 1.18 4 | 5 | require ( 6 | git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 7 | github.com/go-ap/errors v0.0.0-20250527110557-c8db454e53fd 8 | github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 9 | github.com/valyala/fastjson v1.6.4 10 | ) 11 | -------------------------------------------------------------------------------- /intransitive_activity_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestIntransitiveActivityNew(t *testing.T) { 9 | testValue := ID("test") 10 | var testType ActivityVocabularyType = "Arrive" 11 | 12 | a := IntransitiveActivityNew(testValue, testType) 13 | 14 | if a.ID != testValue { 15 | t.Errorf("IntransitiveActivity Id '%v' different than expected '%v'", a.ID, testValue) 16 | } 17 | if a.Type != testType { 18 | t.Errorf("IntransitiveActivity Type '%v' different than expected '%v'", a.Type, testType) 19 | } 20 | 21 | g := IntransitiveActivityNew(testValue, "") 22 | 23 | if g.ID != testValue { 24 | t.Errorf("IntransitiveActivity Id '%v' different than expected '%v'", g.ID, testValue) 25 | } 26 | if g.Type != IntransitiveActivityType { 27 | t.Errorf("IntransitiveActivity Type '%v' different than expected '%v'", g.Type, IntransitiveActivityType) 28 | } 29 | } 30 | 31 | func TestIntransitiveActivityRecipients(t *testing.T) { 32 | bob := PersonNew("bob") 33 | alice := PersonNew("alice") 34 | foo := OrganizationNew("foo") 35 | bar := GroupNew("bar") 36 | 37 | a := IntransitiveActivityNew("test", "t") 38 | 39 | a.To.Append(bob) 40 | a.To.Append(alice) 41 | a.To.Append(foo) 42 | a.To.Append(bar) 43 | if len(a.To) != 4 { 44 | t.Errorf("%T.To should have exactly 4(four) elements, not %d", a, len(a.To)) 45 | } 46 | 47 | a.To.Append(bar) 48 | a.To.Append(alice) 49 | a.To.Append(foo) 50 | a.To.Append(bob) 51 | if len(a.To) != 4 { 52 | t.Errorf("%T.To should have exactly 4(eight) elements, not %d", a, len(a.To)) 53 | } 54 | 55 | a.Recipients() 56 | if len(a.To) != 4 { 57 | t.Errorf("%T.To should have exactly 4(four) elements, not %d", a, len(a.To)) 58 | } 59 | 60 | b := ActivityNew("t", "test", nil) 61 | 62 | b.To.Append(bar) 63 | b.To.Append(alice) 64 | b.To.Append(foo) 65 | b.To.Append(bob) 66 | b.Bto.Append(bar) 67 | b.Bto.Append(alice) 68 | b.Bto.Append(foo) 69 | b.Bto.Append(bob) 70 | b.CC.Append(bar) 71 | b.CC.Append(alice) 72 | b.CC.Append(foo) 73 | b.CC.Append(bob) 74 | b.BCC.Append(bar) 75 | b.BCC.Append(alice) 76 | b.BCC.Append(foo) 77 | b.BCC.Append(bob) 78 | 79 | b.Recipients() 80 | if len(b.To) != 4 { 81 | t.Errorf("%T.To should have exactly 4(four) elements, not %d", b, len(b.To)) 82 | } 83 | if len(b.Bto) != 0 { 84 | t.Errorf("%T.Bto should have exactly 0(zero) elements, not %d", b, len(b.Bto)) 85 | } 86 | if len(b.CC) != 0 { 87 | t.Errorf("%T.CC should have exactly 0(zero) elements, not %d", b, len(b.CC)) 88 | } 89 | if len(b.BCC) != 0 { 90 | t.Errorf("%T.BCC should have exactly 0(zero) elements, not %d", b, len(b.BCC)) 91 | } 92 | var err error 93 | recIds := make([]ID, 0) 94 | err = checkDedup(b.To, &recIds) 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | err = checkDedup(b.Bto, &recIds) 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | err = checkDedup(b.CC, &recIds) 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | err = checkDedup(b.BCC, &recIds) 107 | if err != nil { 108 | t.Error(err) 109 | } 110 | } 111 | 112 | func TestIntransitiveActivity_GetLink(t *testing.T) { 113 | i := IntransitiveActivityNew("test", QuestionType) 114 | 115 | if i.GetID() != "test" { 116 | t.Errorf("%T should return an empty %T object. Received %#v", i, i, i) 117 | } 118 | } 119 | 120 | func TestIntransitiveActivity_GetObject(t *testing.T) { 121 | i := IntransitiveActivityNew("test", QuestionType) 122 | 123 | if i.GetID() != "test" || i.GetType() != QuestionType { 124 | t.Errorf("%T should not return an empty %T object. Received %#v", i, i, i) 125 | } 126 | } 127 | 128 | func TestIntransitiveActivity_IsLink(t *testing.T) { 129 | i := IntransitiveActivityNew("test", QuestionType) 130 | 131 | if i.IsLink() { 132 | t.Errorf("%T should not respond true to IsLink", i) 133 | } 134 | } 135 | 136 | func TestIntransitiveActivity_IsObject(t *testing.T) { 137 | i := IntransitiveActivityNew("test", ActivityType) 138 | 139 | if !i.IsObject() { 140 | t.Errorf("%T should respond true to IsObject", i) 141 | } 142 | } 143 | 144 | func TestIntransitiveActivity_Recipients(t *testing.T) { 145 | to := PersonNew("bob") 146 | o := ObjectNew(ArticleType) 147 | cc := PersonNew("alice") 148 | 149 | o.ID = "something" 150 | 151 | c := IntransitiveActivityNew("act", IntransitiveActivityType) 152 | c.To.Append(to) 153 | c.CC.Append(cc) 154 | c.BCC.Append(cc) 155 | 156 | c.Recipients() 157 | 158 | var err error 159 | recIds := make([]ID, 0) 160 | err = checkDedup(c.To, &recIds) 161 | if err != nil { 162 | t.Error(err) 163 | } 164 | err = checkDedup(c.Bto, &recIds) 165 | if err != nil { 166 | t.Error(err) 167 | } 168 | err = checkDedup(c.CC, &recIds) 169 | if err != nil { 170 | t.Error(err) 171 | } 172 | err = checkDedup(c.BCC, &recIds) 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | } 177 | 178 | func TestIntransitiveActivity_GetID(t *testing.T) { 179 | a := IntransitiveActivityNew("test", IntransitiveActivityType) 180 | 181 | if a.GetID() != "test" { 182 | t.Errorf("%T should return an empty %T object. Received %#v", a, a.GetID(), a.GetID()) 183 | } 184 | } 185 | 186 | func TestIntransitiveActivity_GetType(t *testing.T) { 187 | { 188 | a := IntransitiveActivityNew("test", IntransitiveActivityType) 189 | if a.GetType() != IntransitiveActivityType { 190 | t.Errorf("GetType should return %q for %T, received %q", IntransitiveActivityType, a, a.GetType()) 191 | } 192 | } 193 | { 194 | a := IntransitiveActivityNew("test", ArriveType) 195 | if a.GetType() != ArriveType { 196 | t.Errorf("GetType should return %q for %T, received %q", ArriveType, a, a.GetType()) 197 | } 198 | } 199 | { 200 | a := IntransitiveActivityNew("test", QuestionType) 201 | if a.GetType() != QuestionType { 202 | t.Errorf("GetType should return %q for %T, received %q", QuestionType, a, a.GetType()) 203 | } 204 | } 205 | } 206 | 207 | func TestToIntransitiveActivity(t *testing.T) { 208 | var it Item 209 | act := IntransitiveActivityNew("test", TravelType) 210 | it = act 211 | 212 | a, err := ToIntransitiveActivity(it) 213 | if err != nil { 214 | t.Error(err) 215 | } 216 | if a != act { 217 | t.Errorf("Invalid activity returned by ToActivity #%v", a) 218 | } 219 | 220 | ob := ObjectNew(ArticleType) 221 | it = ob 222 | 223 | o, err := ToIntransitiveActivity(it) 224 | if err == nil { 225 | t.Errorf("Error returned when calling ToActivity with object should not be nil") 226 | } 227 | if o != nil { 228 | t.Errorf("Invalid return by ToActivity #%v, should have been nil", o) 229 | } 230 | } 231 | 232 | func TestIntransitiveActivity_Clean(t *testing.T) { 233 | t.Skipf("TODO") 234 | } 235 | 236 | func TestIntransitiveActivity_IsCollection(t *testing.T) { 237 | t.Skipf("TODO") 238 | } 239 | 240 | func TestIntransitiveActivity_UnmarshalJSON(t *testing.T) { 241 | t.Skipf("TODO") 242 | } 243 | 244 | func TestArriveNew(t *testing.T) { 245 | testValue := ID("test") 246 | 247 | a := ArriveNew(testValue) 248 | 249 | if a.ID != testValue { 250 | t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue) 251 | } 252 | if a.Type != ArriveType { 253 | t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, ArriveType) 254 | } 255 | } 256 | 257 | func TestTravelNew(t *testing.T) { 258 | testValue := ID("test") 259 | 260 | a := TravelNew(testValue) 261 | 262 | if a.ID != testValue { 263 | t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue) 264 | } 265 | if a.Type != TravelType { 266 | t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, TravelType) 267 | } 268 | } 269 | 270 | func TestIntransitiveActivity_Equals(t *testing.T) { 271 | type fields struct { 272 | ID ID 273 | Type ActivityVocabularyType 274 | Name NaturalLanguageValues 275 | Attachment Item 276 | AttributedTo Item 277 | Audience ItemCollection 278 | Content NaturalLanguageValues 279 | Context Item 280 | MediaType MimeType 281 | EndTime time.Time 282 | Generator Item 283 | Icon Item 284 | Image Item 285 | InReplyTo Item 286 | Location Item 287 | Preview Item 288 | Published time.Time 289 | Replies Item 290 | StartTime time.Time 291 | Summary NaturalLanguageValues 292 | Tag ItemCollection 293 | Updated time.Time 294 | URL Item 295 | To ItemCollection 296 | Bto ItemCollection 297 | CC ItemCollection 298 | BCC ItemCollection 299 | Duration time.Duration 300 | Likes Item 301 | Shares Item 302 | Source Source 303 | Actor Item 304 | Target Item 305 | Result Item 306 | Origin Item 307 | Instrument Item 308 | } 309 | tests := []struct { 310 | name string 311 | fields fields 312 | arg Item 313 | want bool 314 | }{ 315 | { 316 | name: "equal-empty-intransitive-activity", 317 | fields: fields{}, 318 | arg: IntransitiveActivity{}, 319 | want: true, 320 | }, 321 | { 322 | name: "equal-intransitive-activity-just-id", 323 | fields: fields{ID: "test"}, 324 | arg: IntransitiveActivity{ID: "test"}, 325 | want: true, 326 | }, 327 | { 328 | name: "equal-intransitive-activity-id", 329 | fields: fields{ID: "test", URL: IRI("example.com")}, 330 | arg: IntransitiveActivity{ID: "test"}, 331 | want: true, 332 | }, 333 | { 334 | name: "equal-false-with-id-and-url", 335 | fields: fields{ID: "test"}, 336 | arg: IntransitiveActivity{ID: "test", URL: IRI("example.com")}, 337 | want: false, 338 | }, 339 | { 340 | name: "not a valid intransitive-activity", 341 | fields: fields{ID: "http://example.com"}, 342 | arg: Link{ID: "http://example.com"}, 343 | want: false, 344 | }, 345 | } 346 | for _, tt := range tests { 347 | t.Run(tt.name, func(t *testing.T) { 348 | a := IntransitiveActivity{ 349 | ID: tt.fields.ID, 350 | Type: tt.fields.Type, 351 | Name: tt.fields.Name, 352 | Attachment: tt.fields.Attachment, 353 | AttributedTo: tt.fields.AttributedTo, 354 | Audience: tt.fields.Audience, 355 | Content: tt.fields.Content, 356 | Context: tt.fields.Context, 357 | MediaType: tt.fields.MediaType, 358 | EndTime: tt.fields.EndTime, 359 | Generator: tt.fields.Generator, 360 | Icon: tt.fields.Icon, 361 | Image: tt.fields.Image, 362 | InReplyTo: tt.fields.InReplyTo, 363 | Location: tt.fields.Location, 364 | Preview: tt.fields.Preview, 365 | Published: tt.fields.Published, 366 | Replies: tt.fields.Replies, 367 | StartTime: tt.fields.StartTime, 368 | Summary: tt.fields.Summary, 369 | Tag: tt.fields.Tag, 370 | Updated: tt.fields.Updated, 371 | URL: tt.fields.URL, 372 | To: tt.fields.To, 373 | Bto: tt.fields.Bto, 374 | CC: tt.fields.CC, 375 | BCC: tt.fields.BCC, 376 | Duration: tt.fields.Duration, 377 | Likes: tt.fields.Likes, 378 | Shares: tt.fields.Shares, 379 | Source: tt.fields.Source, 380 | Actor: tt.fields.Actor, 381 | Target: tt.fields.Target, 382 | Result: tt.fields.Result, 383 | Origin: tt.fields.Origin, 384 | Instrument: tt.fields.Instrument, 385 | } 386 | if got := a.Equals(tt.arg); got != tt.want { 387 | t.Errorf("Equals() = %v, want %v", got, tt.want) 388 | } 389 | }) 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /iri.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/url" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/valyala/fastjson" 14 | ) 15 | 16 | const ( 17 | // ActivityBaseURI the URI for the ActivityStreams namespace 18 | ActivityBaseURI = IRI("https://www.w3.org/ns/activitystreams") 19 | // SecurityContextURI the URI for the security namespace (for an Actor's PublicKey) 20 | SecurityContextURI = IRI("https://w3id.org/security/v1") 21 | // PublicNS is the reference to the Public entity in the ActivityStreams namespace. 22 | // 23 | // Public Addressing 24 | // 25 | // https://www.w3.org/TR/activitypub/#public-addressing 26 | // 27 | // In addition to [ActivityStreams] collections and objects, Activities may additionally be addressed to the 28 | // special "public" collection, with the identifier https://www.w3.org/ns/activitystreams#Public. For example: 29 | // 30 | // { 31 | // "@context": "https://www.w3.org/ns/activitystreams", 32 | // "id": "https://www.w3.org/ns/activitystreams#Public", 33 | // "type": "Collection" 34 | // } 35 | // Activities addressed to this special URI shall be accessible to all users, without authentication. 36 | // Implementations MUST NOT deliver to the "public" special collection; it is not capable of receiving 37 | // actual activities. However, actors MAY have a sharedInbox endpoint which is available for efficient 38 | // shared delivery of public posts (as well as posts to followers-only); see 7.1.3 Shared Inbox Delivery. 39 | // 40 | // NOTE 41 | // Compacting an ActivityStreams object using the ActivityStreams JSON-LD context might result in 42 | // https://www.w3.org/ns/activitystreams#Public being represented as simply Public or as:Public which are valid 43 | // representations of the Public collection. Implementations which treat ActivityStreams objects as simply JSON 44 | // rather than converting an incoming activity over to a local context using JSON-LD tooling should be aware 45 | // of this and should be prepared to accept all three representations. 46 | PublicNS = ActivityBaseURI + "#Public" 47 | ) 48 | 49 | // JsonLDContext is a slice of IRIs that form the default context for the objects in the 50 | // GoActivitypub vocabulary. 51 | // It does not represent just the default ActivityStreams public namespace, but it also 52 | // has the W3 Permanent Identifier Community Group's Security namespace, which appears 53 | // in the Actor type objects, which contain public key related data. 54 | var JsonLDContext = []IRI{ 55 | ActivityBaseURI, 56 | SecurityContextURI, 57 | } 58 | 59 | type ( 60 | // IRI is a Internationalized Resource Identifiers (IRIs) RFC3987 61 | IRI string 62 | IRIs []IRI 63 | ) 64 | 65 | func (i IRI) Format(s fmt.State, verb rune) { 66 | switch verb { 67 | case 's', 'v': 68 | _, _ = io.WriteString(s, i.String()) 69 | } 70 | } 71 | 72 | // String returns the String value of the IRI object 73 | func (i IRI) String() string { 74 | return string(i) 75 | } 76 | 77 | // GetLink 78 | func (i IRI) GetLink() IRI { 79 | return i 80 | } 81 | 82 | // URL 83 | func (i IRI) URL() (*url.URL, error) { 84 | if i == "" { 85 | return nil, errors.New("empty IRI") 86 | } 87 | return url.Parse(string(i)) 88 | } 89 | 90 | // UnmarshalJSON decodes an incoming JSON document into the receiver object. 91 | func (i *IRI) UnmarshalJSON(s []byte) error { 92 | *i = IRI(strings.Trim(string(s), "\"")) 93 | return nil 94 | } 95 | 96 | // MarshalJSON encodes the receiver object to a JSON document. 97 | func (i IRI) MarshalJSON() ([]byte, error) { 98 | if i == "" { 99 | return nil, nil 100 | } 101 | b := make([]byte, 0) 102 | JSONWrite(&b, '"') 103 | JSONWriteS(&b, i.String()) 104 | JSONWrite(&b, '"') 105 | return b, nil 106 | } 107 | 108 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 109 | func (i *IRI) UnmarshalBinary(data []byte) error { 110 | return i.GobDecode(data) 111 | } 112 | 113 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 114 | func (i IRI) MarshalBinary() ([]byte, error) { 115 | return i.GobEncode() 116 | } 117 | 118 | // GobEncode 119 | func (i IRI) GobEncode() ([]byte, error) { 120 | return []byte(i), nil 121 | } 122 | 123 | // GobEncode 124 | func (i IRIs) GobEncode() ([]byte, error) { 125 | if len(i) == 0 { 126 | return []byte{}, nil 127 | } 128 | b := bytes.Buffer{} 129 | gg := gob.NewEncoder(&b) 130 | bb := make([][]byte, 0) 131 | for _, iri := range i { 132 | bb = append(bb, []byte(iri)) 133 | } 134 | if err := gg.Encode(bb); err != nil { 135 | return nil, err 136 | } 137 | return b.Bytes(), nil 138 | } 139 | 140 | // GobDecode 141 | func (i *IRI) GobDecode(data []byte) error { 142 | *i = IRI(data) 143 | return nil 144 | } 145 | 146 | func (i *IRIs) GobDecode(data []byte) error { 147 | if len(data) == 0 { 148 | // NOTE(marius): this behaviour diverges from vanilla gob package 149 | return nil 150 | } 151 | err := gob.NewDecoder(bytes.NewReader(data)).Decode(i) 152 | if err == nil { 153 | return nil 154 | } 155 | bb := make([][]byte, 0) 156 | err = gob.NewDecoder(bytes.NewReader(data)).Decode(&bb) 157 | if err != nil { 158 | return err 159 | } 160 | for _, b := range bb { 161 | *i = append(*i, IRI(b)) 162 | } 163 | return nil 164 | } 165 | 166 | // AddPath concatenates el elements as a path to i 167 | func (i IRI) AddPath(el ...string) IRI { 168 | iri := strings.TrimRight(i.String(), "/") 169 | return IRI(iri + filepath.Clean(filepath.Join("/", filepath.Join(el...)))) 170 | } 171 | 172 | // GetID 173 | func (i IRI) GetID() ID { 174 | return i 175 | } 176 | 177 | // GetType 178 | func (i IRI) GetType() ActivityVocabularyType { 179 | return IRIType 180 | } 181 | 182 | // IsLink 183 | func (i IRI) IsLink() bool { 184 | return true 185 | } 186 | 187 | // IsObject 188 | func (i IRI) IsObject() bool { 189 | return false 190 | } 191 | 192 | // IsCollection returns false for IRI objects 193 | func (i IRI) IsCollection() bool { 194 | return false 195 | } 196 | 197 | // FlattenToIRI checks if Item can be flatten to an IRI and returns it if so 198 | func FlattenToIRI(it Item) Item { 199 | if !IsNil(it) && it.IsObject() && len(it.GetLink()) > 0 { 200 | return it.GetLink() 201 | } 202 | return it 203 | } 204 | 205 | func (i IRIs) MarshalJSON() ([]byte, error) { 206 | if len(i) == 0 { 207 | return []byte{'[', ']'}, nil 208 | } 209 | b := make([]byte, 0) 210 | writeCommaIfNotEmpty := func(notEmpty bool) { 211 | if notEmpty { 212 | JSONWriteS(&b, ",") 213 | } 214 | } 215 | JSONWrite(&b, '[') 216 | for k, iri := range i { 217 | writeCommaIfNotEmpty(k > 0) 218 | JSONWrite(&b, '"') 219 | JSONWriteS(&b, iri.String()) 220 | JSONWrite(&b, '"') 221 | } 222 | JSONWrite(&b, ']') 223 | return b, nil 224 | } 225 | 226 | func (i *IRIs) UnmarshalJSON(data []byte) error { 227 | if i == nil { 228 | return nil 229 | } 230 | p := fastjson.Parser{} 231 | val, err := p.ParseBytes(data) 232 | if err != nil { 233 | return err 234 | } 235 | switch val.Type() { 236 | case fastjson.TypeString: 237 | if iri, ok := asIRI(val); ok && len(iri) > 0 { 238 | *i = append(*i, iri) 239 | } 240 | case fastjson.TypeArray: 241 | for _, v := range val.GetArray() { 242 | if iri, ok := asIRI(v); ok && len(iri) > 0 { 243 | *i = append(*i, iri) 244 | } 245 | } 246 | } 247 | return nil 248 | } 249 | 250 | // GetID returns the ID corresponding to ItemCollection 251 | func (i IRIs) GetID() ID { 252 | return EmptyID 253 | } 254 | 255 | // GetLink returns the empty IRI 256 | func (i IRIs) GetLink() IRI { 257 | return EmptyIRI 258 | } 259 | 260 | // GetType returns the ItemCollection's type 261 | func (i IRIs) GetType() ActivityVocabularyType { 262 | return CollectionOfIRIs 263 | } 264 | 265 | // IsLink returns false for an ItemCollection object 266 | func (i IRIs) IsLink() bool { 267 | return false 268 | } 269 | 270 | // IsObject returns true for a ItemCollection object 271 | func (i IRIs) IsObject() bool { 272 | return false 273 | } 274 | 275 | // IsCollection returns true for IRI slices 276 | func (i IRIs) IsCollection() bool { 277 | return true 278 | } 279 | 280 | // Append facilitates adding elements to IRI slices 281 | // and ensures IRIs implements the Collection interface 282 | func (i *IRIs) Append(it ...Item) error { 283 | for _, ob := range it { 284 | if (*i).Contains(ob.GetLink()) { 285 | continue 286 | } 287 | *i = append(*i, ob.GetLink()) 288 | } 289 | return nil 290 | } 291 | 292 | func (i *IRIs) Collection() ItemCollection { 293 | res := make(ItemCollection, len(*i)) 294 | for k, iri := range *i { 295 | res[k] = iri 296 | } 297 | return res 298 | } 299 | 300 | func (i *IRIs) Count() uint { 301 | return uint(len(*i)) 302 | } 303 | 304 | // Contains verifies if IRIs array contains the received one 305 | func (i IRIs) Contains(r Item) bool { 306 | if len(i) == 0 { 307 | return false 308 | } 309 | for _, iri := range i { 310 | if r.GetLink().Equals(iri, false) { 311 | return true 312 | } 313 | } 314 | return false 315 | } 316 | 317 | func validURL(u *url.URL, checkScheme bool) bool { 318 | if u == nil { 319 | return false 320 | } 321 | if len(u.Host) == 0 { 322 | return false 323 | } 324 | if checkScheme { 325 | return len(u.Scheme) > 0 326 | } 327 | return true 328 | } 329 | 330 | func stripFragment(u string) string { 331 | p := strings.Index(u, "#") 332 | if p <= 0 { 333 | p = len(u) 334 | } 335 | return u[:p] 336 | } 337 | 338 | func stripScheme(u string) string { 339 | p := strings.Index(u, "://") 340 | if p < 0 { 341 | p = 0 342 | } 343 | return u[p:] 344 | } 345 | 346 | func irisEqual(i1, i2 IRI, checkScheme bool) bool { 347 | u, e := i1.URL() 348 | uw, ew := i2.URL() 349 | if e != nil || ew != nil || !validURL(u, checkScheme) || !validURL(uw, checkScheme) { 350 | return strings.EqualFold(i1.String(), i2.String()) 351 | } 352 | if checkScheme { 353 | if !strings.EqualFold(u.Scheme, uw.Scheme) { 354 | return false 355 | } 356 | } 357 | if !strings.EqualFold(u.Host, uw.Host) { 358 | return false 359 | } 360 | if !(u.Path == "/" && uw.Path == "" || u.Path == "" && uw.Path == "/") && 361 | !strings.EqualFold(filepath.Clean(u.Path), filepath.Clean(uw.Path)) { 362 | return false 363 | } 364 | uq := u.Query() 365 | uwq := uw.Query() 366 | if len(uq) != len(uwq) { 367 | return false 368 | } 369 | for k, uqv := range uq { 370 | uwqv, ok := uwq[k] 371 | if !ok { 372 | return false 373 | } 374 | if len(uqv) != len(uwqv) { 375 | return false 376 | } 377 | for _, uqvv := range uqv { 378 | eq := false 379 | for _, uwqvv := range uwqv { 380 | if uwqvv == uqvv { 381 | eq = true 382 | continue 383 | } 384 | } 385 | if !eq { 386 | return false 387 | } 388 | } 389 | } 390 | return true 391 | } 392 | 393 | // Equals verifies if our receiver IRI is equals with the "with" IRI 394 | func (i IRI) Equals(with IRI, checkScheme bool) bool { 395 | is := stripFragment(string(i)) 396 | ws := stripFragment(string(with)) 397 | if !checkScheme { 398 | is = stripScheme(is) 399 | ws = stripScheme(ws) 400 | } 401 | if strings.EqualFold(is, ws) { 402 | return true 403 | } 404 | return irisEqual(i, with, checkScheme) 405 | } 406 | 407 | func hostSplit(h string) (string, string) { 408 | pieces := strings.Split(h, ":") 409 | if len(pieces) == 0 { 410 | return "", "" 411 | } 412 | if len(pieces) == 1 { 413 | return pieces[0], "" 414 | } 415 | return pieces[0], pieces[1] 416 | } 417 | 418 | func (i IRI) Contains(what IRI, checkScheme bool) bool { 419 | u, e := i.URL() 420 | uw, ew := what.URL() 421 | if e != nil || ew != nil { 422 | return strings.Contains(i.String(), what.String()) 423 | } 424 | if checkScheme { 425 | if u.Scheme != uw.Scheme { 426 | return false 427 | } 428 | } 429 | uHost, _ := hostSplit(u.Host) 430 | uwHost, _ := hostSplit(uw.Host) 431 | if uHost != uwHost { 432 | return false 433 | } 434 | p := u.Path 435 | if p != "" { 436 | p = filepath.Clean(p) 437 | } 438 | pw := uw.Path 439 | if pw != "" { 440 | pw = filepath.Clean(pw) 441 | } 442 | return strings.Contains(p, pw) 443 | } 444 | 445 | func (i IRI) ItemsMatch(col ...Item) bool { 446 | for _, it := range col { 447 | if match := it.GetLink().Contains(i, false); !match { 448 | return false 449 | } 450 | } 451 | return true 452 | } 453 | -------------------------------------------------------------------------------- /iri_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestIRI_GetLink(t *testing.T) { 12 | val := "http://example.com" 13 | u := IRI(val) 14 | if u.GetLink() != IRI(val) { 15 | t.Errorf("IRI %q should equal %q", u, val) 16 | } 17 | } 18 | 19 | func TestIRI_String(t *testing.T) { 20 | val := "http://example.com" 21 | u := IRI(val) 22 | if u.String() != val { 23 | t.Errorf("IRI %q should equal %q", u, val) 24 | } 25 | } 26 | 27 | func TestIRI_GetID(t *testing.T) { 28 | i := IRI("http://example.com") 29 | if id := i.GetID(); !id.IsValid() || id != ID(i) { 30 | t.Errorf("ID %q (%T) should equal %q (%T)", id, id, i, ID(i)) 31 | } 32 | } 33 | 34 | func TestIRI_GetType(t *testing.T) { 35 | i := IRI("http://example.com") 36 | if i.GetType() != IRIType { 37 | t.Errorf("Invalid type for %T object %s, expected %s", i, i.GetType(), IRIType) 38 | } 39 | } 40 | 41 | func TestIRI_IsLink(t *testing.T) { 42 | i := IRI("http://example.com") 43 | if i.IsLink() != true { 44 | t.Errorf("%T.IsLink() returned %t, expected %t", i, i.IsLink(), true) 45 | } 46 | } 47 | 48 | func TestIRI_IsObject(t *testing.T) { 49 | i := IRI("http://example.com") 50 | if i.IsObject() { 51 | t.Errorf("%T.IsObject() returned %t, expected %t", i, i.IsObject(), false) 52 | } 53 | ii := IRI([]byte("https://example.com")) 54 | if ii.IsObject() { 55 | t.Errorf("%T.IsObject() returned %t, expected %t", ii, ii.IsObject(), false) 56 | } 57 | iii := &ii 58 | if iii.IsObject() { 59 | t.Errorf("%T.IsObject() returned %t, expected %t", iii, iii.IsObject(), false) 60 | } 61 | } 62 | 63 | func TestIRI_UnmarshalJSON(t *testing.T) { 64 | val := "http://example.com" 65 | i := IRI("") 66 | 67 | err := i.UnmarshalJSON([]byte(val)) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | if val != i.String() { 72 | t.Errorf("%T invalid value after Unmarshal %q, expected %q", i, i, val) 73 | } 74 | } 75 | 76 | func TestIRI_MarshalJSON(t *testing.T) { 77 | value := []byte("http://example.com") 78 | i := IRI(value) 79 | 80 | v, err := i.MarshalJSON() 81 | if err != nil { 82 | t.Error(err) 83 | } 84 | expected := fmt.Sprintf("%q", value) 85 | if expected != string(v) { 86 | t.Errorf("Invalid value after MarshalJSON: %s, expected %s", v, expected) 87 | } 88 | } 89 | 90 | func TestFlattenToIRI(t *testing.T) { 91 | t.Skipf("TODO") 92 | } 93 | 94 | func TestIRI_URL(t *testing.T) { 95 | tests := []struct { 96 | name string 97 | i IRI 98 | want *url.URL 99 | wantErr bool 100 | }{ 101 | { 102 | name: "empty", 103 | i: "", 104 | want: nil, 105 | wantErr: true, // empty IRI 106 | }, 107 | { 108 | name: "example with fragment", 109 | i: "https://example.com/#fragment", 110 | want: &url.URL{ 111 | Scheme: "https", 112 | Host: "example.com", 113 | Path: "/", 114 | Fragment: "fragment", 115 | }, 116 | wantErr: false, 117 | }, 118 | } 119 | for _, tt := range tests { 120 | t.Run(tt.name, func(t *testing.T) { 121 | got, err := tt.i.URL() 122 | if (err != nil) != tt.wantErr { 123 | t.Errorf("URL() error = %v, wantErr %v", err, tt.wantErr) 124 | return 125 | } 126 | if !reflect.DeepEqual(got, tt.want) { 127 | t.Errorf("URL() got = %v, want %v", got, tt.want) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestIRIs_Contains(t *testing.T) { 134 | t.Skipf("TODO") 135 | } 136 | 137 | func TestIRI_Contains(t *testing.T) { 138 | t.Skip("TODO") 139 | } 140 | 141 | func TestIRI_IsCollection(t *testing.T) { 142 | t.Skip("TODO") 143 | } 144 | 145 | func TestIRIs_UnmarshalJSON(t *testing.T) { 146 | type args struct { 147 | d []byte 148 | } 149 | tests := []struct { 150 | name string 151 | args args 152 | obj IRIs 153 | want IRIs 154 | err error 155 | }{ 156 | { 157 | name: "empty", 158 | args: args{[]byte{'{', '}'}}, 159 | want: nil, 160 | err: nil, 161 | }, 162 | { 163 | name: "IRI", 164 | args: args{[]byte("\"http://example.com\"")}, 165 | want: IRIs{IRI("http://example.com")}, 166 | err: nil, 167 | }, 168 | { 169 | name: "IRIs", 170 | args: args{[]byte(fmt.Sprintf("[%q, %q, %q]", "http://example.com", "http://example.net", "http://example.org"))}, 171 | want: IRIs{ 172 | IRI("http://example.com"), 173 | IRI("http://example.net"), 174 | IRI("http://example.org"), 175 | }, 176 | err: nil, 177 | }, 178 | } 179 | for _, tt := range tests { 180 | t.Run(tt.name, func(t *testing.T) { 181 | err := tt.obj.UnmarshalJSON(tt.args.d) 182 | if (err != nil && tt.err == nil) || (err == nil && tt.err != nil) { 183 | if !errors.Is(err, tt.err) { 184 | t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.err) 185 | } 186 | return 187 | } 188 | if !assertDeepEquals(t.Errorf, tt.obj, tt.want) { 189 | t.Errorf("UnmarshalJSON() got = %#v, want %#v", tt.obj, tt.want) 190 | } 191 | }) 192 | } 193 | } 194 | 195 | func TestIRIs_MarshalJSON(t *testing.T) { 196 | value1 := []byte("http://example.com") 197 | value2 := []byte("http://example.net") 198 | value3 := []byte("http://example.org") 199 | i := IRIs{ 200 | IRI(value1), 201 | IRI(value2), 202 | IRI(value3), 203 | } 204 | 205 | v, err := i.MarshalJSON() 206 | if err != nil { 207 | t.Error(err) 208 | } 209 | expected := fmt.Sprintf("[%q, %q, %q]", value1, value2, value3) 210 | if expected == string(v) { 211 | t.Errorf("Invalid value after MarshalJSON: %s, expected %s", v, expected) 212 | } 213 | } 214 | 215 | func TestIRI_AddPath(t *testing.T) { 216 | t.Skip("TODO") 217 | } 218 | 219 | func TestIRI_ItemMatches(t *testing.T) { 220 | t.Skip("TODO") 221 | } 222 | 223 | func TestIRI_GobDecode(t *testing.T) { 224 | tests := []struct { 225 | name string 226 | i IRI 227 | data []byte 228 | wantErr bool 229 | }{ 230 | { 231 | name: "empty", 232 | i: "", 233 | data: []byte{}, 234 | wantErr: false, 235 | }, 236 | { 237 | name: "some iri", 238 | i: "https://example.com", 239 | data: gobValue([]byte("https://example.com")), 240 | wantErr: false, 241 | }, 242 | } 243 | for _, tt := range tests { 244 | t.Run(tt.name, func(t *testing.T) { 245 | if err := tt.i.GobDecode(tt.data); (err != nil) != tt.wantErr { 246 | t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr) 247 | } 248 | }) 249 | } 250 | } 251 | 252 | func TestIRI_GobEncode(t *testing.T) { 253 | tests := []struct { 254 | name string 255 | i IRI 256 | want []byte 257 | wantErr bool 258 | }{ 259 | { 260 | name: "empty", 261 | i: "", 262 | want: []byte{}, 263 | wantErr: false, 264 | }, 265 | { 266 | name: "some iri", 267 | i: "https://example.com", 268 | want: []byte("https://example.com"), 269 | wantErr: false, 270 | }, 271 | } 272 | for _, tt := range tests { 273 | t.Run(tt.name, func(t *testing.T) { 274 | got, err := tt.i.GobEncode() 275 | if (err != nil) != tt.wantErr { 276 | t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr) 277 | return 278 | } 279 | if !reflect.DeepEqual(got, tt.want) { 280 | t.Errorf("GobEncode() got = %v, want %v", got, tt.want) 281 | } 282 | }) 283 | } 284 | } 285 | 286 | func TestIRI_Equals(t *testing.T) { 287 | type args struct { 288 | with IRI 289 | check bool 290 | } 291 | tests := []struct { 292 | name string 293 | i IRI 294 | args args 295 | want bool 296 | }{ 297 | { 298 | name: "just host", 299 | i: "http://example.com", 300 | args: args{ 301 | with: IRI("http://example.com"), 302 | check: true, 303 | }, 304 | want: true, 305 | }, 306 | { 307 | name: "host and path", 308 | i: "http://example.com/ana/are/mere", 309 | args: args{ 310 | with: IRI("http://example.com/ana/are/mere"), 311 | check: true, 312 | }, 313 | want: true, 314 | }, 315 | { 316 | name: "different schemes check scheme", 317 | i: "https://example.com/ana/are/mere", 318 | args: args{ 319 | with: IRI("http://example.com/ana/are/mere"), 320 | check: true, 321 | }, 322 | want: false, 323 | }, 324 | { 325 | name: "different schemes, don't check scheme", 326 | i: "https://example.com/ana/are/mere", 327 | args: args{ 328 | with: IRI("http://example.com/ana/are/mere"), 329 | check: false, 330 | }, 331 | want: true, 332 | }, 333 | { 334 | name: "same host different scheme, same query - different order", 335 | i: "https://example.com?ana=mere&foo=bar", 336 | args: args{ 337 | with: "http://example.com?foo=bar&ana=mere", 338 | check: false, 339 | }, 340 | want: true, 341 | }, 342 | { 343 | name: "same host, different scheme and same path, same query different order", 344 | i: "http://example.com/ana/are/mere?foo=bar&ana=mere", 345 | args: args{ 346 | with: "https://example.com/ana/are/mere?ana=mere&foo=bar", 347 | check: false, 348 | }, 349 | want: true, 350 | }, 351 | { 352 | name: "same host different scheme, same query", 353 | i: "https://example.com?ana=mere", 354 | args: args{ 355 | with: "http://example.com?ana=mere", 356 | check: false, 357 | }, 358 | want: true, 359 | }, 360 | { 361 | name: "different host same scheme", 362 | i: "http://example1.com", 363 | args: args{ 364 | with: "http://example.com", 365 | check: true, 366 | }, 367 | want: false, 368 | }, 369 | { 370 | name: "same host, same scheme and different path", 371 | i: "same host, same scheme and different path", 372 | args: args{ 373 | with: "http://example.com/ana/are/mere", 374 | check: true, 375 | }, 376 | want: false, 377 | }, 378 | { 379 | name: "same host same scheme, different query key", 380 | i: "http://example.com?ana1=mere", 381 | args: args{ 382 | with: "http://example.com?ana=mere", 383 | check: false, 384 | }, 385 | want: false, 386 | }, 387 | { 388 | name: "same host same scheme, different query value", 389 | i: "http://example.com?ana=mere", 390 | args: args{ 391 | with: "http://example.com?ana=mere1", 392 | check: false, 393 | }, 394 | // This was true in the url.Parse implementation 395 | want: false, 396 | }, 397 | } 398 | for _, tt := range tests { 399 | t.Run(tt.name, func(t *testing.T) { 400 | if got := tt.i.Equals(tt.args.with, tt.args.check); got != tt.want { 401 | t.Errorf("Equals() = %v, want %v", got, tt.want) 402 | } 403 | }) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // Item struct 10 | type Item = ObjectOrLink 11 | 12 | const ( 13 | // EmptyIRI represents a zero length IRI 14 | EmptyIRI IRI = "" 15 | // NilIRI represents by convention an IRI which is nil 16 | // Its use is mostly to check if a property of an ActivityPub Item is nil 17 | NilIRI IRI = "-" 18 | 19 | // EmptyID represents a zero length ID 20 | EmptyID = EmptyIRI 21 | // NilID represents by convention an ID which is nil, see details of NilIRI 22 | NilID = NilIRI 23 | ) 24 | 25 | func itemsNeedSwapping(i1, i2 Item) bool { 26 | if IsIRI(i1) && !IsIRI(i2) { 27 | return true 28 | } 29 | t1 := i1.GetType() 30 | t2 := i2.GetType() 31 | if ObjectTypes.Contains(t2) { 32 | return !ObjectTypes.Contains(t1) 33 | } 34 | return false 35 | } 36 | 37 | // ItemsEqual checks if it and with Items are equal 38 | func ItemsEqual(it, with Item) bool { 39 | if IsNil(it) || IsNil(with) { 40 | return IsNil(with) && IsNil(it) 41 | } 42 | if itemsNeedSwapping(it, with) { 43 | return ItemsEqual(with, it) 44 | } 45 | result := false 46 | if IsIRI(with) || IsIRI(it) { 47 | // NOTE(marius): I'm not sure this logic is sound: 48 | // if only one item is an IRI it should not be equal to the other even if it has the same ID 49 | result = it.GetLink().Equals(with.GetLink(), false) 50 | } else if IsItemCollection(it) { 51 | if !IsItemCollection(with) { 52 | return false 53 | } 54 | _ = OnItemCollection(it, func(c *ItemCollection) error { 55 | result = c.Equals(with) 56 | return nil 57 | }) 58 | } else if IsObject(it) { 59 | _ = OnObject(it, func(i *Object) error { 60 | result = i.Equals(with) 61 | return nil 62 | }) 63 | if ActivityTypes.Contains(with.GetType()) { 64 | _ = OnActivity(it, func(i *Activity) error { 65 | result = i.Equals(with) 66 | return nil 67 | }) 68 | } else if ActorTypes.Contains(with.GetType()) { 69 | _ = OnActor(it, func(i *Actor) error { 70 | result = i.Equals(with) 71 | return nil 72 | }) 73 | } else if it.IsCollection() { 74 | if it.GetType() == CollectionType { 75 | _ = OnCollection(it, func(c *Collection) error { 76 | result = c.Equals(with) 77 | return nil 78 | }) 79 | } 80 | if it.GetType() == OrderedCollectionType { 81 | _ = OnOrderedCollection(it, func(c *OrderedCollection) error { 82 | result = c.Equals(with) 83 | return nil 84 | }) 85 | } 86 | if it.GetType() == CollectionPageType { 87 | _ = OnCollectionPage(it, func(c *CollectionPage) error { 88 | result = c.Equals(with) 89 | return nil 90 | }) 91 | } 92 | if it.GetType() == OrderedCollectionPageType { 93 | _ = OnOrderedCollectionPage(it, func(c *OrderedCollectionPage) error { 94 | result = c.Equals(with) 95 | return nil 96 | }) 97 | } 98 | } 99 | } 100 | return result 101 | } 102 | 103 | // IsItemCollection returns if the current Item interface holds a Collection 104 | func IsItemCollection(it LinkOrIRI) bool { 105 | _, ok := it.(ItemCollection) 106 | _, okP := it.(*ItemCollection) 107 | return ok || okP || IsIRIs(it) 108 | } 109 | 110 | // IsIRI returns if the current Item interface holds an IRI 111 | func IsIRI(it LinkOrIRI) bool { 112 | _, okV := it.(IRI) 113 | _, okP := it.(*IRI) 114 | return okV || okP 115 | } 116 | 117 | // IsIRIs returns if the current Item interface holds an IRI slice 118 | func IsIRIs(it LinkOrIRI) bool { 119 | _, okV := it.(IRIs) 120 | _, okP := it.(*IRIs) 121 | return okV || okP 122 | } 123 | 124 | // IsLink returns if the current Item interface holds a Link 125 | func IsLink(it LinkOrIRI) bool { 126 | _, okV := it.(Link) 127 | _, okP := it.(*Link) 128 | return okV || okP 129 | } 130 | 131 | // IsObject returns if the current Item interface holds an Object 132 | func IsObject(it LinkOrIRI) bool { 133 | switch ob := it.(type) { 134 | case Actor, *Actor, 135 | Object, *Object, Profile, *Profile, Place, *Place, Relationship, *Relationship, Tombstone, *Tombstone, 136 | Activity, *Activity, IntransitiveActivity, *IntransitiveActivity, Question, *Question, 137 | Collection, *Collection, CollectionPage, *CollectionPage, 138 | OrderedCollection, *OrderedCollection, OrderedCollectionPage, *OrderedCollectionPage: 139 | return ob != nil 140 | default: 141 | return false 142 | } 143 | } 144 | 145 | // IsNil checks if the object matching an ObjectOrLink interface is nil 146 | func IsNil(it LinkOrIRI) bool { 147 | if it == nil { 148 | return true 149 | } 150 | // This is the default if the argument can't be cast to Object, as is the case for an ItemCollection 151 | isNil := false 152 | if IsIRI(it) { 153 | isNil = len(it.GetLink()) == 0 || strings.EqualFold(it.GetLink().String(), NilIRI.String()) 154 | } else if IsItemCollection(it) { 155 | if v, ok := it.(ItemCollection); ok { 156 | return v == nil 157 | } 158 | if v, ok := it.(*ItemCollection); ok { 159 | return v == nil 160 | } 161 | if v, ok := it.(IRIs); ok { 162 | return v == nil 163 | } 164 | if v, ok := it.(*IRIs); ok { 165 | return v == nil 166 | } 167 | } else if IsObject(it) { 168 | if ob, ok := it.(Item); ok { 169 | _ = OnObject(ob, func(o *Object) error { 170 | isNil = o == nil 171 | return nil 172 | }) 173 | } 174 | } else if IsLink(it) { 175 | _ = OnLink(it, func(l *Link) error { 176 | isNil = l == nil 177 | return nil 178 | }) 179 | } else { 180 | // NOTE(marius): we're not dealing with a type that we know about, so we use slow reflection 181 | // as we still care about the result 182 | v := reflect.ValueOf(it) 183 | isNil = v.Kind() == reflect.Pointer && v.IsNil() 184 | } 185 | return isNil 186 | } 187 | 188 | func ErrorInvalidType[T Objects | Links | IRIs](received LinkOrIRI) error { 189 | return fmt.Errorf("unable to convert %T to %T", received, new(T)) 190 | } 191 | 192 | // OnItem runs function "fn" on the Item "it", with the benefit of destructuring "it" to individual 193 | // items if it's actually an ItemCollection or an object holding an ItemCollection 194 | // 195 | // It is expected that the caller handles the logic of dealing with different Item implementations 196 | // internally in "fn". 197 | func OnItem(it Item, fn func(Item) error) error { 198 | if it == nil { 199 | return nil 200 | } 201 | if !IsItemCollection(it) { 202 | return fn(it) 203 | } 204 | return OnItemCollection(it, func(col *ItemCollection) error { 205 | for _, it := range *col { 206 | if err := OnItem(it, fn); err != nil { 207 | return err 208 | } 209 | } 210 | return nil 211 | }) 212 | } 213 | -------------------------------------------------------------------------------- /item_collection.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // ItemCollection represents an array of items 8 | type ItemCollection []Item 9 | 10 | // GetID returns the ID corresponding to ItemCollection 11 | func (i ItemCollection) GetID() ID { 12 | return EmptyID 13 | } 14 | 15 | // GetLink returns the empty IRI 16 | func (i ItemCollection) GetLink() IRI { 17 | return EmptyIRI 18 | } 19 | 20 | // GetType returns the ItemCollection's type 21 | func (i ItemCollection) GetType() ActivityVocabularyType { 22 | return CollectionOfItems 23 | } 24 | 25 | // IsLink returns false for an ItemCollection object 26 | func (i ItemCollection) IsLink() bool { 27 | return false 28 | } 29 | 30 | // IsObject returns true for a ItemCollection object 31 | func (i ItemCollection) IsObject() bool { 32 | return false 33 | } 34 | 35 | func (i ItemCollection) IRIs() IRIs { 36 | if i == nil { 37 | return nil 38 | } 39 | 40 | iris := make(IRIs, 0, len(i)) 41 | for _, it := range i { 42 | iris = append(iris, it.GetLink()) 43 | } 44 | return iris 45 | } 46 | 47 | // MarshalJSON encodes the receiver object to a JSON document. 48 | func (i ItemCollection) MarshalJSON() ([]byte, error) { 49 | if i == nil { 50 | return nil, nil 51 | } 52 | b := make([]byte, 0) 53 | JSONWriteItemCollectionValue(&b, i, true) 54 | return b, nil 55 | } 56 | 57 | // Append facilitates adding elements to Item arrays 58 | // and ensures ItemCollection implements the Collection interface 59 | func (i *ItemCollection) Append(it ...Item) error { 60 | for _, ob := range it { 61 | if i.Contains(ob) { 62 | continue 63 | } 64 | *i = append(*i, ob) 65 | } 66 | return nil 67 | } 68 | 69 | // Count returns the length of Items in the item collection 70 | func (i *ItemCollection) Count() uint { 71 | if i == nil { 72 | return 0 73 | } 74 | return uint(len(*i)) 75 | } 76 | 77 | // First returns the ID corresponding to ItemCollection 78 | func (i ItemCollection) First() Item { 79 | if len(i) == 0 { 80 | return nil 81 | } 82 | return i[0] 83 | } 84 | 85 | // Normalize returns the first item if the collection contains only one, 86 | // the full collection if the collection contains more than one item, 87 | // or nil 88 | func (i ItemCollection) Normalize() Item { 89 | if len(i) == 0 { 90 | return nil 91 | } 92 | if len(i) == 1 { 93 | return i[0] 94 | } 95 | return i 96 | } 97 | 98 | // Collection returns the current object as collection interface 99 | func (i *ItemCollection) Collection() ItemCollection { 100 | return *i 101 | } 102 | 103 | // IsCollection returns true for ItemCollection arrays 104 | func (i ItemCollection) IsCollection() bool { 105 | return true 106 | } 107 | 108 | // Contains verifies if IRIs array contains the received one 109 | func (i ItemCollection) Contains(r Item) bool { 110 | if len(i) == 0 { 111 | return false 112 | } 113 | for _, it := range i { 114 | if ItemsEqual(it, r) { 115 | return true 116 | } 117 | } 118 | return false 119 | } 120 | 121 | // Remove removes the r Item from the i ItemCollection if it contains it 122 | func (i *ItemCollection) Remove(r Item) { 123 | li := len(*i) 124 | if li == 0 { 125 | return 126 | } 127 | if r == nil { 128 | return 129 | } 130 | remIdx := -1 131 | for idx, it := range *i { 132 | if ItemsEqual(it, r) { 133 | remIdx = idx 134 | } 135 | } 136 | if remIdx == -1 { 137 | return 138 | } 139 | if remIdx < li-1 { 140 | *i = append((*i)[:remIdx], (*i)[remIdx+1:]...) 141 | } else { 142 | *i = (*i)[:remIdx] 143 | } 144 | } 145 | 146 | // ItemCollectionDeduplication normalizes the received arguments lists into a single unified one 147 | func ItemCollectionDeduplication(recCols ...*ItemCollection) ItemCollection { 148 | rec := make(ItemCollection, 0) 149 | 150 | for _, recCol := range recCols { 151 | if recCol == nil { 152 | continue 153 | } 154 | 155 | toRemove := make([]int, 0) 156 | for i, cur := range *recCol { 157 | save := true 158 | if cur == nil { 159 | continue 160 | } 161 | var testIt IRI 162 | if cur.IsObject() { 163 | testIt = cur.GetID() 164 | } else if cur.IsLink() { 165 | testIt = cur.GetLink() 166 | } else { 167 | continue 168 | } 169 | for _, it := range rec { 170 | if testIt.Equals(it.GetID(), false) { 171 | // mark the element for removal 172 | toRemove = append(toRemove, i) 173 | save = false 174 | } 175 | } 176 | if save { 177 | rec = append(rec, testIt) 178 | } 179 | } 180 | 181 | sort.Sort(sort.Reverse(sort.IntSlice(toRemove))) 182 | for _, idx := range toRemove { 183 | *recCol = append((*recCol)[:idx], (*recCol)[idx+1:]...) 184 | } 185 | } 186 | return rec 187 | } 188 | 189 | // ToItemCollection returns the item collection contained as part of OrderedCollection, OrderedCollectionPage, 190 | // Collection and CollectionPage. 191 | // It also converts an IRI slice into an equivalent ItemCollection. 192 | func ToItemCollection(it Item) (*ItemCollection, error) { 193 | switch i := it.(type) { 194 | case *ItemCollection: 195 | return i, nil 196 | case ItemCollection: 197 | return &i, nil 198 | case *OrderedCollection: 199 | return &i.OrderedItems, nil 200 | case *OrderedCollectionPage: 201 | return &i.OrderedItems, nil 202 | case *Collection: 203 | return &i.Items, nil 204 | case *CollectionPage: 205 | return &i.Items, nil 206 | case IRIs: 207 | iris := make(ItemCollection, len(i)) 208 | for j, ob := range i { 209 | iris[j] = ob 210 | } 211 | return &iris, nil 212 | case *IRIs: 213 | iris := make(ItemCollection, len(*i)) 214 | for j, ob := range *i { 215 | iris[j] = ob 216 | } 217 | return &iris, nil 218 | default: 219 | return reflectItemToType[ItemCollection](it) 220 | } 221 | return nil, ErrorInvalidType[ItemCollection](it) 222 | } 223 | 224 | // ToIRIs 225 | func ToIRIs(it Item) (*IRIs, error) { 226 | switch i := it.(type) { 227 | case *IRIs: 228 | return i, nil 229 | case IRIs: 230 | return &i, nil 231 | case ItemCollection: 232 | iris := i.IRIs() 233 | return &iris, nil 234 | case *ItemCollection: 235 | iris := make(IRIs, len(*i)) 236 | for j, ob := range *i { 237 | iris[j] = ob.GetLink() 238 | } 239 | return &iris, nil 240 | default: 241 | return reflectItemToType[IRIs](it) 242 | } 243 | return nil, ErrorInvalidType[IRIs](it) 244 | } 245 | 246 | // ItemsMatch 247 | func (i ItemCollection) ItemsMatch(col ...Item) bool { 248 | for _, it := range col { 249 | if match := i.Contains(it); !match { 250 | return false 251 | } 252 | } 253 | return true 254 | } 255 | 256 | // Equals 257 | func (i ItemCollection) Equals(with Item) bool { 258 | if IsNil(with) { 259 | return IsNil(i) || len(i) == 0 260 | } 261 | if !with.IsCollection() { 262 | return false 263 | } 264 | if with.GetType() != CollectionOfItems { 265 | return false 266 | } 267 | result := true 268 | _ = OnItemCollection(with, func(w *ItemCollection) error { 269 | if w.Count() != i.Count() { 270 | result = false 271 | return nil 272 | } 273 | for _, it := range i { 274 | if !w.Contains(it.GetLink()) { 275 | result = false 276 | return nil 277 | } 278 | } 279 | return nil 280 | }) 281 | return result 282 | } 283 | 284 | // Clean removes Bto and BCC properties on all the members of the collection 285 | func (i ItemCollection) Clean() { 286 | for j, it := range i { 287 | i[j] = CleanRecipients(it) 288 | } 289 | } 290 | 291 | func (i ItemCollection) Recipients() ItemCollection { 292 | all := make(ItemCollection, 0) 293 | for _, it := range i { 294 | _ = OnObject(it, func(ob *Object) error { 295 | aud := ob.Audience 296 | _ = all.Append(ItemCollectionDeduplication(&ob.To, &ob.CC, &ob.Bto, &ob.BCC, &aud)...) 297 | return nil 298 | }) 299 | } 300 | return ItemCollectionDeduplication(&all) 301 | } 302 | -------------------------------------------------------------------------------- /item_collection_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestItemCollection_Append(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestItemCollection_Collection(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestItemCollection_GetID(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestItemCollection_GetLink(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestItemCollection_GetType(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestItemCollection_IsLink(t *testing.T) { 29 | t.Skipf("TODO") 30 | } 31 | 32 | func TestItemCollection_IsObject(t *testing.T) { 33 | t.Skipf("TODO") 34 | } 35 | 36 | func TestItemCollection_First(t *testing.T) { 37 | t.Skipf("TODO") 38 | } 39 | 40 | func TestItemCollection_Count(t *testing.T) { 41 | t.Skipf("TODO") 42 | } 43 | 44 | func TestItemCollection_Contains(t *testing.T) { 45 | t.Skipf("TODO") 46 | } 47 | 48 | func TestItemCollection_IsCollection(t *testing.T) { 49 | t.Skipf("TODO") 50 | } 51 | 52 | func TestToItemCollection(t *testing.T) { 53 | t.Skipf("TODO") 54 | } 55 | 56 | func TestItemCollection_Remove(t *testing.T) { 57 | tests := []struct { 58 | name string 59 | i ItemCollection 60 | arg Item 61 | }{ 62 | { 63 | name: "empty_collection_nil_item", 64 | i: ItemCollection{}, 65 | arg: nil, 66 | }, 67 | { 68 | name: "empty_collection_non_nil_item", 69 | i: ItemCollection{}, 70 | arg: &Object{}, 71 | }, 72 | { 73 | name: "non_empty_collection_nil_item", 74 | i: ItemCollection{ 75 | &Object{ID: "test"}, 76 | }, 77 | arg: nil, 78 | }, 79 | { 80 | name: "non_empty_collection_non_contained_item_empty_ID", 81 | i: ItemCollection{ 82 | &Object{ID: "test"}, 83 | }, 84 | arg: &Object{}, 85 | }, 86 | { 87 | name: "non_empty_collection_non_contained_item", 88 | i: ItemCollection{ 89 | &Object{ID: "test"}, 90 | }, 91 | arg: &Object{ID: "test123"}, 92 | }, 93 | { 94 | name: "non_empty_collection_just_contained_item", 95 | i: ItemCollection{ 96 | &Object{ID: "test"}, 97 | }, 98 | arg: &Object{ID: "test"}, 99 | }, 100 | { 101 | name: "non_empty_collection_contained_item_first_pos", 102 | i: ItemCollection{ 103 | &Object{ID: "test"}, 104 | &Object{ID: "test123"}, 105 | }, 106 | arg: &Object{ID: "test"}, 107 | }, 108 | { 109 | name: "non_empty_collection_contained_item_not_first_pos", 110 | i: ItemCollection{ 111 | &Object{ID: "test123"}, 112 | &Object{ID: "test"}, 113 | &Object{ID: "test321"}, 114 | }, 115 | arg: &Object{ID: "test"}, 116 | }, 117 | { 118 | name: "non_empty_collection_contained_item_last_pos", 119 | i: ItemCollection{ 120 | &Object{ID: "test123"}, 121 | &Object{ID: "test"}, 122 | }, 123 | arg: &Object{ID: "test"}, 124 | }, 125 | } 126 | for _, tt := range tests { 127 | t.Run(tt.name, func(t *testing.T) { 128 | origContains := tt.i.Contains(tt.arg) 129 | origLen := tt.i.Count() 130 | should := "" 131 | does := "n't" 132 | if origContains { 133 | should = "n't" 134 | does = "" 135 | } 136 | 137 | tt.i.Remove(tt.arg) 138 | if tt.i.Contains(tt.arg) { 139 | t.Errorf("%T should%s contain %T, but it does%s: %#v", tt.i, should, tt.arg, does, tt.i) 140 | } 141 | if origContains { 142 | if tt.i.Count() > origLen-1 { 143 | t.Errorf("%T should have a count lower than %d, got %d", tt.i, origLen, tt.i.Count()) 144 | } 145 | } else { 146 | if tt.i.Count() != origLen { 147 | t.Errorf("%T should have a count equal to %d, got %d", tt.i, origLen, tt.i.Count()) 148 | } 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func TestItemCollectionDeduplication(t *testing.T) { 155 | tests := []struct { 156 | name string 157 | args []*ItemCollection 158 | want ItemCollection 159 | remaining []*ItemCollection 160 | }{ 161 | { 162 | name: "empty", 163 | }, 164 | { 165 | name: "no-overlap", 166 | args: []*ItemCollection{ 167 | { 168 | IRI("https://example.com"), 169 | IRI("https://example.com/2"), 170 | }, 171 | { 172 | IRI("https://example.com/1"), 173 | }, 174 | }, 175 | want: ItemCollection{ 176 | IRI("https://example.com"), 177 | IRI("https://example.com/2"), 178 | IRI("https://example.com/1"), 179 | }, 180 | remaining: []*ItemCollection{ 181 | { 182 | IRI("https://example.com"), 183 | IRI("https://example.com/2"), 184 | }, 185 | { 186 | IRI("https://example.com/1"), 187 | }, 188 | }, 189 | }, 190 | { 191 | name: "some-overlap", 192 | args: []*ItemCollection{ 193 | { 194 | IRI("https://example.com"), 195 | IRI("https://example.com/2"), 196 | }, 197 | { 198 | IRI("https://example.com/1"), 199 | IRI("https://example.com/2"), 200 | }, 201 | }, 202 | want: ItemCollection{ 203 | IRI("https://example.com"), 204 | IRI("https://example.com/2"), 205 | IRI("https://example.com/1"), 206 | }, 207 | remaining: []*ItemCollection{ 208 | { 209 | IRI("https://example.com"), 210 | IRI("https://example.com/2"), 211 | }, 212 | { 213 | IRI("https://example.com/1"), 214 | }, 215 | }, 216 | }, 217 | { 218 | name: "test from spammy", 219 | args: []*ItemCollection{ 220 | { 221 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 222 | IRI("https://www.w3.org/ns/activitystreams#Public"), 223 | }, 224 | { 225 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 226 | }, 227 | { 228 | IRI("https://example.dev"), 229 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 230 | IRI("https://www.w3.org/ns/activitystreams#Public"), 231 | }, 232 | }, 233 | want: ItemCollection{ 234 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 235 | IRI("https://www.w3.org/ns/activitystreams#Public"), 236 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 237 | IRI("https://example.dev"), 238 | }, 239 | remaining: []*ItemCollection{ 240 | { 241 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 242 | IRI("https://www.w3.org/ns/activitystreams#Public"), 243 | }, 244 | { 245 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 246 | }, 247 | { 248 | IRI("https://example.dev"), 249 | }, 250 | }, 251 | }, 252 | { 253 | name: "different order for spammy test", 254 | args: []*ItemCollection{ 255 | { 256 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 257 | IRI("https://www.w3.org/ns/activitystreams#Public"), 258 | }, 259 | { 260 | IRI("https://example.dev"), 261 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 262 | IRI("https://www.w3.org/ns/activitystreams#Public"), 263 | }, 264 | { 265 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 266 | }, 267 | }, 268 | want: ItemCollection{ 269 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 270 | IRI("https://www.w3.org/ns/activitystreams#Public"), 271 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 272 | IRI("https://example.dev"), 273 | }, 274 | remaining: []*ItemCollection{ 275 | { 276 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4/followers"), 277 | IRI("https://www.w3.org/ns/activitystreams#Public"), 278 | }, 279 | { 280 | IRI("https://example.dev"), 281 | IRI("https://example.dev/a801139a-0d9a-4703-b0a5-9d14ae1438e4"), 282 | }, 283 | {}, 284 | }, 285 | }, 286 | } 287 | for _, tt := range tests { 288 | t.Run(tt.name, func(t *testing.T) { 289 | if got := ItemCollectionDeduplication(tt.args...); !tt.want.Equals(got) { 290 | t.Errorf("ItemCollectionDeduplication() = %v, want %v", got, tt.want) 291 | } 292 | if len(tt.remaining) != len(tt.args) { 293 | t.Errorf("ItemCollectionDeduplication() arguments count %d, want %d", len(tt.args), len(tt.remaining)) 294 | } 295 | for i, remArg := range tt.remaining { 296 | arg := tt.args[i] 297 | if !remArg.Equals(arg) { 298 | t.Errorf("ItemCollectionDeduplication() argument at pos %d = %v, want %v", i, arg, remArg) 299 | } 300 | } 301 | }) 302 | } 303 | } 304 | 305 | func TestToItemCollection1(t *testing.T) { 306 | tests := []struct { 307 | name string 308 | it Item 309 | want *ItemCollection 310 | wantErr bool 311 | }{ 312 | { 313 | name: "empty", 314 | }, 315 | { 316 | name: "IRIs to ItemCollection", 317 | it: IRIs{"https://example.com", "https://example.com/example"}, 318 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 319 | wantErr: false, 320 | }, 321 | { 322 | name: "ItemCollection to ItemCollection", 323 | it: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 324 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 325 | wantErr: false, 326 | }, 327 | { 328 | name: "*ItemCollection to ItemCollection", 329 | it: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 330 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 331 | wantErr: false, 332 | }, 333 | { 334 | name: "Collection to ItemCollection", 335 | it: &Collection{Items: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}}, 336 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 337 | wantErr: false, 338 | }, 339 | { 340 | name: "CollectionPage to ItemCollection", 341 | it: &CollectionPage{Items: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}}, 342 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 343 | wantErr: false, 344 | }, 345 | { 346 | name: "OrderedCollection to ItemCollection", 347 | it: &OrderedCollection{OrderedItems: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}}, 348 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 349 | wantErr: false, 350 | }, 351 | { 352 | name: "OrderedCollectionPage to ItemOrderedCollection", 353 | it: &OrderedCollectionPage{OrderedItems: ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}}, 354 | want: &ItemCollection{IRI("https://example.com"), IRI("https://example.com/example")}, 355 | wantErr: false, 356 | }, 357 | } 358 | for _, tt := range tests { 359 | t.Run(tt.name, func(t *testing.T) { 360 | got, err := ToItemCollection(tt.it) 361 | if (err != nil) != tt.wantErr { 362 | t.Errorf("ToItemCollection() error = %v, wantErr %v", err, tt.wantErr) 363 | return 364 | } 365 | if !reflect.DeepEqual(got, tt.want) { 366 | t.Errorf("ToItemCollection() got = %v, want %v", got, tt.want) 367 | } 368 | }) 369 | } 370 | } 371 | 372 | func TestItemCollection_IRIs(t *testing.T) { 373 | tests := []struct { 374 | name string 375 | i ItemCollection 376 | want IRIs 377 | }{ 378 | { 379 | name: "empty", 380 | i: nil, 381 | want: nil, 382 | }, 383 | { 384 | name: "one item", 385 | i: ItemCollection{ 386 | &Object{ID: "https://example.com"}, 387 | }, 388 | want: IRIs{"https://example.com"}, 389 | }, 390 | { 391 | name: "two items", 392 | i: ItemCollection{ 393 | &Object{ID: "https://example.com"}, 394 | &Actor{ID: "https://example.com/~jdoe"}, 395 | }, 396 | want: IRIs{"https://example.com", "https://example.com/~jdoe"}, 397 | }, 398 | { 399 | name: "mixed items", 400 | i: ItemCollection{ 401 | &Object{ID: "https://example.com"}, 402 | IRI("https://example.com/666"), 403 | &Actor{ID: "https://example.com/~jdoe"}, 404 | }, 405 | want: IRIs{"https://example.com", "https://example.com/666", "https://example.com/~jdoe"}, 406 | }, 407 | } 408 | for _, tt := range tests { 409 | t.Run(tt.name, func(t *testing.T) { 410 | if got := tt.i.IRIs(); !reflect.DeepEqual(got, tt.want) { 411 | t.Errorf("IRIs() = %v, want %v", got, tt.want) 412 | } 413 | }) 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /item_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import "testing" 4 | 5 | func TestFlatten(t *testing.T) { 6 | t.Skipf("TODO") 7 | } 8 | 9 | func TestItemsEqual(t *testing.T) { 10 | type args struct { 11 | it Item 12 | with Item 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want bool 18 | }{ 19 | { 20 | name: "nil_items_equal", 21 | args: args{nil, nil}, 22 | want: true, 23 | }, 24 | { 25 | name: "nil_item_with_object", 26 | args: args{nil, &Object{}}, 27 | want: false, 28 | }, 29 | { 30 | name: "nil_item_with_object#1", 31 | args: args{&Object{}, nil}, 32 | want: false, 33 | }, 34 | { 35 | name: "empty_objects", 36 | args: args{&Object{}, &Object{}}, 37 | want: true, 38 | }, 39 | { 40 | name: "empty_objects_different_alias_type", 41 | args: args{&Activity{}, &Object{}}, 42 | want: true, 43 | }, 44 | { 45 | name: "empty_objects_different_alias_type#1", 46 | args: args{&Actor{}, &Object{}}, 47 | want: true, 48 | }, 49 | { 50 | name: "same_id_object", 51 | args: args{&Object{ID: "test"}, &Object{ID: "test"}}, 52 | want: true, 53 | }, 54 | { 55 | name: "same_id_object_different_alias", 56 | args: args{&Activity{ID: "test"}, &Object{ID: "test"}}, 57 | want: true, 58 | }, 59 | { 60 | name: "same_id_object_different_alias#1", 61 | args: args{&Activity{ID: "test"}, &Actor{ID: "test"}}, 62 | want: true, 63 | }, 64 | { 65 | name: "different_id_objects", 66 | args: args{&Object{ID: "test1"}, &Object{ID: "test"}}, 67 | want: false, 68 | }, 69 | { 70 | name: "different_id_types", 71 | args: args{&Object{ID: "test", Type: NoteType}, &Object{ID: "test", Type: ArticleType}}, 72 | want: false, 73 | }, 74 | } 75 | for _, tt := range tests { 76 | t.Run(tt.name, func(t *testing.T) { 77 | if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want { 78 | t.Errorf("ItemsEqual() = %v, want %v", got, tt.want) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestIsNil(t *testing.T) { 85 | type args struct { 86 | it Item 87 | } 88 | var ( 89 | o *Object 90 | col *ItemCollection 91 | iris *IRIs 92 | obNil Item = o 93 | colNil Item = col 94 | itIRIs Item = iris 95 | ) 96 | tests := []struct { 97 | name string 98 | args args 99 | want bool 100 | }{ 101 | { 102 | name: "nil is nil", 103 | args: args{ 104 | it: nil, 105 | }, 106 | want: true, 107 | }, 108 | { 109 | name: "Item is nil", 110 | args: args{ 111 | it: Item(nil), 112 | }, 113 | want: true, 114 | }, 115 | { 116 | name: "Object nil", 117 | args: args{ 118 | it: obNil, 119 | }, 120 | want: true, 121 | }, 122 | { 123 | name: "IRIs nil", 124 | args: args{ 125 | it: iris, 126 | }, 127 | want: true, 128 | }, 129 | { 130 | name: "IRIs as Item nil", 131 | args: args{ 132 | it: itIRIs, 133 | }, 134 | want: true, 135 | }, 136 | { 137 | name: "IRIs not nil", 138 | args: args{ 139 | it: IRIs{}, 140 | }, 141 | want: false, 142 | }, 143 | { 144 | name: "IRIs as Item not nil", 145 | args: args{ 146 | it: Item(IRIs{}), 147 | }, 148 | want: false, 149 | }, 150 | { 151 | name: "ItemCollection nil", 152 | args: args{ 153 | it: col, 154 | }, 155 | want: true, 156 | }, 157 | { 158 | name: "ItemCollection as Item nil", 159 | args: args{ 160 | it: colNil, 161 | }, 162 | want: true, 163 | }, 164 | { 165 | name: "ItemCollection not nil", 166 | args: args{ 167 | it: ItemCollection{}, 168 | }, 169 | want: false, 170 | }, 171 | { 172 | name: "object-not-nil", 173 | args: args{ 174 | it: &Object{}, 175 | }, 176 | want: false, 177 | }, 178 | { 179 | name: "place-not-nil", 180 | args: args{ 181 | it: &Place{}, 182 | }, 183 | want: false, 184 | }, 185 | { 186 | name: "tombstone-not-nil", 187 | args: args{ 188 | it: &Tombstone{}, 189 | }, 190 | want: false, 191 | }, 192 | { 193 | name: "collection-not-nil", 194 | args: args{ 195 | it: &Collection{}, 196 | }, 197 | want: false, 198 | }, 199 | { 200 | name: "activity-not-nil", 201 | args: args{ 202 | it: &Activity{}, 203 | }, 204 | want: false, 205 | }, 206 | { 207 | name: "intransitive-activity-not-nil", 208 | args: args{ 209 | it: &IntransitiveActivity{}, 210 | }, 211 | want: false, 212 | }, 213 | { 214 | name: "actor-not-nil", 215 | args: args{ 216 | it: &Actor{}, 217 | }, 218 | want: false, 219 | }, 220 | } 221 | for _, tt := range tests { 222 | t.Run(tt.name, func(t *testing.T) { 223 | if got := IsNil(tt.args.it); got != tt.want { 224 | t.Errorf("IsNil() = %v, want %v", got, tt.want) 225 | } 226 | }) 227 | } 228 | } 229 | 230 | func TestItemsEqual1(t *testing.T) { 231 | type args struct { 232 | it Item 233 | with Item 234 | } 235 | tests := []struct { 236 | name string 237 | args args 238 | want bool 239 | }{ 240 | { 241 | name: "nil", 242 | args: args{}, 243 | want: true, 244 | }, 245 | { 246 | name: "equal empty items", 247 | args: args{ 248 | it: &Object{}, 249 | with: &Actor{}, 250 | }, 251 | want: true, 252 | }, 253 | { 254 | name: "equal same ID items", 255 | args: args{ 256 | it: &Object{ID: "example-1"}, 257 | with: &Object{ID: "example-1"}, 258 | }, 259 | want: true, 260 | }, 261 | { 262 | name: "different IDs", 263 | args: args{ 264 | it: &Object{ID: "example-1"}, 265 | with: &Object{ID: "example-2"}, 266 | }, 267 | want: false, 268 | }, 269 | { 270 | name: "different properties", 271 | args: args{ 272 | it: &Object{ID: "example-1"}, 273 | with: &Object{Type: ArticleType}, 274 | }, 275 | want: false, 276 | }, 277 | } 278 | for _, tt := range tests { 279 | t.Run(tt.name, func(t *testing.T) { 280 | if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want { 281 | t.Errorf("ItemsEqual() = %v, want %v", got, tt.want) 282 | } 283 | }) 284 | } 285 | } 286 | 287 | func TestIsObject(t *testing.T) { 288 | type args struct { 289 | it Item 290 | } 291 | tests := []struct { 292 | name string 293 | args args 294 | want bool 295 | }{ 296 | { 297 | name: "nil", 298 | args: args{}, 299 | want: false, 300 | }, 301 | { 302 | name: "interface with nil value", 303 | args: args{Item(nil)}, 304 | want: false, 305 | }, 306 | { 307 | name: "empty object", 308 | args: args{Object{}}, 309 | want: true, 310 | }, 311 | { 312 | name: "pointer to empty object", 313 | args: args{&Object{}}, 314 | want: true, 315 | }, 316 | } 317 | for _, tt := range tests { 318 | t.Run(tt.name, func(t *testing.T) { 319 | if got := IsObject(tt.args.it); got != tt.want { 320 | t.Errorf("IsObject() = %v, want %v", got, tt.want) 321 | } 322 | }) 323 | } 324 | } 325 | 326 | func TestItemsEqual2(t *testing.T) { 327 | type args struct { 328 | it Item 329 | with Item 330 | } 331 | tests := []struct { 332 | name string 333 | args args 334 | want bool 335 | }{ 336 | { 337 | name: "nil vs nil", 338 | args: args{ 339 | it: nil, 340 | with: nil, 341 | }, 342 | want: true, 343 | }, 344 | { 345 | name: "nil vs object", 346 | args: args{ 347 | it: nil, 348 | with: Object{}, 349 | }, 350 | want: false, 351 | }, 352 | { 353 | name: "object vs nil", 354 | args: args{ 355 | it: Object{}, 356 | with: nil, 357 | }, 358 | want: false, 359 | }, 360 | { 361 | name: "empty object vs empty object", 362 | args: args{ 363 | it: Object{}, 364 | with: Object{}, 365 | }, 366 | want: true, 367 | }, 368 | { 369 | name: "object-id vs empty object", 370 | args: args{ 371 | it: Object{ID: "https://example.com"}, 372 | with: Object{}, 373 | }, 374 | want: false, 375 | }, 376 | { 377 | name: "empty object vs object-id", 378 | args: args{ 379 | it: Object{}, 380 | with: Object{ID: "https://example.com"}, 381 | }, 382 | want: false, 383 | }, 384 | } 385 | for _, tt := range tests { 386 | t.Run(tt.name, func(t *testing.T) { 387 | if got := ItemsEqual(tt.args.it, tt.args.with); got != tt.want { 388 | t.Errorf("ItemsEqual() = %v, want %v", got, tt.want) 389 | } 390 | }) 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /link.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | 8 | "github.com/valyala/fastjson" 9 | ) 10 | 11 | // LinkTypes represent the valid values for a Link object 12 | var LinkTypes = ActivityVocabularyTypes{ 13 | LinkType, 14 | MentionType, 15 | } 16 | 17 | type Links interface { 18 | Link | IRI 19 | } 20 | 21 | // A Link is an indirect, qualified reference to a resource identified by a URL. 22 | // The fundamental model for links is established by [ RFC5988]. 23 | // Many of the properties defined by the Activity Vocabulary allow values that are either instances of APObject or Link. 24 | // When a Link is used, it establishes a qualified relation connecting the subject 25 | // (the containing object) to the resource identified by the href. 26 | // Properties of the Link are properties of the reference as opposed to properties of the resource. 27 | type Link struct { 28 | // Provides the globally unique identifier for an APObject or Link. 29 | ID ID `jsonld:"id,omitempty"` 30 | // Identifies the APObject or Link type. Multiple values may be specified. 31 | Type ActivityVocabularyType `jsonld:"type,omitempty"` 32 | // A simple, human-readable, plain-text name for the object. 33 | // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. 34 | Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` 35 | // A link relation associated with a Link. The value must conform to both the [HTML5] and 36 | // [RFC5988](https://tools.ietf.org/html/rfc5988) "link relation" definitions. 37 | // In the [HTML5], any string not containing the "space" U+0020, "tab" (U+0009), "LF" (U+000A), 38 | // "FF" (U+000C), "CR" (U+000D) or "," (U+002C) characters can be used as a valid link relation. 39 | Rel IRI `jsonld:"rel,omitempty"` 40 | // When used on a Link, identifies the MIME media type of the referenced resource. 41 | MediaType MimeType `jsonld:"mediaType,omitempty"` 42 | // On a Link, specifies a hint as to the rendering height in device-independent pixels of the linked resource. 43 | Height uint `jsonld:"height,omitempty"` 44 | // On a Link, specifies a hint as to the rendering width in device-independent pixels of the linked resource. 45 | Width uint `jsonld:"width,omitempty"` 46 | // Identifies an entity that provides a preview of this object. 47 | Preview Item `jsonld:"preview,omitempty"` 48 | // The target resource pointed to by a Link. 49 | Href IRI `jsonld:"href,omitempty"` 50 | // Hints as to the language used by the target resource. 51 | // Value must be a [BCP47](https://tools.ietf.org/html/bcp47) Language-Tag. 52 | HrefLang LangRef `jsonld:"hrefLang,omitempty"` 53 | } 54 | 55 | // Mention is a specialized Link that represents an @mention. 56 | type Mention = Link 57 | 58 | // LinkNew initializes a new Link 59 | func LinkNew(id ID, typ ActivityVocabularyType) *Link { 60 | if !LinkTypes.Contains(typ) { 61 | typ = LinkType 62 | } 63 | return &Link{ID: id, Type: typ} 64 | } 65 | 66 | // MentionNew initializes a new Mention 67 | func MentionNew(id ID) *Mention { 68 | return &Mention{ID: id, Type: MentionType} 69 | } 70 | 71 | // IsLink validates if current Link is a Link 72 | func (l Link) IsLink() bool { 73 | return l.Type == LinkType || LinkTypes.Contains(l.Type) 74 | } 75 | 76 | // IsObject validates if current Link is an GetID 77 | func (l Link) IsObject() bool { 78 | return l.Type == ObjectType || ObjectTypes.Contains(l.Type) 79 | } 80 | 81 | // IsCollection returns false for Link objects 82 | func (l Link) IsCollection() bool { 83 | return false 84 | } 85 | 86 | // GetID returns the ID corresponding to the Link object 87 | func (l Link) GetID() ID { 88 | return l.ID 89 | } 90 | 91 | // GetLink returns the IRI corresponding to the current Link 92 | func (l Link) GetLink() IRI { 93 | return IRI(l.ID) 94 | } 95 | 96 | // GetType returns the Type corresponding to the Mention object 97 | func (l Link) GetType() ActivityVocabularyType { 98 | return l.Type 99 | } 100 | 101 | // MarshalJSON encodes the receiver object to a JSON document. 102 | func (l Link) MarshalJSON() ([]byte, error) { 103 | b := make([]byte, 0) 104 | JSONWrite(&b, '{') 105 | 106 | if JSONWriteLinkValue(&b, l) { 107 | JSONWrite(&b, '}') 108 | return b, nil 109 | } 110 | return nil, nil 111 | } 112 | 113 | // UnmarshalJSON decodes an incoming JSON document into the receiver object. 114 | func (l *Link) UnmarshalJSON(data []byte) error { 115 | p := fastjson.Parser{} 116 | val, err := p.ParseBytes(data) 117 | if err != nil { 118 | return err 119 | } 120 | return JSONLoadLink(val, l) 121 | } 122 | 123 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 124 | func (l *Link) UnmarshalBinary(data []byte) error { 125 | return l.GobDecode(data) 126 | } 127 | 128 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 129 | func (l Link) MarshalBinary() ([]byte, error) { 130 | return l.GobEncode() 131 | } 132 | 133 | func (l Link) GobEncode() ([]byte, error) { 134 | mm := make(map[string][]byte) 135 | hasData, err := mapLinkProperties(mm, l) 136 | if err != nil { 137 | return nil, err 138 | } 139 | if !hasData { 140 | return []byte{}, nil 141 | } 142 | bb := bytes.Buffer{} 143 | g := gob.NewEncoder(&bb) 144 | if err := g.Encode(mm); err != nil { 145 | return nil, err 146 | } 147 | return bb.Bytes(), nil 148 | } 149 | 150 | func (l *Link) GobDecode(data []byte) error { 151 | if len(data) == 0 { 152 | return nil 153 | } 154 | mm, err := gobDecodeObjectAsMap(data) 155 | if err != nil { 156 | return err 157 | } 158 | return unmapLinkProperties(mm, l) 159 | } 160 | 161 | func (l Link) Format(s fmt.State, verb rune) { 162 | switch verb { 163 | case 's', 'v': 164 | _, _ = fmt.Fprintf(s, "%T[%s] { }", l, l.Type) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /link_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestLinkNew(t *testing.T) { 9 | testValue := ID("test") 10 | var testType ActivityVocabularyType 11 | 12 | l := LinkNew(testValue, testType) 13 | 14 | if l.ID != testValue { 15 | t.Errorf("APObject Id '%v' different than expected '%v'", l.ID, testValue) 16 | } 17 | if l.Type != LinkType { 18 | t.Errorf("APObject Type '%v' different than expected '%v'", l.Type, LinkType) 19 | } 20 | } 21 | 22 | func TestLink_IsLink(t *testing.T) { 23 | l := LinkNew("test", LinkType) 24 | if !l.IsLink() { 25 | t.Errorf("%#v should be a valid link", l.Type) 26 | } 27 | m := LinkNew("test", MentionType) 28 | if !m.IsLink() { 29 | t.Errorf("%#v should be a valid link", m.Type) 30 | } 31 | } 32 | 33 | func TestLink_IsObject(t *testing.T) { 34 | l := LinkNew("test", LinkType) 35 | if l.IsObject() { 36 | t.Errorf("%#v should not be a valid object", l.Type) 37 | } 38 | m := LinkNew("test", MentionType) 39 | if m.IsObject() { 40 | t.Errorf("%#v should not be a valid object", m.Type) 41 | } 42 | } 43 | 44 | func TestLink_GetID(t *testing.T) { 45 | t.Skipf("TODO") 46 | } 47 | 48 | func TestLink_GetLink(t *testing.T) { 49 | t.Skipf("TODO") 50 | } 51 | 52 | func TestLink_GetType(t *testing.T) { 53 | t.Skipf("TODO") 54 | } 55 | 56 | func TestLink_UnmarshalJSON(t *testing.T) { 57 | t.Skipf("TODO") 58 | } 59 | 60 | func TestMentionNew(t *testing.T) { 61 | t.Skipf("TODO") 62 | } 63 | 64 | func TestLink_IsCollection(t *testing.T) { 65 | t.Skipf("TODO") 66 | } 67 | 68 | func TestLink_GobEncode(t *testing.T) { 69 | type fields struct { 70 | ID ID 71 | Type ActivityVocabularyType 72 | Name NaturalLanguageValues 73 | Rel IRI 74 | MediaType MimeType 75 | Height uint 76 | Width uint 77 | Preview Item 78 | Href IRI 79 | HrefLang LangRef 80 | } 81 | tests := []struct { 82 | name string 83 | fields fields 84 | want []byte 85 | wantErr bool 86 | }{ 87 | { 88 | name: "empty", 89 | fields: fields{}, 90 | want: []byte{}, 91 | wantErr: false, 92 | }, 93 | } 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | l := Link{ 97 | ID: tt.fields.ID, 98 | Type: tt.fields.Type, 99 | Name: tt.fields.Name, 100 | Rel: tt.fields.Rel, 101 | MediaType: tt.fields.MediaType, 102 | Height: tt.fields.Height, 103 | Width: tt.fields.Width, 104 | Preview: tt.fields.Preview, 105 | Href: tt.fields.Href, 106 | HrefLang: tt.fields.HrefLang, 107 | } 108 | got, err := l.GobEncode() 109 | if (err != nil) != tt.wantErr { 110 | t.Errorf("GobEncode() error = %v, wantErr %v", err, tt.wantErr) 111 | return 112 | } 113 | if !reflect.DeepEqual(got, tt.want) { 114 | t.Errorf("GobEncode() got = %v, want %v", got, tt.want) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestLink_GobDecode(t *testing.T) { 121 | type fields struct { 122 | ID ID 123 | Type ActivityVocabularyType 124 | Name NaturalLanguageValues 125 | Rel IRI 126 | MediaType MimeType 127 | Height uint 128 | Width uint 129 | Preview Item 130 | Href IRI 131 | HrefLang LangRef 132 | } 133 | tests := []struct { 134 | name string 135 | fields fields 136 | data []byte 137 | wantErr bool 138 | }{ 139 | { 140 | name: "empty", 141 | fields: fields{}, 142 | data: []byte{}, 143 | wantErr: false, 144 | }, 145 | } 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | l := &Link{ 149 | ID: tt.fields.ID, 150 | Type: tt.fields.Type, 151 | Name: tt.fields.Name, 152 | Rel: tt.fields.Rel, 153 | MediaType: tt.fields.MediaType, 154 | Height: tt.fields.Height, 155 | Width: tt.fields.Width, 156 | Preview: tt.fields.Preview, 157 | Href: tt.fields.Href, 158 | HrefLang: tt.fields.HrefLang, 159 | } 160 | if err := l.GobDecode(tt.data); (err != nil) != tt.wantErr { 161 | t.Errorf("GobDecode() error = %v, wantErr %v", err, tt.wantErr) 162 | } 163 | }) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /object_id.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | // ID designates a unique global identifier. 4 | // All Objects in [ActivityStreams] should have unique global identifiers. 5 | // ActivityPub extends this requirement; all objects distributed by the ActivityPub protocol MUST 6 | // have unique global identifiers, unless they are intentionally transient 7 | // (short-lived activities that are not intended to be able to be looked up, 8 | // such as some kinds of chat messages or game notifications). 9 | // These identifiers must fall into one of the following groups: 10 | // 11 | // 1. Publicly de-referenceable URIs, such as HTTPS URIs, with their authority belonging 12 | // to that of their originating server. (Publicly facing content SHOULD use HTTPS URIs). 13 | // 2. An ID explicitly specified as the JSON null object, which implies an anonymous object 14 | // (a part of its parent context) 15 | type ID = IRI 16 | 17 | // IsValid returns if the receiver pointer is not nil and if dereferenced it has a positive length. 18 | func (i *ID) IsValid() bool { 19 | return i != nil && len(*i) > 0 20 | } 21 | -------------------------------------------------------------------------------- /object_id_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestID_UnmarshalJSON(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | data []byte 12 | want ID 13 | }{ 14 | { 15 | name: "nil", 16 | data: []byte(nil), 17 | want: "", 18 | }, 19 | { 20 | name: "empty", 21 | data: []byte(""), 22 | want: "", 23 | }, 24 | { 25 | name: "something", 26 | data: []byte("something"), 27 | want: "something", 28 | }, 29 | } 30 | 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | got := ID("") 34 | got.UnmarshalJSON(tt.data) 35 | if got != tt.want { 36 | t.Errorf("UnmarshalJSON() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestID_MarshalJSON(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | i ID 46 | want []byte 47 | wantErr error 48 | }{ 49 | { 50 | name: "nil", 51 | i: "", 52 | want: []byte(nil), 53 | }, 54 | { 55 | name: "empty", 56 | i: "", 57 | want: []byte(""), 58 | }, 59 | { 60 | name: "something", 61 | i: "something", 62 | want: []byte(`"something"`), 63 | }, 64 | } 65 | 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | got, err := tt.i.MarshalJSON() 69 | if tt.wantErr != nil { 70 | if err == nil { 71 | t.Errorf("MarshalJSON() returned no error but expected %v", tt.wantErr) 72 | } 73 | if tt.wantErr.Error() != err.Error() { 74 | t.Errorf("MarshalJSON() returned error %v but expected %v", err, tt.wantErr) 75 | } 76 | return 77 | } 78 | if !bytes.Equal(got, tt.want) { 79 | t.Errorf("MarshalJSON() = %s, want %s", got, tt.want) 80 | } 81 | }) 82 | } 83 | t.Skip("TODO") 84 | } 85 | 86 | func TestID_IsValid(t *testing.T) { 87 | tests := []struct { 88 | name string 89 | i ID 90 | want bool 91 | }{ 92 | { 93 | name: "empty", 94 | i: "", 95 | want: false, 96 | }, 97 | { 98 | name: "something", 99 | i: "something", 100 | want: true, 101 | }, 102 | } 103 | for _, tt := range tests { 104 | t.Run(tt.name, func(t *testing.T) { 105 | if got := tt.i.IsValid(); got != tt.want { 106 | t.Errorf("IsValid() = %v, want %v", got, tt.want) 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ordered_collection_page_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestOrderedCollectionPageNew(t *testing.T) { 9 | testValue := ID("test") 10 | 11 | c := OrderedCollectionNew(testValue) 12 | p := OrderedCollectionPageNew(c) 13 | if reflect.DeepEqual(p, c) { 14 | t.Errorf("Invalid ordered collection parent '%v'", p.PartOf) 15 | } 16 | if p.PartOf != c.GetLink() { 17 | t.Errorf("Invalid collection '%v'", p.PartOf) 18 | } 19 | } 20 | 21 | func TestOrderedCollectionPage_UnmarshalJSON(t *testing.T) { 22 | p := OrderedCollectionPage{} 23 | 24 | dataEmpty := []byte("{}") 25 | p.UnmarshalJSON(dataEmpty) 26 | if p.ID != "" { 27 | t.Errorf("Unmarshaled object should have empty ID, received %q", p.ID) 28 | } 29 | if p.Type != "" { 30 | t.Errorf("Unmarshaled object should have empty Type, received %q", p.Type) 31 | } 32 | if p.AttributedTo != nil { 33 | t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", p.AttributedTo) 34 | } 35 | if len(p.Name) != 0 { 36 | t.Errorf("Unmarshaled object should have empty Name, received %q", p.Name) 37 | } 38 | if len(p.Summary) != 0 { 39 | t.Errorf("Unmarshaled object should have empty Summary, received %q", p.Summary) 40 | } 41 | if len(p.Content) != 0 { 42 | t.Errorf("Unmarshaled object should have empty Content, received %q", p.Content) 43 | } 44 | if p.TotalItems != 0 { 45 | t.Errorf("Unmarshaled object should have empty TotalItems, received %d", p.TotalItems) 46 | } 47 | if len(p.OrderedItems) > 0 { 48 | t.Errorf("Unmarshaled object should have empty OrderedItems, received %v", p.OrderedItems) 49 | } 50 | if p.URL != nil { 51 | t.Errorf("Unmarshaled object should have empty URL, received %v", p.URL) 52 | } 53 | if !p.Published.IsZero() { 54 | t.Errorf("Unmarshaled object should have empty Published, received %q", p.Published) 55 | } 56 | if !p.StartTime.IsZero() { 57 | t.Errorf("Unmarshaled object should have empty StartTime, received %q", p.StartTime) 58 | } 59 | if !p.Updated.IsZero() { 60 | t.Errorf("Unmarshaled object should have empty Updated, received %q", p.Updated) 61 | } 62 | if p.PartOf != nil { 63 | t.Errorf("Unmarshaled object should have empty PartOf, received %q", p.PartOf) 64 | } 65 | if p.Current != nil { 66 | t.Errorf("Unmarshaled object should have empty Current, received %q", p.Current) 67 | } 68 | if p.First != nil { 69 | t.Errorf("Unmarshaled object should have empty First, received %q", p.First) 70 | } 71 | if p.Last != nil { 72 | t.Errorf("Unmarshaled object should have empty Last, received %q", p.Last) 73 | } 74 | if p.Next != nil { 75 | t.Errorf("Unmarshaled object should have empty Next, received %q", p.Next) 76 | } 77 | if p.Prev != nil { 78 | t.Errorf("Unmarshaled object should have empty Prev, received %q", p.Prev) 79 | } 80 | } 81 | 82 | func TestOrderedCollectionPage_Append(t *testing.T) { 83 | id := ID("test") 84 | 85 | val := Object{ID: ID("grrr")} 86 | 87 | c := OrderedCollectionNew(id) 88 | 89 | p := OrderedCollectionPageNew(c) 90 | p.Append(val) 91 | 92 | if p.PartOf != c.GetLink() { 93 | t.Errorf("OrderedCollection page should point to OrderedCollection %q", c.GetLink()) 94 | } 95 | if p.Count() != 1 { 96 | t.Errorf("OrderedCollection page of %q should have exactly one element", p.GetID()) 97 | } 98 | if !reflect.DeepEqual(p.OrderedItems[0], val) { 99 | t.Errorf("First item in Inbox is does not match %q", val.ID) 100 | } 101 | } 102 | 103 | func TestOrderedCollectionPage_Collection(t *testing.T) { 104 | id := ID("test") 105 | 106 | c := OrderedCollectionNew(id) 107 | p := OrderedCollectionPageNew(c) 108 | 109 | if !reflect.DeepEqual(p.Collection(), p.OrderedItems) { 110 | t.Errorf("Collection items should be equal %v %v", p.Collection(), p.OrderedItems) 111 | } 112 | } 113 | 114 | func TestOrderedCollectionPage_Contains(t *testing.T) { 115 | t.Skipf("TODO") 116 | } 117 | 118 | func TestToOrderedCollectionPage(t *testing.T) { 119 | err := func(it Item) error { return ErrorInvalidType[OrderedCollectionPage](it) } 120 | tests := map[string]struct { 121 | it Item 122 | want *OrderedCollectionPage 123 | wantErr error 124 | }{ 125 | "OrderedCollectionPage": { 126 | it: new(OrderedCollectionPage), 127 | want: new(OrderedCollectionPage), 128 | wantErr: nil, 129 | }, 130 | "OrderedCollection": { 131 | it: new(OrderedCollection), 132 | want: new(OrderedCollectionPage), 133 | wantErr: err(new(OrderedCollection)), 134 | }, 135 | "Collection": { 136 | it: new(Collection), 137 | want: new(OrderedCollectionPage), 138 | wantErr: err(new(Collection)), 139 | }, 140 | "CollectionPage": { 141 | it: new(CollectionPage), 142 | want: new(OrderedCollectionPage), 143 | wantErr: nil, 144 | }, 145 | } 146 | for name, tt := range tests { 147 | t.Run(name, func(t *testing.T) { 148 | got, err := ToOrderedCollectionPage(tt.it) 149 | if tt.wantErr != nil && err == nil { 150 | t.Errorf("ToOrderedCollectionPage() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr) 151 | return 152 | } 153 | if err != nil { 154 | if tt.wantErr == nil { 155 | t.Errorf("ToOrderedCollectionPage() returned unexpected error[%T]%s", err, err) 156 | return 157 | } 158 | if !reflect.DeepEqual(err, tt.wantErr) { 159 | t.Errorf("ToOrderedCollectionPage() received error %v, wanted error %v", err, tt.wantErr) 160 | return 161 | } 162 | return 163 | } 164 | if !reflect.DeepEqual(got, tt.want) { 165 | t.Errorf("ToOrderedCollectionPage() got = %v, want %v", got, tt.want) 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /ordered_collection_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestOrderedCollectionNew(t *testing.T) { 9 | testValue := ID("test") 10 | 11 | c := OrderedCollectionNew(testValue) 12 | 13 | if c.ID != testValue { 14 | t.Errorf("APObject Id '%v' different than expected '%v'", c.ID, testValue) 15 | } 16 | if c.Type != OrderedCollectionType { 17 | t.Errorf("APObject Type '%v' different than expected '%v'", c.Type, OrderedCollectionType) 18 | } 19 | } 20 | 21 | func Test_OrderedCollection_Append(t *testing.T) { 22 | id := ID("test") 23 | 24 | val := Object{ID: ID("grrr")} 25 | 26 | c := OrderedCollectionNew(id) 27 | c.Append(val) 28 | 29 | if c.Count() != 1 { 30 | t.Errorf("Inbox collection of %q should have one element", c.GetID()) 31 | } 32 | if !reflect.DeepEqual(c.OrderedItems[0], val) { 33 | t.Errorf("First item in Inbox is does not match %q", val.ID) 34 | } 35 | } 36 | 37 | func TestOrderedCollection_Append(t *testing.T) { 38 | id := ID("test") 39 | 40 | val := Object{ID: ID("grrr")} 41 | 42 | c := OrderedCollectionNew(id) 43 | 44 | p := OrderedCollectionPageNew(c) 45 | p.Append(val) 46 | 47 | if p.PartOf != c.GetLink() { 48 | t.Errorf("Ordereed collection page should point to ordered collection %q", c.GetLink()) 49 | } 50 | if p.Count() != 1 { 51 | t.Errorf("Ordered collection page of %q should have exactly one element", p.GetID()) 52 | } 53 | if !reflect.DeepEqual(p.OrderedItems[0], val) { 54 | t.Errorf("First item in Inbox is does not match %q", val.ID) 55 | } 56 | } 57 | 58 | func TestOrderedCollection_Collection(t *testing.T) { 59 | id := ID("test") 60 | 61 | o := OrderedCollectionNew(id) 62 | 63 | if !reflect.DeepEqual(o.Collection(), o.OrderedItems) { 64 | t.Errorf("Collection items should be equal %v %v", o.Collection(), o.OrderedItems) 65 | } 66 | } 67 | 68 | func TestOrderedCollection_GetID(t *testing.T) { 69 | id := ID("test") 70 | 71 | c := OrderedCollectionNew(id) 72 | 73 | if c.GetID() != id { 74 | t.Errorf("GetID should return %q, received %q", id, c.GetID()) 75 | } 76 | } 77 | 78 | func TestOrderedCollection_GetLink(t *testing.T) { 79 | id := ID("test") 80 | link := IRI(id) 81 | 82 | c := OrderedCollectionNew(id) 83 | 84 | if c.GetLink() != link { 85 | t.Errorf("GetLink should return %q, received %q", link, c.GetLink()) 86 | } 87 | } 88 | 89 | func TestOrderedCollection_GetType(t *testing.T) { 90 | id := ID("test") 91 | 92 | c := OrderedCollectionNew(id) 93 | 94 | if c.GetType() != OrderedCollectionType { 95 | t.Errorf("OrderedCollection Type should be %q, received %q", OrderedCollectionType, c.GetType()) 96 | } 97 | } 98 | 99 | func TestOrderedCollection_IsLink(t *testing.T) { 100 | id := ID("test") 101 | 102 | c := OrderedCollectionNew(id) 103 | 104 | if c.IsLink() != false { 105 | t.Errorf("OrderedCollection should not be a link, received %t", c.IsLink()) 106 | } 107 | } 108 | 109 | func TestOrderedCollection_IsObject(t *testing.T) { 110 | id := ID("test") 111 | 112 | c := OrderedCollectionNew(id) 113 | 114 | if c.IsObject() != true { 115 | t.Errorf("OrderedCollection should be an object, received %t", c.IsObject()) 116 | } 117 | } 118 | 119 | func TestOrderedCollection_UnmarshalJSON(t *testing.T) { 120 | c := OrderedCollection{} 121 | 122 | dataEmpty := []byte("{}") 123 | c.UnmarshalJSON(dataEmpty) 124 | if c.ID != "" { 125 | t.Errorf("Unmarshaled object should have empty ID, received %q", c.ID) 126 | } 127 | if c.Type != "" { 128 | t.Errorf("Unmarshaled object should have empty Type, received %q", c.Type) 129 | } 130 | if c.AttributedTo != nil { 131 | t.Errorf("Unmarshaled object should have empty AttributedTo, received %q", c.AttributedTo) 132 | } 133 | if len(c.Name) != 0 { 134 | t.Errorf("Unmarshaled object should have empty Name, received %q", c.Name) 135 | } 136 | if len(c.Summary) != 0 { 137 | t.Errorf("Unmarshaled object should have empty Summary, received %q", c.Summary) 138 | } 139 | if len(c.Content) != 0 { 140 | t.Errorf("Unmarshaled object should have empty Content, received %q", c.Content) 141 | } 142 | if c.TotalItems != 0 { 143 | t.Errorf("Unmarshaled object should have empty TotalItems, received %d", c.TotalItems) 144 | } 145 | if len(c.OrderedItems) > 0 { 146 | t.Errorf("Unmarshaled object should have empty OrderedItems, received %v", c.OrderedItems) 147 | } 148 | if c.URL != nil { 149 | t.Errorf("Unmarshaled object should have empty URL, received %v", c.URL) 150 | } 151 | if !c.Published.IsZero() { 152 | t.Errorf("Unmarshaled object should have empty Published, received %q", c.Published) 153 | } 154 | if !c.StartTime.IsZero() { 155 | t.Errorf("Unmarshaled object should have empty StartTime, received %q", c.StartTime) 156 | } 157 | if !c.Updated.IsZero() { 158 | t.Errorf("Unmarshaled object should have empty Updated, received %q", c.Updated) 159 | } 160 | } 161 | 162 | func TestOrderedCollection_Count(t *testing.T) { 163 | id := ID("test") 164 | 165 | c := OrderedCollectionNew(id) 166 | 167 | if c.TotalItems != 0 { 168 | t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems) 169 | } 170 | if len(c.OrderedItems) > 0 { 171 | t.Errorf("Empty object should have empty Items, received %v", c.OrderedItems) 172 | } 173 | if c.Count() != uint(len(c.OrderedItems)) { 174 | t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.OrderedItems)) 175 | } 176 | 177 | c.Append(IRI("test")) 178 | if c.TotalItems != 0 { 179 | t.Errorf("Empty object should have empty TotalItems, received %d", c.TotalItems) 180 | } 181 | if c.Count() != uint(len(c.OrderedItems)) { 182 | t.Errorf("%T.Count() returned %d, expected %d", c, c.Count(), len(c.OrderedItems)) 183 | } 184 | } 185 | 186 | func TestOrderedCollectionPage_Count(t *testing.T) { 187 | id := ID("test") 188 | 189 | c := OrderedCollectionNew(id) 190 | p := OrderedCollectionPageNew(c) 191 | 192 | if p.TotalItems != 0 { 193 | t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems) 194 | } 195 | if len(p.OrderedItems) > 0 { 196 | t.Errorf("Empty object should have empty Items, received %v", p.OrderedItems) 197 | } 198 | if p.Count() != uint(len(p.OrderedItems)) { 199 | t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.OrderedItems)) 200 | } 201 | 202 | p.Append(IRI("test")) 203 | if p.TotalItems != 0 { 204 | t.Errorf("Empty object should have empty TotalItems, received %d", p.TotalItems) 205 | } 206 | if p.Count() != uint(len(p.OrderedItems)) { 207 | t.Errorf("%T.Count() returned %d, expected %d", c, p.Count(), len(p.OrderedItems)) 208 | } 209 | } 210 | 211 | func TestOnOrderedCollection(t *testing.T) { 212 | t.Skipf("TODO") 213 | } 214 | 215 | func TestToOrderedCollection(t *testing.T) { 216 | //err := func(it Item) error { return ErrorInvalidType[OrderedCollection](it) } 217 | tests := map[string]struct { 218 | it Item 219 | want *OrderedCollection 220 | wantErr error 221 | }{ 222 | "OrderedCollection": { 223 | it: new(OrderedCollection), 224 | want: new(OrderedCollection), 225 | wantErr: nil, 226 | }, 227 | "OrderedCollectionPage": { 228 | it: new(OrderedCollectionPage), 229 | want: new(OrderedCollection), 230 | wantErr: nil, 231 | }, 232 | "Collection": { 233 | it: new(Collection), 234 | want: new(OrderedCollection), 235 | wantErr: nil, 236 | }, 237 | "CollectionPage": { 238 | it: new(CollectionPage), 239 | want: new(OrderedCollection), 240 | wantErr: nil, 241 | }, 242 | } 243 | for name, tt := range tests { 244 | t.Run(name, func(t *testing.T) { 245 | got, err := ToOrderedCollection(tt.it) 246 | if tt.wantErr != nil && err == nil { 247 | t.Errorf("ToOrderedCollection() no error returned, wanted error = [%T]%s", tt.wantErr, tt.wantErr) 248 | return 249 | } 250 | if err != nil { 251 | if tt.wantErr == nil { 252 | t.Errorf("ToOrderedCollection() returned unexpected error[%T]%s", err, err) 253 | return 254 | } 255 | if !reflect.DeepEqual(err, tt.wantErr) { 256 | t.Errorf("ToOrderedCollection() received error %v, wanted error %v", err, tt.wantErr) 257 | return 258 | } 259 | return 260 | } 261 | if !reflect.DeepEqual(got, tt.want) { 262 | t.Errorf("ToOrderedCollection() got = %v, want %v", got, tt.want) 263 | } 264 | }) 265 | } 266 | } 267 | 268 | func TestOrderedCollection_Contains(t *testing.T) { 269 | t.Skipf("TODO") 270 | } 271 | 272 | func TestOrderedCollection_MarshalJSON(t *testing.T) { 273 | t.Skipf("TODO") 274 | } 275 | 276 | func TestOrderedCollection_ItemMatches(t *testing.T) { 277 | t.Skipf("TODO") 278 | } 279 | 280 | func TestOrderedCollection_IsCollection(t *testing.T) { 281 | t.Skipf("TODO") 282 | } 283 | -------------------------------------------------------------------------------- /place_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestPlace_Recipients(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestToPlace(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestPlace_GetID(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestPlace_GetLink(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestPlace_GetType(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestPlace_IsCollection(t *testing.T) { 29 | t.Skipf("TODO") 30 | } 31 | 32 | func TestPlace_IsLink(t *testing.T) { 33 | t.Skipf("TODO") 34 | } 35 | 36 | func TestPlace_IsObject(t *testing.T) { 37 | t.Skipf("TODO") 38 | } 39 | 40 | func TestPlace_UnmarshalJSON(t *testing.T) { 41 | t.Skipf("TODO") 42 | } 43 | 44 | func TestPlace_Clean(t *testing.T) { 45 | t.Skipf("TODO") 46 | } 47 | 48 | func assertPlaceWithTesting(fn canErrorFunc, expected *Place) withPlaceFn { 49 | return func(p *Place) error { 50 | if !assertDeepEquals(fn, p, expected) { 51 | return fmt.Errorf("not equal") 52 | } 53 | return nil 54 | } 55 | } 56 | 57 | func TestOnPlace(t *testing.T) { 58 | testPlace := Place{ 59 | ID: "https://example.com", 60 | } 61 | type args struct { 62 | it Item 63 | fn func(canErrorFunc, *Place) withPlaceFn 64 | } 65 | tests := []struct { 66 | name string 67 | args args 68 | wantErr bool 69 | }{ 70 | { 71 | name: "single", 72 | args: args{testPlace, assertPlaceWithTesting}, 73 | wantErr: false, 74 | }, 75 | { 76 | name: "single fails", 77 | args: args{Place{ID: "https://not-equals"}, assertPlaceWithTesting}, 78 | wantErr: true, 79 | }, 80 | { 81 | name: "collectionOfPlaces", 82 | args: args{ItemCollection{testPlace, testPlace}, assertPlaceWithTesting}, 83 | wantErr: false, 84 | }, 85 | { 86 | name: "collectionOfPlaces fails", 87 | args: args{ItemCollection{testPlace, Place{ID: "https://not-equals"}}, assertPlaceWithTesting}, 88 | wantErr: true, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | var logFn canErrorFunc 93 | if tt.wantErr { 94 | logFn = t.Logf 95 | } else { 96 | logFn = t.Errorf 97 | } 98 | t.Run(tt.name, func(t *testing.T) { 99 | if err := OnPlace(tt.args.it, tt.args.fn(logFn, &testPlace)); (err != nil) != tt.wantErr { 100 | t.Errorf("OnPlace() error = %v, wantErr %v", err, tt.wantErr) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /profile.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/valyala/fastjson" 10 | ) 11 | 12 | // Profile a Profile is a content object that describes another Object, 13 | // typically used to describe CanReceiveActivities Type objects. 14 | // The describes property is used to reference the object being described by the profile. 15 | type Profile struct { 16 | // ID provides the globally unique identifier for anActivity Pub Object or Link. 17 | ID ID `jsonld:"id,omitempty"` 18 | // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. 19 | Type ActivityVocabularyType `jsonld:"type,omitempty"` 20 | // Name a simple, human-readable, plain-text name for the object. 21 | // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. 22 | Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` 23 | // Attachment identifies a resource attached or related to an object that potentially requires special handling. 24 | // The intent is to provide a model that is at least semantically similar to attachments in email. 25 | Attachment Item `jsonld:"attachment,omitempty"` 26 | // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. 27 | // For instance, an object might be attributed to the completion of another activity. 28 | AttributedTo Item `jsonld:"attributedTo,omitempty"` 29 | // Audience identifies one or more entities that represent the total population of entities 30 | // for which the object can considered to be relevant. 31 | Audience ItemCollection `jsonld:"audience,omitempty"` 32 | // Content or textual representation of the Activity Pub Object encoded as a JSON string. 33 | // By default, the value of content is HTML. 34 | // The mediaType property can be used in the object to indicate a different content type. 35 | // (The content MAY be expressed using multiple language-tagged values.) 36 | Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` 37 | // Context identifies the context within which the object exists or an activity was performed. 38 | // The notion of "context" used is intentionally vague. 39 | // The intended function is to serve as a means of grouping objects and activities that share a 40 | // common originating context or purpose. An example could be all activities relating to a common project or event. 41 | Context Item `jsonld:"context,omitempty"` 42 | // MediaType when used on an Object, identifies the MIME media type of the value of the content property. 43 | // If not specified, the content property is assumed to contain text/html content. 44 | MediaType MimeType `jsonld:"mediaType,omitempty"` 45 | // EndTime the date and time describing the actual or expected ending time of the object. 46 | // When used with an Activity object, for instance, the endTime property specifies the moment 47 | // the activity concluded or is expected to conclude. 48 | EndTime time.Time `jsonld:"endTime,omitempty"` 49 | // Generator identifies the entity (e.g. an application) that generated the object. 50 | Generator Item `jsonld:"generator,omitempty"` 51 | // Icon indicates an entity that describes an icon for this object. 52 | // The image should have an aspect ratio of one (horizontal) to one (vertical) 53 | // and should be suitable for presentation at a small size. 54 | Icon Item `jsonld:"icon,omitempty"` 55 | // Image indicates an entity that describes an image for this object. 56 | // Unlike the icon property, there are no aspect ratio or display size limitations assumed. 57 | Image Item `jsonld:"image,omitempty"` 58 | // InReplyTo indicates one or more entities for which this object is considered a response. 59 | InReplyTo Item `jsonld:"inReplyTo,omitempty"` 60 | // Location indicates one or more physical or logical locations associated with the object. 61 | Location Item `jsonld:"location,omitempty"` 62 | // Preview identifies an entity that provides a preview of this object. 63 | Preview Item `jsonld:"preview,omitempty"` 64 | // Published the date and time at which the object was published 65 | Published time.Time `jsonld:"published,omitempty"` 66 | // Replies identifies a Collection containing objects considered to be responses to this object. 67 | Replies Item `jsonld:"replies,omitempty"` 68 | // StartTime the date and time describing the actual or expected starting time of the object. 69 | // When used with an Activity object, for instance, the startTime property specifies 70 | // the moment the activity began or is scheduled to begin. 71 | StartTime time.Time `jsonld:"startTime,omitempty"` 72 | // Summary a natural language summarization of the object encoded as HTML. 73 | // *Multiple language tagged summaries may be provided.) 74 | Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` 75 | // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. 76 | // The key difference between attachment and tag is that the former implies association by inclusion, 77 | // while the latter implies associated by reference. 78 | Tag ItemCollection `jsonld:"tag,omitempty"` 79 | // Updated the date and time at which the object was updated 80 | Updated time.Time `jsonld:"updated,omitempty"` 81 | // URL identifies one or more links to representations of the object 82 | URL Item `jsonld:"url,omitempty"` 83 | // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object 84 | To ItemCollection `jsonld:"to,omitempty"` 85 | // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. 86 | Bto ItemCollection `jsonld:"bto,omitempty"` 87 | // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. 88 | CC ItemCollection `jsonld:"cc,omitempty"` 89 | // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. 90 | BCC ItemCollection `jsonld:"bcc,omitempty"` 91 | // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, 92 | // the duration property indicates the object's approximate duration. 93 | // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], 94 | // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). 95 | Duration time.Duration `jsonld:"duration,omitempty"` 96 | // This is a list of all Like activities with this object as the object property, added as a side effect. 97 | // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 98 | // of an authenticated user or as appropriate when no authentication is given. 99 | Likes Item `jsonld:"likes,omitempty"` 100 | // This is a list of all Announce activities with this object as the object property, added as a side effect. 101 | // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 102 | // of an authenticated user or as appropriate when no authentication is given. 103 | Shares Item `jsonld:"shares,omitempty"` 104 | // Source property is intended to convey some sort of source from which the content markup was derived, 105 | // as a form of provenance, or to support future editing by clients. 106 | // In general, clients do the conversion from source to content, not the other way around. 107 | Source Source `jsonld:"source,omitempty"` 108 | // Describes On a Profile object, the describes property identifies the object described by the Profile. 109 | Describes Item `jsonld:"describes,omitempty"` 110 | } 111 | 112 | // IsLink returns false for Profile objects 113 | func (p Profile) IsLink() bool { 114 | return false 115 | } 116 | 117 | // IsObject returns true for Profile objects 118 | func (p Profile) IsObject() bool { 119 | return true 120 | } 121 | 122 | // IsCollection returns false for Profile objects 123 | func (p Profile) IsCollection() bool { 124 | return false 125 | } 126 | 127 | // GetLink returns the IRI corresponding to the current Profile object 128 | func (p Profile) GetLink() IRI { 129 | return IRI(p.ID) 130 | } 131 | 132 | // GetType returns the type of the current Profile 133 | func (p Profile) GetType() ActivityVocabularyType { 134 | return p.Type 135 | } 136 | 137 | // GetID returns the ID corresponding to the current Profile 138 | func (p Profile) GetID() ID { 139 | return p.ID 140 | } 141 | 142 | // UnmarshalJSON decodes an incoming JSON document into the receiver object. 143 | func (p *Profile) UnmarshalJSON(data []byte) error { 144 | par := fastjson.Parser{} 145 | val, err := par.ParseBytes(data) 146 | if err != nil { 147 | return err 148 | } 149 | return JSONLoadProfile(val, p) 150 | } 151 | 152 | // MarshalJSON encodes the receiver object to a JSON document. 153 | func (p Profile) MarshalJSON() ([]byte, error) { 154 | b := make([]byte, 0) 155 | notEmpty := false 156 | JSONWrite(&b, '{') 157 | 158 | OnObject(p, func(o *Object) error { 159 | return nil 160 | }) 161 | 162 | if p.Describes != nil { 163 | notEmpty = JSONWriteItemProp(&b, "describes", p.Describes) || notEmpty 164 | } 165 | 166 | if notEmpty { 167 | JSONWrite(&b, '}') 168 | return b, nil 169 | } 170 | return nil, nil 171 | } 172 | 173 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 174 | func (p *Profile) UnmarshalBinary(data []byte) error { 175 | return p.GobDecode(data) 176 | } 177 | 178 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 179 | func (p Profile) MarshalBinary() ([]byte, error) { 180 | return p.GobEncode() 181 | } 182 | 183 | // GobEncode 184 | func (p Profile) GobEncode() ([]byte, error) { 185 | mm := make(map[string][]byte) 186 | hasData, err := mapProfileProperties(mm, p) 187 | if err != nil { 188 | return nil, err 189 | } 190 | if !hasData { 191 | return []byte{}, nil 192 | } 193 | bb := bytes.Buffer{} 194 | g := gob.NewEncoder(&bb) 195 | if err := g.Encode(mm); err != nil { 196 | return nil, err 197 | } 198 | return bb.Bytes(), nil 199 | } 200 | 201 | // GobDecode 202 | func (p *Profile) GobDecode(data []byte) error { 203 | if len(data) == 0 { 204 | return nil 205 | } 206 | mm, err := gobDecodeObjectAsMap(data) 207 | if err != nil { 208 | return err 209 | } 210 | return unmapProfileProperties(mm, p) 211 | } 212 | 213 | // Recipients performs recipient de-duplication on the Profile object's To, Bto, CC and BCC properties 214 | func (p *Profile) Recipients() ItemCollection { 215 | aud := p.Audience 216 | return ItemCollectionDeduplication(&p.To, &p.CC, &p.Bto, &p.BCC, &aud) 217 | } 218 | 219 | // Clean removes Bto and BCC properties 220 | func (p *Profile) Clean() { 221 | _ = OnObject(p, func(o *Object) error { 222 | o.Clean() 223 | return nil 224 | }) 225 | } 226 | 227 | func (p Profile) Format(s fmt.State, verb rune) { 228 | switch verb { 229 | case 's', 'v': 230 | _, _ = fmt.Fprintf(s, "%T[%s] { }", p, p.Type) 231 | } 232 | } 233 | 234 | // ToProfile tries to convert the "it" Item to a Profile object 235 | func ToProfile(it Item) (*Profile, error) { 236 | switch i := it.(type) { 237 | case *Profile: 238 | return i, nil 239 | case Profile: 240 | return &i, nil 241 | default: 242 | return reflectItemToType[Profile](it) 243 | } 244 | } 245 | 246 | type withProfileFn func(*Profile) error 247 | 248 | // OnProfile calls function fn on it Item if it can be asserted to type *Profile 249 | // 250 | // This function should be called if trying to access the Profile specific properties 251 | // like "describes". 252 | // For the other properties OnObject should be used instead. 253 | func OnProfile(it Item, fn withProfileFn) error { 254 | if it == nil { 255 | return nil 256 | } 257 | if IsItemCollection(it) { 258 | return OnItemCollection(it, func(col *ItemCollection) error { 259 | for _, it := range *col { 260 | if IsLink(it) { 261 | continue 262 | } 263 | if err := OnProfile(it, fn); err != nil { 264 | return err 265 | } 266 | } 267 | return nil 268 | }) 269 | } 270 | ob, err := ToProfile(it) 271 | if err != nil { 272 | return err 273 | } 274 | return fn(ob) 275 | } 276 | -------------------------------------------------------------------------------- /profile_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestProfile_Recipients(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestToProfile(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestProfile_GetID(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestProfile_GetLink(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestProfile_GetType(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestProfile_IsCollection(t *testing.T) { 29 | t.Skipf("TODO") 30 | } 31 | 32 | func TestProfile_IsLink(t *testing.T) { 33 | t.Skipf("TODO") 34 | } 35 | 36 | func TestProfile_IsObject(t *testing.T) { 37 | t.Skipf("TODO") 38 | } 39 | 40 | func TestProfile_UnmarshalJSON(t *testing.T) { 41 | t.Skipf("TODO") 42 | } 43 | 44 | func TestProfile_Clean(t *testing.T) { 45 | t.Skipf("TODO") 46 | } 47 | 48 | func assertProfileWithTesting(fn canErrorFunc, expected *Profile) withProfileFn { 49 | return func(p *Profile) error { 50 | if !assertDeepEquals(fn, p, expected) { 51 | return fmt.Errorf("not equal") 52 | } 53 | return nil 54 | } 55 | } 56 | 57 | func TestOnProfile(t *testing.T) { 58 | testProfile := Profile{ 59 | ID: "https://example.com", 60 | } 61 | type args struct { 62 | it Item 63 | fn func(canErrorFunc, *Profile) withProfileFn 64 | } 65 | tests := []struct { 66 | name string 67 | args args 68 | wantErr bool 69 | }{ 70 | { 71 | name: "single", 72 | args: args{testProfile, assertProfileWithTesting}, 73 | wantErr: false, 74 | }, 75 | { 76 | name: "single fails", 77 | args: args{&Profile{ID: "https://not-equal"}, assertProfileWithTesting}, 78 | wantErr: true, 79 | }, 80 | { 81 | name: "collection of profiles", 82 | args: args{ItemCollection{testProfile, testProfile}, assertProfileWithTesting}, 83 | wantErr: false, 84 | }, 85 | { 86 | name: "collection of profiles fails", 87 | args: args{ItemCollection{testProfile, &Profile{ID: "not-equal"}}, assertProfileWithTesting}, 88 | wantErr: true, 89 | }, 90 | } 91 | for _, tt := range tests { 92 | t.Run(tt.name, func(t *testing.T) { 93 | var logFn canErrorFunc 94 | if tt.wantErr { 95 | logFn = t.Logf 96 | } else { 97 | logFn = t.Errorf 98 | } 99 | if err := OnProfile(tt.args.it, tt.args.fn(logFn, &testProfile)); (err != nil) != tt.wantErr { 100 | t.Errorf("OnProfile() error = %v, wantErr %v", err, tt.wantErr) 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /question_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import "testing" 4 | 5 | func TestQuestionNew(t *testing.T) { 6 | testValue := ID("test") 7 | 8 | a := QuestionNew(testValue) 9 | 10 | if a.ID != testValue { 11 | t.Errorf("Activity Id '%v' different than expected '%v'", a.ID, testValue) 12 | } 13 | if a.Type != QuestionType { 14 | t.Errorf("Activity Type '%v' different than expected '%v'", a.Type, QuestionType) 15 | } 16 | } 17 | 18 | func TestQuestion_GetID(t *testing.T) { 19 | a := QuestionNew("test") 20 | 21 | if a.GetID() != "test" { 22 | t.Errorf("%T should return an empty %T object. Received %#v", a, a.GetID(), a.GetID()) 23 | } 24 | } 25 | 26 | func TestQuestion_IsObject(t *testing.T) { 27 | a := QuestionNew("test") 28 | 29 | if !a.IsObject() { 30 | t.Errorf("%T should respond true to IsObject", a) 31 | } 32 | } 33 | 34 | func TestQuestion_IsLink(t *testing.T) { 35 | a := QuestionNew("test") 36 | 37 | if a.IsLink() { 38 | t.Errorf("%T should respond false to IsLink", a) 39 | } 40 | } 41 | 42 | func TestQuestion_GetLink(t *testing.T) { 43 | a := QuestionNew("test") 44 | 45 | if a.GetLink() != "test" { 46 | t.Errorf("GetLink should return \"test\" for %T, received %q", a, a.GetLink()) 47 | } 48 | } 49 | 50 | func TestQuestion_GetType(t *testing.T) { 51 | a := QuestionNew("test") 52 | 53 | if a.GetType() != QuestionType { 54 | t.Errorf("GetType should return %q for %T, received %q", QuestionType, a, a.GetType()) 55 | } 56 | } 57 | 58 | func TestToQuestion(t *testing.T) { 59 | var it Item 60 | act := QuestionNew("test") 61 | it = act 62 | 63 | a, err := ToQuestion(it) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | if a != act { 68 | t.Errorf("Invalid activity returned by ToActivity #%v", a) 69 | } 70 | 71 | ob := ObjectNew(ArticleType) 72 | it = ob 73 | 74 | o, err := ToQuestion(it) 75 | if err == nil { 76 | t.Errorf("Error returned when calling ToActivity with object should not be nil") 77 | } 78 | if o != nil { 79 | t.Errorf("Invalid return by ToActivity #%v, should have been nil", o) 80 | } 81 | } 82 | 83 | func TestQuestion_IsCollection(t *testing.T) { 84 | t.Skipf("TODO") 85 | } 86 | 87 | func TestQuestion_UnmarshalJSON(t *testing.T) { 88 | t.Skipf("TODO") 89 | } 90 | -------------------------------------------------------------------------------- /relationship.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/valyala/fastjson" 11 | ) 12 | 13 | // Relationship describes a relationship between two individuals. 14 | // The subject and object properties are used to identify the connected individuals. 15 | // See 5.2 Representing Relationships Between Entities for additional information. 16 | // 17 | // 5.2: The relationship property specifies the kind of relationship that exists between the two individuals identified 18 | // by the subject and object properties. Used together, these three properties form what is commonly known 19 | // as a "reified statement" where subject identifies the subject, relationship identifies the predicate, 20 | // and object identifies the object. 21 | type Relationship struct { 22 | // ID provides the globally unique identifier for anActivity Pub Object or Link. 23 | ID ID `jsonld:"id,omitempty"` 24 | // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. 25 | Type ActivityVocabularyType `jsonld:"type,omitempty"` 26 | // Name a simple, human-readable, plain-text name for the object. 27 | // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. 28 | Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` 29 | // Attachment identifies a resource attached or related to an object that potentially requires special handling. 30 | // The intent is to provide a model that is at least semantically similar to attachments in email. 31 | Attachment Item `jsonld:"attachment,omitempty"` 32 | // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. 33 | // For instance, an object might be attributed to the completion of another activity. 34 | AttributedTo Item `jsonld:"attributedTo,omitempty"` 35 | // Audience identifies one or more entities that represent the total population of entities 36 | // for which the object can considered to be relevant. 37 | Audience ItemCollection `jsonld:"audience,omitempty"` 38 | // Content or textual representation of the Activity Pub Object encoded as a JSON string. 39 | // By default, the value of content is HTML. 40 | // The mediaType property can be used in the object to indicate a different content type. 41 | // (The content MAY be expressed using multiple language-tagged values.) 42 | Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` 43 | // Context identifies the context within which the object exists or an activity was performed. 44 | // The notion of "context" used is intentionally vague. 45 | // The intended function is to serve as a means of grouping objects and activities that share a 46 | // common originating context or purpose. An example could be all activities relating to a common project or event. 47 | Context Item `jsonld:"context,omitempty"` 48 | // MediaType when used on an Object, identifies the MIME media type of the value of the content property. 49 | // If not specified, the content property is assumed to contain text/html content. 50 | MediaType MimeType `jsonld:"mediaType,omitempty"` 51 | // EndTime the date and time describing the actual or expected ending time of the object. 52 | // When used with an Activity object, for instance, the endTime property specifies the moment 53 | // the activity concluded or is expected to conclude. 54 | EndTime time.Time `jsonld:"endTime,omitempty"` 55 | // Generator identifies the entity (e.g. an application) that generated the object. 56 | Generator Item `jsonld:"generator,omitempty"` 57 | // Icon indicates an entity that describes an icon for this object. 58 | // The image should have an aspect ratio of one (horizontal) to one (vertical) 59 | // and should be suitable for presentation at a small size. 60 | Icon Item `jsonld:"icon,omitempty"` 61 | // Image indicates an entity that describes an image for this object. 62 | // Unlike the icon property, there are no aspect ratio or display size limitations assumed. 63 | Image Item `jsonld:"image,omitempty"` 64 | // InReplyTo indicates one or more entities for which this object is considered a response. 65 | InReplyTo Item `jsonld:"inReplyTo,omitempty"` 66 | // Location indicates one or more physical or logical locations associated with the object. 67 | Location Item `jsonld:"location,omitempty"` 68 | // Preview identifies an entity that provides a preview of this object. 69 | Preview Item `jsonld:"preview,omitempty"` 70 | // Published the date and time at which the object was published 71 | Published time.Time `jsonld:"published,omitempty"` 72 | // Replies identifies a Collection containing objects considered to be responses to this object. 73 | Replies Item `jsonld:"replies,omitempty"` 74 | // StartTime the date and time describing the actual or expected starting time of the object. 75 | // When used with an Activity object, for instance, the startTime property specifies 76 | // the moment the activity began or is scheduled to begin. 77 | StartTime time.Time `jsonld:"startTime,omitempty"` 78 | // Summary a natural language summarization of the object encoded as HTML. 79 | // *Multiple language tagged summaries may be provided.) 80 | Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` 81 | // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. 82 | // The key difference between attachment and tag is that the former implies association by inclusion, 83 | // while the latter implies associated by reference. 84 | Tag ItemCollection `jsonld:"tag,omitempty"` 85 | // Updated the date and time at which the object was updated 86 | Updated time.Time `jsonld:"updated,omitempty"` 87 | // URL identifies one or more links to representations of the object 88 | URL Item `jsonld:"url,omitempty"` 89 | // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object 90 | To ItemCollection `jsonld:"to,omitempty"` 91 | // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. 92 | Bto ItemCollection `jsonld:"bto,omitempty"` 93 | // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. 94 | CC ItemCollection `jsonld:"cc,omitempty"` 95 | // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. 96 | BCC ItemCollection `jsonld:"bcc,omitempty"` 97 | // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, 98 | // the duration property indicates the object's approximate duration. 99 | // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], 100 | // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). 101 | Duration time.Duration `jsonld:"duration,omitempty"` 102 | // This is a list of all Like activities with this object as the object property, added as a side effect. 103 | // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 104 | // of an authenticated user or as appropriate when no authentication is given. 105 | Likes Item `jsonld:"likes,omitempty"` 106 | // This is a list of all Announce activities with this object as the object property, added as a side effect. 107 | // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 108 | // of an authenticated user or as appropriate when no authentication is given. 109 | Shares Item `jsonld:"shares,omitempty"` 110 | // Source property is intended to convey some sort of source from which the content markup was derived, 111 | // as a form of provenance, or to support future editing by clients. 112 | // In general, clients do the conversion from source to content, not the other way around. 113 | Source Source `jsonld:"source,omitempty"` 114 | // Subject property identifies one of the connected individuals. 115 | // For instance, for a Relationship object describing "John is related to Sally", subject would refer to John. 116 | Subject Item `jsonld:"subject,omitempty"` 117 | // Object property identifies one of the connected individuals. 118 | // For instance, for a Relationship object describing "John is related to Sally", object would refer to Sally. 119 | Object Item `jsonld:"object,omitempty"` 120 | // Relationship property identifies the kind of relationship that exists between subject and object. 121 | Relationship Item `jsonld:"relationship,omitempty"` 122 | } 123 | 124 | // IsLink returns false for Relationship objects 125 | func (r Relationship) IsLink() bool { 126 | return false 127 | } 128 | 129 | // IsObject returns true for Relationship objects 130 | func (r Relationship) IsObject() bool { 131 | return true 132 | } 133 | 134 | // IsCollection returns false for Relationship objects 135 | func (r Relationship) IsCollection() bool { 136 | return false 137 | } 138 | 139 | // GetLink returns the IRI corresponding to the current Relationship object 140 | func (r Relationship) GetLink() IRI { 141 | return IRI(r.ID) 142 | } 143 | 144 | // GetType returns the type of the current Relationship 145 | func (r Relationship) GetType() ActivityVocabularyType { 146 | return r.Type 147 | } 148 | 149 | // GetID returns the ID corresponding to the current Relationship 150 | func (r Relationship) GetID() ID { 151 | return r.ID 152 | } 153 | 154 | // UnmarshalJSON decodes an incoming JSON document into the receiver object. 155 | func (r *Relationship) UnmarshalJSON(data []byte) error { 156 | par := fastjson.Parser{} 157 | val, err := par.ParseBytes(data) 158 | if err != nil { 159 | return err 160 | } 161 | return JSONLoadRelationship(val, r) 162 | } 163 | 164 | // MarshalJSON encodes the receiver object to a JSON document. 165 | func (r Relationship) MarshalJSON() ([]byte, error) { 166 | b := make([]byte, 0) 167 | notEmpty := false 168 | JSONWrite(&b, '{') 169 | 170 | OnObject(r, func(o *Object) error { 171 | notEmpty = JSONWriteObjectValue(&b, *o) 172 | return nil 173 | }) 174 | 175 | if r.Subject != nil { 176 | notEmpty = JSONWriteItemProp(&b, "subject", r.Subject) || notEmpty 177 | } 178 | if r.Object != nil { 179 | notEmpty = JSONWriteItemProp(&b, "object", r.Object) || notEmpty 180 | } 181 | if r.Relationship != nil { 182 | notEmpty = JSONWriteItemProp(&b, "relationship", r.Relationship) || notEmpty 183 | } 184 | 185 | if notEmpty { 186 | JSONWrite(&b, '}') 187 | return b, nil 188 | } 189 | return nil, nil 190 | } 191 | 192 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 193 | func (r *Relationship) UnmarshalBinary(data []byte) error { 194 | return r.GobDecode(data) 195 | } 196 | 197 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 198 | func (r Relationship) MarshalBinary() ([]byte, error) { 199 | return r.GobEncode() 200 | } 201 | 202 | // GobEncode 203 | func (r Relationship) GobEncode() ([]byte, error) { 204 | mm := make(map[string][]byte) 205 | hasData, err := mapRelationshipProperties(mm, r) 206 | if err != nil { 207 | return nil, err 208 | } 209 | if !hasData { 210 | return []byte{}, nil 211 | } 212 | bb := bytes.Buffer{} 213 | g := gob.NewEncoder(&bb) 214 | if err := g.Encode(mm); err != nil { 215 | return nil, err 216 | } 217 | return bb.Bytes(), nil 218 | } 219 | 220 | // GobDecode 221 | func (r *Relationship) GobDecode(data []byte) error { 222 | if len(data) == 0 { 223 | return nil 224 | } 225 | mm, err := gobDecodeObjectAsMap(data) 226 | if err != nil { 227 | return err 228 | } 229 | return unmapRelationshipProperties(mm, r) 230 | } 231 | 232 | // Recipients performs recipient de-duplication on the Relationship object's To, Bto, CC and BCC properties 233 | func (r *Relationship) Recipients() ItemCollection { 234 | aud := r.Audience 235 | return ItemCollectionDeduplication(&r.To, &r.CC, &r.Bto, &r.BCC, &aud) 236 | } 237 | 238 | // Clean removes Bto and BCC properties 239 | func (r *Relationship) Clean() { 240 | _ = OnObject(r, func(o *Object) error { 241 | o.Clean() 242 | return nil 243 | }) 244 | } 245 | 246 | func (r Relationship) Format(s fmt.State, verb rune) { 247 | switch verb { 248 | case 's', 'v': 249 | _, _ = fmt.Fprintf(s, "%T[%s] { }", r, r.Type) 250 | } 251 | } 252 | 253 | // ToRelationship tries to convert the "it" Item to a Relationship object. 254 | func ToRelationship(it Item) (*Relationship, error) { 255 | switch i := it.(type) { 256 | case *Relationship: 257 | return i, nil 258 | case Relationship: 259 | return &i, nil 260 | case *Object: 261 | return (*Relationship)(unsafe.Pointer(i)), nil 262 | case Object: 263 | return (*Relationship)(unsafe.Pointer(&i)), nil 264 | default: 265 | return reflectItemToType[Relationship](it) 266 | } 267 | } 268 | 269 | type withRelationshipFn func(*Relationship) error 270 | 271 | // OnRelationship calls function fn on it Item if it can be asserted to type *Relationship 272 | // 273 | // This function should be called if trying to access the Relationship specific properties 274 | // like "subject", "object", or "relationship". 275 | // For the other properties OnObject should be used instead. 276 | func OnRelationship(it Item, fn withRelationshipFn) error { 277 | if it == nil { 278 | return nil 279 | } 280 | ob, err := ToRelationship(it) 281 | if err != nil { 282 | return err 283 | } 284 | return fn(ob) 285 | } 286 | -------------------------------------------------------------------------------- /relationship_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import "testing" 4 | 5 | func TestRelationship_GetID(t *testing.T) { 6 | t.Skipf("TODO") 7 | } 8 | 9 | func TestRelationship_GetLink(t *testing.T) { 10 | t.Skipf("TODO") 11 | } 12 | 13 | func TestRelationship_GetType(t *testing.T) { 14 | t.Skipf("TODO") 15 | } 16 | 17 | func TestRelationship_IsCollection(t *testing.T) { 18 | t.Skipf("TODO") 19 | } 20 | 21 | func TestRelationship_IsLink(t *testing.T) { 22 | t.Skipf("TODO") 23 | } 24 | 25 | func TestRelationship_IsObject(t *testing.T) { 26 | t.Skipf("TODO") 27 | } 28 | 29 | func TestRelationship_UnmarshalJSON(t *testing.T) { 30 | t.Skipf("TODO") 31 | } 32 | 33 | func TestRelationship_Clean(t *testing.T) { 34 | t.Skipf("TODO") 35 | } 36 | -------------------------------------------------------------------------------- /tests/integration_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | j "github.com/go-ap/jsonld" 8 | 9 | pub "github.com/go-ap/activitypub" 10 | ) 11 | 12 | func TestAcceptSerialization(t *testing.T) { 13 | obj := pub.AcceptNew("https://localhost/myactivity", nil) 14 | obj.Name = make(pub.NaturalLanguageValues, 1) 15 | obj.Name.Set("en", pub.Content("test")) 16 | obj.Name.Set("fr", pub.Content("teste")) 17 | 18 | uri := "https://www.w3.org/ns/activitystreams" 19 | p := j.WithContext(j.IRI(uri)) 20 | 21 | data, err := p.Marshal(obj) 22 | if err != nil { 23 | t.Errorf("Error: %v", err) 24 | } 25 | 26 | if !strings.Contains(string(data), uri) { 27 | t.Errorf("Could not find context url %#v in output %s", p.Context, data) 28 | } 29 | if !strings.Contains(string(data), string(obj.ID)) { 30 | t.Errorf("Could not find id %#v in output %s", string(obj.ID), data) 31 | } 32 | if !strings.Contains(string(data), string(obj.Name.Get("en"))) { 33 | t.Errorf("Could not find name %#v in output %s", string(obj.Name.Get("en")), data) 34 | } 35 | if !strings.Contains(string(data), string(obj.Name.Get("fr"))) { 36 | t.Errorf("Could not find name %#v in output %s", string(obj.Name.Get("fr")), data) 37 | } 38 | if !strings.Contains(string(data), string(obj.Type)) { 39 | t.Errorf("Could not find activity type %#v in output %s", obj.Type, data) 40 | } 41 | } 42 | 43 | func TestCreateActivityHTTPSerialization(t *testing.T) { 44 | id := pub.ID("test_object") 45 | obj := pub.AcceptNew(id, nil) 46 | obj.Name.Set("en", pub.Content("Accept New")) 47 | 48 | uri := string(pub.ActivityBaseURI) 49 | 50 | data, err := j.WithContext(j.IRI(uri)).Marshal(obj) 51 | if err != nil { 52 | t.Errorf("Error: %v", err) 53 | } 54 | 55 | if !strings.Contains(string(data), uri) { 56 | t.Errorf("Could not find context url %#v in output %s", j.GetContext(), data) 57 | } 58 | if !strings.Contains(string(data), string(obj.ID)) { 59 | t.Errorf("Could not find id %#v in output %s", string(obj.ID), data) 60 | } 61 | if !strings.Contains(string(data), obj.Name.Get("en").String()) { 62 | t.Errorf("Could not find name %s in output %s", obj.Name.Get("en"), data) 63 | } 64 | if !strings.Contains(string(data), string(obj.Type)) { 65 | t.Errorf("Could not find activity type %#v in output %s", obj.Type, data) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/mocks/activity_create_multiple_objects.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Create", 3 | "actor": "https://littr.git/api/accounts/anonymous", 4 | "object": [ 5 | { 6 | "type": "Note", 7 | "attributedTo": "https://littr.git/api/accounts/anonymous", 8 | "inReplyTo": "https://littr.git/api/accounts/system/outbox/7ca154ff", 9 | "content": "
Hello world
", 10 | "to": "https://www.w3.org/ns/activitystreams#Public" 11 | }, 12 | { 13 | "type": "Article", 14 | "id": "http://www.test.example/article/1", 15 | "name": "This someday will grow up to be an article", 16 | "inReplyTo": [ 17 | "http://www.test.example/object/1", 18 | "http://www.test.example/object/778" 19 | ] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /tests/mocks/activity_create_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Create", 3 | "actor": "https://littr.git/api/accounts/anonymous", 4 | 5 | "object": { 6 | "type": "Note", 7 | "attributedTo": "https://littr.git/api/accounts/anonymous", 8 | "inReplyTo": "https://littr.git/api/accounts/system/outbox/7ca154ff", 9 | "content": "Hello world
", 10 | "to": "https://www.w3.org/ns/activitystreams#Public" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/mocks/activity_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Activity", 4 | "summary": "Sally did something to a note", 5 | "actor": { 6 | "type": "Person", 7 | "name": "Sally" 8 | }, 9 | "object": { 10 | "type": "Note", 11 | "name": "A Note" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/mocks/article_with_multiple_inreplyto.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Article", 4 | "id": "http://www.test.example/article/1", 5 | "name": "This someday will grow up to be an article", 6 | "inReplyTo": [ 7 | "http://www.test.example/object/1", 8 | "http://www.test.example/object/778" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/mocks/empty.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /tests/mocks/like_activity_with_iri_actor.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Like", 3 | "actor": "https://littr.git/api/accounts/24d4b96f", 4 | "object": { 5 | "id": "https://littr.git/api/accounts/ana/liked/7ca154ff", 6 | "type": "Article" 7 | }, 8 | "published": "2018-09-06T15:15:09Z", 9 | "to": null, 10 | "cc": null 11 | } 12 | -------------------------------------------------------------------------------- /tests/mocks/link_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Link", 4 | "href": "http://example.org/abc", 5 | "hrefLang": "en", 6 | "mediaType": "text/html", 7 | "name": "An example link" 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/natural_language_values.json: -------------------------------------------------------------------------------- 1 | { 2 | "-": "\n\t\t\n", 3 | "en": "Ana got apples ⓐ", 4 | "fr": "Aná a des pommes ⒜", 5 | "ro": "Ana are mere" 6 | } 7 | -------------------------------------------------------------------------------- /tests/mocks/object_no_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "id": "http://www.test.example/object/1", 4 | "name": "A Simple, non-specific object without a type" 5 | } 6 | -------------------------------------------------------------------------------- /tests/mocks/object_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Object", 4 | "id": "http://www.test.example/object/1", 5 | "name": "A Simple, non-specific object" 6 | } -------------------------------------------------------------------------------- /tests/mocks/object_with_audience.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Object", 4 | "id": "http://www.test.example/object/1", 5 | "to": [ 6 | "https://www.w3.org/ns/activitystreams#Public" 7 | ], 8 | "bto": [ 9 | "http://example.com/sharedInbox" 10 | ], 11 | "cc": [ 12 | "https://example.com/actors/ana", 13 | "https://example.com/actors/bob" 14 | ], 15 | "bcc": [ 16 | "https://darkside.cookie/actors/darthvader" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/mocks/object_with_replies.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Object", 4 | "id": "http://www.test.example/object/1", 5 | "replies": { 6 | "id": "http://www.test.example/object/1/replies", 7 | "type": "Collection", 8 | "totalItems": 1, 9 | "items": [ 10 | { 11 | "id": "http://www.test.example/object/1/replies/2", 12 | "type": "Article", 13 | "name": "Example title" 14 | } 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/mocks/object_with_tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Object", 4 | "id": "http://www.test.example/object/1", 5 | "name": "A Simple, non-specific object", 6 | "tag": [ 7 | { 8 | "name": "#my_tag", 9 | "id": "http://example.com/tag/my_tag", 10 | "type": "Mention" 11 | }, 12 | { 13 | "name": "@ana", 14 | "type": "Mention", 15 | "id": "http://example.com/users/ana" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tests/mocks/object_with_url.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context":"https://www.w3.org/ns/activitystreams", 3 | "url":"http://littr.git/api/accounts/system" 4 | } 5 | -------------------------------------------------------------------------------- /tests/mocks/object_with_url_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context":"https://www.w3.org/ns/activitystreams", 3 | "url": [ 4 | "http://littr.git/api/accounts/system", 5 | "http://littr.git/~system" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tests/mocks/ordered_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "id": "http://example.com/outbox", 4 | "type": "OrderedCollection", 5 | "url": "http://example.com/outbox", 6 | "totalItems": 1, 7 | "orderedItems": [ 8 | { 9 | "id": "http://example.com/outbox/53c6fb47", 10 | "type": "Article", 11 | "name": "Example title", 12 | "content": "Example content!", 13 | "url": "http://example.com/53c6fb47", 14 | "mediaType": "text/markdown", 15 | "published": "2018-07-05T16:46:44.00000Z", 16 | "generator": "http://example.com", 17 | "attributedTo": "http://example.com/accounts/alice" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/mocks/ordered_collection_page.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "id": "http://example.com/outbox?page=2", 4 | "type": "OrderedCollectionPage", 5 | "url": "http://example.com/outbox?page=2", 6 | "totalItems": 1, 7 | "partOf": "http://example.com/outbox", 8 | "current": "http://example.com/outbox?page=2", 9 | "next": "http://example.com/outbox?page=3", 10 | "prev" : "http://example.com/outbox?page=1", 11 | "startIndex": 100, 12 | "orderedItems": [ 13 | { 14 | "id": "http://example.com/outbox/53c6fb47", 15 | "type": "Article", 16 | "name": "Example title", 17 | "content": "Example content!", 18 | "url": "http://example.com/53c6fb47", 19 | "mediaType": "text/markdown", 20 | "published": "2018-07-05T16:46:44.00000Z", 21 | "generator": "http://example.com", 22 | "attributedTo": "http://example.com/accounts/alice" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/mocks/person_with_outbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "id": "http://example.com/accounts/ana", 4 | "type": "Person", 5 | "name": "ana", 6 | "url": "http://example.com/accounts/ana", 7 | "outbox": { 8 | "id": "http://example.com/accounts/ana/outbox", 9 | "type": "OrderedCollection", 10 | "url": "http://example.com/outbox" 11 | }, 12 | "preferredUsername": "Ana" 13 | } 14 | -------------------------------------------------------------------------------- /tests/mocks/travel_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": "https://www.w3.org/ns/activitystreams", 3 | "type": "Travel", 4 | "summary": "Sally went to work", 5 | "actor": { 6 | "type": "Person", 7 | "name": "Sally" 8 | }, 9 | "target": { 10 | "type": "Place", 11 | "name": "Work" 12 | } 13 | } -------------------------------------------------------------------------------- /tests/server_common_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | // Common server tests... 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | // Server: Fetching the inbox 10 | // Try retrieving the actor's inbox of an actor. 11 | // Server responds to GET request at inbox URL 12 | func TestInboxGETRequest(t *testing.T) { 13 | desc := ` 14 | Server: Fetching the inbox 15 | Try retrieving the actor's inbox of an actor. 16 | 17 | Server responds to GET request at inbox URL 18 | ` 19 | t.Skip(desc) 20 | } 21 | 22 | // Server: Fetching the inbox 23 | // Try retrieving the actor's inbox of an actor. 24 | // inbox is an OrderedCollection 25 | func TestInboxIsOrderedCollection(t *testing.T) { 26 | desc := ` 27 | Server: Fetching the inbox 28 | Try retrieving the actor's inbox of an actor. 29 | 30 | inbox is an OrderedCollection 31 | ` 32 | t.Skip(desc) 33 | } 34 | 35 | // Server: Fetching the inbox 36 | // Try retrieving the actor's inbox of an actor. 37 | // Server filters inbox content according to the requester's permission 38 | func TestInboxFilteringBasedOnPermissions(t *testing.T) { 39 | desc := ` 40 | Server: Fetching the inbox 41 | Try retrieving the actor's inbox of an actor. 42 | 43 | Server filters inbox content according to the requester's permission 44 | ` 45 | t.Skip(desc) 46 | } 47 | 48 | /* 49 | func Test_(t *testing.T) { 50 | desc := ` 51 | ` 52 | t.Skip(desc) 53 | } 54 | */ 55 | -------------------------------------------------------------------------------- /tombstone.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/valyala/fastjson" 11 | ) 12 | 13 | // Tombstone a Tombstone represents a content object that has been deleted. 14 | // It can be used in Collections to signify that there used to be an object at this position, 15 | // but it has been deleted. 16 | type Tombstone struct { 17 | // ID provides the globally unique identifier for anActivity Pub Object or Link. 18 | ID ID `jsonld:"id,omitempty"` 19 | // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. 20 | Type ActivityVocabularyType `jsonld:"type,omitempty"` 21 | // Name a simple, human-readable, plain-text name for the object. 22 | // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. 23 | Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` 24 | // Attachment identifies a resource attached or related to an object that potentially requires special handling. 25 | // The intent is to provide a model that is at least semantically similar to attachments in email. 26 | Attachment Item `jsonld:"attachment,omitempty"` 27 | // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. 28 | // For instance, an object might be attributed to the completion of another activity. 29 | AttributedTo Item `jsonld:"attributedTo,omitempty"` 30 | // Audience identifies one or more entities that represent the total population of entities 31 | // for which the object can considered to be relevant. 32 | Audience ItemCollection `jsonld:"audience,omitempty"` 33 | // Content or textual representation of the Activity Pub Object encoded as a JSON string. 34 | // By default, the value of content is HTML. 35 | // The mediaType property can be used in the object to indicate a different content type. 36 | // (The content MAY be expressed using multiple language-tagged values.) 37 | Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` 38 | // Context identifies the context within which the object exists or an activity was performed. 39 | // The notion of "context" used is intentionally vague. 40 | // The intended function is to serve as a means of grouping objects and activities that share a 41 | // common originating context or purpose. An example could be all activities relating to a common project or event. 42 | Context Item `jsonld:"context,omitempty"` 43 | // MediaType when used on an Object, identifies the MIME media type of the value of the content property. 44 | // If not specified, the content property is assumed to contain text/html content. 45 | MediaType MimeType `jsonld:"mediaType,omitempty"` 46 | // EndTime the date and time describing the actual or expected ending time of the object. 47 | // When used with an Activity object, for instance, the endTime property specifies the moment 48 | // the activity concluded or is expected to conclude. 49 | EndTime time.Time `jsonld:"endTime,omitempty"` 50 | // Generator identifies the entity (e.g. an application) that generated the object. 51 | Generator Item `jsonld:"generator,omitempty"` 52 | // Icon indicates an entity that describes an icon for this object. 53 | // The image should have an aspect ratio of one (horizontal) to one (vertical) 54 | // and should be suitable for presentation at a small size. 55 | Icon Item `jsonld:"icon,omitempty"` 56 | // Image indicates an entity that describes an image for this object. 57 | // Unlike the icon property, there are no aspect ratio or display size limitations assumed. 58 | Image Item `jsonld:"image,omitempty"` 59 | // InReplyTo indicates one or more entities for which this object is considered a response. 60 | InReplyTo Item `jsonld:"inReplyTo,omitempty"` 61 | // Location indicates one or more physical or logical locations associated with the object. 62 | Location Item `jsonld:"location,omitempty"` 63 | // Preview identifies an entity that provides a preview of this object. 64 | Preview Item `jsonld:"preview,omitempty"` 65 | // Published the date and time at which the object was published 66 | Published time.Time `jsonld:"published,omitempty"` 67 | // Replies identifies a Collection containing objects considered to be responses to this object. 68 | Replies Item `jsonld:"replies,omitempty"` 69 | // StartTime the date and time describing the actual or expected starting time of the object. 70 | // When used with an Activity object, for instance, the startTime property specifies 71 | // the moment the activity began or is scheduled to begin. 72 | StartTime time.Time `jsonld:"startTime,omitempty"` 73 | // Summary a natural language summarization of the object encoded as HTML. 74 | // *Multiple language tagged summaries may be provided.) 75 | Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` 76 | // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. 77 | // The key difference between attachment and tag is that the former implies association by inclusion, 78 | // while the latter implies associated by reference. 79 | Tag ItemCollection `jsonld:"tag,omitempty"` 80 | // Updated the date and time at which the object was updated 81 | Updated time.Time `jsonld:"updated,omitempty"` 82 | // URL identifies one or more links to representations of the object 83 | URL Item `jsonld:"url,omitempty"` 84 | // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object 85 | To ItemCollection `jsonld:"to,omitempty"` 86 | // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. 87 | Bto ItemCollection `jsonld:"bto,omitempty"` 88 | // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. 89 | CC ItemCollection `jsonld:"cc,omitempty"` 90 | // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. 91 | BCC ItemCollection `jsonld:"bcc,omitempty"` 92 | // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, 93 | // the duration property indicates the object's approximate duration. 94 | // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], 95 | // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). 96 | Duration time.Duration `jsonld:"duration,omitempty"` 97 | // This is a list of all Like activities with this object as the object property, added as a side effect. 98 | // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 99 | // of an authenticated user or as appropriate when no authentication is given. 100 | Likes Item `jsonld:"likes,omitempty"` 101 | // This is a list of all Announce activities with this object as the object property, added as a side effect. 102 | // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges 103 | // of an authenticated user or as appropriate when no authentication is given. 104 | Shares Item `jsonld:"shares,omitempty"` 105 | // Source property is intended to convey some sort of source from which the content markup was derived, 106 | // as a form of provenance, or to support future editing by clients. 107 | // In general, clients do the conversion from source to content, not the other way around. 108 | Source Source `jsonld:"source,omitempty"` 109 | // FormerType On a Tombstone object, the formerType property identifies the type of the object that was deleted. 110 | FormerType ActivityVocabularyType `jsonld:"formerType,omitempty"` 111 | // Deleted On a Tombstone object, the deleted property is a timestamp for when the object was deleted. 112 | Deleted time.Time `jsonld:"deleted,omitempty"` 113 | } 114 | 115 | // IsLink returns false for Tombstone objects 116 | func (t Tombstone) IsLink() bool { 117 | return false 118 | } 119 | 120 | // IsObject returns true for Tombstone objects 121 | func (t Tombstone) IsObject() bool { 122 | return true 123 | } 124 | 125 | // IsCollection returns false for Tombstone objects 126 | func (t Tombstone) IsCollection() bool { 127 | return false 128 | } 129 | 130 | // GetLink returns the IRI corresponding to the current Tombstone object 131 | func (t Tombstone) GetLink() IRI { 132 | return IRI(t.ID) 133 | } 134 | 135 | // GetType returns the type of the current Tombstone 136 | func (t Tombstone) GetType() ActivityVocabularyType { 137 | return t.Type 138 | } 139 | 140 | // GetID returns the ID corresponding to the current Tombstone 141 | func (t Tombstone) GetID() ID { 142 | return t.ID 143 | } 144 | 145 | // UnmarshalJSON decodes an incoming JSON document into the receiver object. 146 | func (t *Tombstone) UnmarshalJSON(data []byte) error { 147 | par := fastjson.Parser{} 148 | val, err := par.ParseBytes(data) 149 | if err != nil { 150 | return err 151 | } 152 | return JSONLoadTombstone(val, t) 153 | } 154 | 155 | // MarshalJSON encodes the receiver object to a JSON document. 156 | func (t Tombstone) MarshalJSON() ([]byte, error) { 157 | b := make([]byte, 0) 158 | notEmpty := false 159 | JSONWrite(&b, '{') 160 | 161 | OnObject(t, func(o *Object) error { 162 | notEmpty = JSONWriteObjectValue(&b, *o) 163 | return nil 164 | }) 165 | if len(t.FormerType) > 0 { 166 | if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 { 167 | notEmpty = JSONWriteProp(&b, "formerType", v) || notEmpty 168 | } 169 | } 170 | if !t.Deleted.IsZero() { 171 | notEmpty = JSONWriteTimeProp(&b, "deleted", t.Deleted) || notEmpty 172 | } 173 | if notEmpty { 174 | JSONWrite(&b, '}') 175 | return b, nil 176 | } 177 | return nil, nil 178 | } 179 | 180 | // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. 181 | func (t *Tombstone) UnmarshalBinary(data []byte) error { 182 | return t.GobDecode(data) 183 | } 184 | 185 | // MarshalBinary implements the encoding.BinaryMarshaler interface. 186 | func (t Tombstone) MarshalBinary() ([]byte, error) { 187 | return t.GobEncode() 188 | } 189 | 190 | // GobEncode 191 | func (t Tombstone) GobEncode() ([]byte, error) { 192 | mm := make(map[string][]byte) 193 | hasData, err := mapTombstoneProperties(mm, t) 194 | if err != nil { 195 | return nil, err 196 | } 197 | if !hasData { 198 | return []byte{}, nil 199 | } 200 | bb := bytes.Buffer{} 201 | g := gob.NewEncoder(&bb) 202 | if err := g.Encode(mm); err != nil { 203 | return nil, err 204 | } 205 | return bb.Bytes(), nil 206 | } 207 | 208 | // GobDecode 209 | func (t *Tombstone) GobDecode(data []byte) error { 210 | if len(data) == 0 { 211 | return nil 212 | } 213 | mm, err := gobDecodeObjectAsMap(data) 214 | if err != nil { 215 | return err 216 | } 217 | return unmapTombstoneProperties(mm, t) 218 | } 219 | 220 | // Recipients performs recipient de-duplication on the Tombstone object's To, Bto, CC and BCC properties 221 | func (t *Tombstone) Recipients() ItemCollection { 222 | aud := t.Audience 223 | return ItemCollectionDeduplication(&t.To, &t.CC, &t.Bto, &t.BCC, &aud) 224 | } 225 | 226 | // Clean removes Bto and BCC properties 227 | func (t *Tombstone) Clean() { 228 | _ = OnObject(t, func(o *Object) error { 229 | o.Clean() 230 | return nil 231 | }) 232 | } 233 | 234 | func (t Tombstone) Format(s fmt.State, verb rune) { 235 | switch verb { 236 | case 's', 'v': 237 | _, _ = fmt.Fprintf(s, "%T[%s] { formerType: %q }", t, t.Type, t.FormerType) 238 | } 239 | } 240 | 241 | // ToTombstone 242 | func ToTombstone(it Item) (*Tombstone, error) { 243 | switch i := it.(type) { 244 | case *Tombstone: 245 | return i, nil 246 | case Tombstone: 247 | return &i, nil 248 | case *Object: 249 | return (*Tombstone)(unsafe.Pointer(i)), nil 250 | case Object: 251 | return (*Tombstone)(unsafe.Pointer(&i)), nil 252 | default: 253 | return reflectItemToType[Tombstone](it) 254 | } 255 | } 256 | 257 | type withTombstoneFn func(*Tombstone) error 258 | 259 | // OnTombstone calls function fn on it Item if it can be asserted to type *Tombstone 260 | // 261 | // This function should be called if trying to access the Tombstone specific properties 262 | // like "formerType" or "deleted". 263 | // For the other properties OnObject should be used instead. 264 | func OnTombstone(it Item, fn withTombstoneFn) error { 265 | if it == nil { 266 | return nil 267 | } 268 | if IsItemCollection(it) { 269 | return OnItemCollection(it, func(col *ItemCollection) error { 270 | for _, it := range *col { 271 | if err := OnTombstone(it, fn); err != nil { 272 | return err 273 | } 274 | } 275 | return nil 276 | }) 277 | } 278 | ob, err := ToTombstone(it) 279 | if err != nil { 280 | return err 281 | } 282 | return fn(ob) 283 | } 284 | -------------------------------------------------------------------------------- /tombstone_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestTombstone_GetID(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestTombstone_GetLink(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestTombstone_GetType(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestTombstone_IsCollection(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestTombstone_IsLink(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestTombstone_IsObject(t *testing.T) { 29 | t.Skipf("TODO") 30 | } 31 | 32 | func TestTombstone_UnmarshalJSON(t *testing.T) { 33 | t.Skipf("TODO") 34 | } 35 | 36 | func TestTombstone_Clean(t *testing.T) { 37 | t.Skipf("TODO") 38 | } 39 | 40 | func assertTombstoneWithTesting(fn canErrorFunc, expected *Tombstone) withTombstoneFn { 41 | return func(p *Tombstone) error { 42 | if !assertDeepEquals(fn, p, expected) { 43 | return fmt.Errorf("not equal") 44 | } 45 | return nil 46 | } 47 | } 48 | 49 | func TestOnTombstone(t *testing.T) { 50 | testTombstone := Tombstone{ 51 | ID: "https://example.com", 52 | } 53 | type args struct { 54 | it Item 55 | fn func(canErrorFunc, *Tombstone) withTombstoneFn 56 | } 57 | tests := []struct { 58 | name string 59 | args args 60 | wantErr bool 61 | }{ 62 | { 63 | name: "single", 64 | args: args{testTombstone, assertTombstoneWithTesting}, 65 | wantErr: false, 66 | }, 67 | { 68 | name: "single fails", 69 | args: args{&Tombstone{ID: "https://not-equal"}, assertTombstoneWithTesting}, 70 | wantErr: true, 71 | }, 72 | { 73 | name: "collection of profiles", 74 | args: args{ItemCollection{testTombstone, testTombstone}, assertTombstoneWithTesting}, 75 | wantErr: false, 76 | }, 77 | { 78 | name: "collection of profiles fails", 79 | args: args{ItemCollection{testTombstone, &Tombstone{ID: "not-equal"}}, assertTombstoneWithTesting}, 80 | wantErr: true, 81 | }, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | var logFn canErrorFunc 86 | if tt.wantErr { 87 | logFn = t.Logf 88 | } else { 89 | logFn = t.Errorf 90 | } 91 | if err := OnTombstone(tt.args.it, tt.args.fn(logFn, &testTombstone)); (err != nil) != tt.wantErr { 92 | t.Errorf("OnTombstone() error = %v, wantErr %v", err, tt.wantErr) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /typer_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPathTyper_Type(t *testing.T) { 9 | t.Skipf("TODO") 10 | } 11 | 12 | func TestValidActivityCollection(t *testing.T) { 13 | t.Skipf("TODO") 14 | } 15 | 16 | func TestValidCollection(t *testing.T) { 17 | t.Skipf("TODO") 18 | } 19 | 20 | func TestValidObjectCollection(t *testing.T) { 21 | t.Skipf("TODO") 22 | } 23 | 24 | func TestValidCollectionIRI(t *testing.T) { 25 | t.Skipf("TODO") 26 | } 27 | 28 | func TestSplit(t *testing.T) { 29 | t.Skipf("TODO") 30 | } 31 | 32 | func TestCollectionTypes_Of(t *testing.T) { 33 | type args struct { 34 | o Item 35 | t CollectionPath 36 | } 37 | tests := []struct { 38 | name string 39 | args args 40 | want Item 41 | }{ 42 | { 43 | name: "nil from nil object", 44 | args: args{ 45 | o: nil, 46 | t: "likes", 47 | }, 48 | want: nil, 49 | }, 50 | { 51 | name: "nil from invalid CollectionPath type", 52 | args: args{ 53 | o: Object{ 54 | Likes: IRI("test"), 55 | }, 56 | t: "like", 57 | }, 58 | want: nil, 59 | }, 60 | { 61 | name: "nil from nil CollectionPath type", 62 | args: args{ 63 | o: Object{ 64 | Likes: nil, 65 | }, 66 | t: "likes", 67 | }, 68 | want: nil, 69 | }, 70 | { 71 | name: "get likes iri", 72 | args: args{ 73 | o: Object{ 74 | Likes: IRI("test"), 75 | }, 76 | t: "likes", 77 | }, 78 | want: IRI("test"), 79 | }, 80 | } 81 | 82 | for _, test := range tests { 83 | t.Run(test.name, func(t *testing.T) { 84 | if ob := test.args.t.Of(test.args.o); ob != test.want { 85 | t.Errorf("Object received %#v is different, expected #%v", ob, test.want) 86 | } 87 | }) 88 | } 89 | } 90 | 91 | func TestCollectionType_IRI(t *testing.T) { 92 | type args struct { 93 | o Item 94 | t CollectionPath 95 | } 96 | tests := []struct { 97 | name string 98 | args args 99 | want IRI 100 | }{ 101 | { 102 | name: "just path from nil object", 103 | args: args{ 104 | o: nil, 105 | t: "likes", 106 | }, 107 | want: IRI("/likes"), 108 | }, 109 | { 110 | name: "emptyIRI from invalid CollectionPath type", 111 | args: args{ 112 | o: Object{ 113 | Likes: IRI("test"), 114 | }, 115 | t: "like", 116 | }, 117 | want: "/like", 118 | }, 119 | { 120 | name: "just path from object without ID", 121 | args: args{ 122 | o: Object{}, 123 | t: "likes", 124 | }, 125 | want: IRI("/likes"), 126 | }, 127 | { 128 | name: "likes iri on object", 129 | args: args{ 130 | o: Object{ 131 | ID: "http://example.com", 132 | Likes: IRI("test"), 133 | }, 134 | t: "likes", 135 | }, 136 | want: IRI("test"), 137 | }, 138 | } 139 | 140 | for _, test := range tests { 141 | t.Run(test.name, func(t *testing.T) { 142 | if ob := test.args.t.IRI(test.args.o); ob != test.want { 143 | t.Errorf("IRI received %q is different, expected %q", ob, test.want) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | func TestCollectionType_OfActor(t *testing.T) { 150 | t.Skipf("TODO") 151 | } 152 | 153 | func TestCollectionTypes_Contains(t *testing.T) { 154 | t.Skipf("TODO") 155 | } 156 | 157 | func TestIRIf(t *testing.T) { 158 | type args struct { 159 | i IRI 160 | t CollectionPath 161 | } 162 | tests := []struct { 163 | name string 164 | args args 165 | want IRI 166 | }{ 167 | { 168 | name: "nil iri", 169 | args: args{ 170 | i: Object{}.ID, 171 | t: "inbox", 172 | }, 173 | want: "/inbox", 174 | }, 175 | { 176 | name: "empty iri", 177 | args: args{ 178 | i: "", 179 | t: "inbox", 180 | }, 181 | want: "/inbox", 182 | }, 183 | { 184 | name: "plain concat", 185 | args: args{ 186 | i: "https://example.com", 187 | t: "inbox", 188 | }, 189 | want: "https://example.com/inbox", 190 | }, 191 | { 192 | name: "strip root from iri", 193 | args: args{ 194 | i: "https://example.com/", 195 | t: "inbox", 196 | }, 197 | want: "https://example.com/inbox", 198 | }, 199 | { 200 | name: "invalid iri", 201 | args: args{ 202 | i: "example.com", 203 | t: "test", 204 | }, 205 | want: "example.com/test", 206 | }, 207 | } 208 | for _, tt := range tests { 209 | t.Run(tt.name, func(t *testing.T) { 210 | if got := IRIf(tt.args.i, tt.args.t); got != tt.want { 211 | t.Errorf("IRIf() = %v, want %v", got, tt.want) 212 | } 213 | }) 214 | } 215 | } 216 | 217 | func TestCollectionType_AddTo(t *testing.T) { 218 | type args struct { 219 | i Item 220 | } 221 | var i Item 222 | var o *Object 223 | tests := []struct { 224 | name string 225 | t CollectionPath 226 | args args 227 | want IRI 228 | want1 bool 229 | }{ 230 | { 231 | name: "simple", 232 | t: "test", 233 | args: args{ 234 | i: &Object{ID: "http://example.com/addTo"}, 235 | }, 236 | want: "http://example.com/addTo/test", 237 | want1: false, // this seems to always be false 238 | }, 239 | { 240 | name: "on-nil-item", 241 | t: "test", 242 | args: args{ 243 | i: i, 244 | }, 245 | want: NilIRI, 246 | want1: false, 247 | }, 248 | { 249 | name: "on-nil", 250 | t: "test", 251 | args: args{ 252 | i: nil, 253 | }, 254 | want: NilIRI, 255 | want1: false, 256 | }, 257 | { 258 | name: "on-nil-object", 259 | t: "test", 260 | args: args{ 261 | i: o, 262 | }, 263 | want: NilIRI, 264 | want1: false, 265 | }, 266 | { 267 | name: "on-nil-item", 268 | t: "test", 269 | args: args{ 270 | i: i, 271 | }, 272 | want: NilIRI, 273 | want1: false, 274 | }, 275 | } 276 | for _, tt := range tests { 277 | t.Run(tt.name, func(t *testing.T) { 278 | got, got1 := tt.t.AddTo(tt.args.i) 279 | if got != tt.want { 280 | t.Errorf("AddTo() got = %v, want %v", got, tt.want) 281 | } 282 | if got1 != tt.want1 { 283 | t.Errorf("AddTo() got1 = %v, want %v", got1, tt.want1) 284 | } 285 | }) 286 | } 287 | } 288 | 289 | func TestCollectionPaths_Split(t *testing.T) { 290 | tests := []struct { 291 | name string 292 | t CollectionPaths 293 | given IRI 294 | maybeActor IRI 295 | maybeCol CollectionPath 296 | }{ 297 | { 298 | name: "empty", 299 | t: nil, 300 | given: "", 301 | maybeActor: "", 302 | maybeCol: "", 303 | }, 304 | { 305 | name: "nil with example.com", 306 | t: nil, 307 | given: "example.com", 308 | maybeActor: "example.com", 309 | maybeCol: "", 310 | }, 311 | { 312 | name: "nil with https://example.com", 313 | t: nil, 314 | given: "https://example.com/", 315 | maybeActor: "https://example.com", 316 | maybeCol: Unknown, 317 | }, 318 | { 319 | name: "outbox with https://example.com/outbox", 320 | t: CollectionPaths{Outbox}, 321 | given: "https://example.com/outbox", 322 | maybeActor: "https://example.com", 323 | maybeCol: Outbox, 324 | }, 325 | { 326 | name: "{outbox,inbox} with https://example.com/inbox", 327 | t: CollectionPaths{Outbox, Inbox}, 328 | given: "https://example.com/inbox", 329 | maybeActor: "https://example.com", 330 | maybeCol: Inbox, 331 | }, 332 | { 333 | // TODO(marius): This feels wrong. 334 | name: "outbox with https://example.com/inbox", 335 | t: CollectionPaths{Outbox}, 336 | given: "https://example.com/inbox", 337 | maybeActor: "https://example.com", 338 | maybeCol: Unknown, 339 | }, 340 | { 341 | name: "invalid url", 342 | t: CollectionPaths{Inbox}, 343 | given: "127.0.0.1:666/inbox", 344 | maybeActor: "127.0.0.1:666", 345 | maybeCol: Inbox, 346 | }, 347 | { 348 | name: "invalid url - collection doesn't match", 349 | t: CollectionPaths{Outbox}, 350 | given: "127.0.0.1:666/inbox", 351 | maybeActor: "127.0.0.1:666/inbox", 352 | maybeCol: Unknown, 353 | }, 354 | } 355 | for _, tt := range tests { 356 | t.Run(tt.name, func(t *testing.T) { 357 | ma, mc := tt.t.Split(tt.given) 358 | if ma != tt.maybeActor { 359 | t.Errorf("Split() got Actor = %q, want %q", ma, tt.maybeActor) 360 | } 361 | if mc != tt.maybeCol { 362 | t.Errorf("Split() got Colletion Path = %q, want %q", mc, tt.maybeCol) 363 | } 364 | }) 365 | } 366 | } 367 | 368 | func TestCollectionPath_Of(t *testing.T) { 369 | tests := []struct { 370 | name string 371 | t CollectionPath 372 | arg Item 373 | want Item 374 | }{ 375 | { 376 | name: "all-nil", 377 | t: "", 378 | }, 379 | { 380 | name: "inbox-nil", 381 | t: Inbox, 382 | }, 383 | { 384 | name: "outbox-nil", 385 | t: Outbox, 386 | }, 387 | { 388 | name: "followers-nil", 389 | t: Followers, 390 | }, 391 | { 392 | name: "following-nil", 393 | t: Following, 394 | }, 395 | { 396 | name: "liked-nil", 397 | t: Liked, 398 | }, 399 | { 400 | name: "likes-nil", 401 | t: Likes, 402 | }, 403 | { 404 | name: "shares-nil", 405 | t: Shares, 406 | }, 407 | { 408 | name: "replies-nil", 409 | t: Replies, 410 | }, 411 | { 412 | name: "inbox-empty", 413 | t: Inbox, 414 | arg: &Actor{}, 415 | }, 416 | { 417 | name: "outbox-empty", 418 | t: Outbox, 419 | arg: &Actor{}, 420 | }, 421 | { 422 | name: "followers-empty", 423 | t: Followers, 424 | arg: &Actor{}, 425 | }, 426 | { 427 | name: "following-empty", 428 | t: Following, 429 | arg: &Actor{}, 430 | }, 431 | { 432 | name: "liked-empty", 433 | t: Liked, 434 | arg: &Actor{}, 435 | }, 436 | { 437 | name: "likes-empty", 438 | t: Likes, 439 | arg: &Object{}, 440 | }, 441 | { 442 | name: "shares-empty", 443 | t: Shares, 444 | arg: &Object{}, 445 | }, 446 | { 447 | name: "replies-empty", 448 | t: Replies, 449 | arg: &Object{}, 450 | }, 451 | // 452 | { 453 | name: "inbox", 454 | t: Inbox, 455 | arg: &Actor{ 456 | Type: PersonType, 457 | Inbox: IRI("https://example.com/inbox"), 458 | }, 459 | want: IRI("https://example.com/inbox"), 460 | }, 461 | { 462 | name: "outbox", 463 | t: Outbox, 464 | arg: &Actor{ 465 | Type: PersonType, 466 | Outbox: IRI("https://example.com/outbox"), 467 | }, 468 | want: IRI("https://example.com/outbox"), 469 | }, 470 | { 471 | name: "followers", 472 | t: Followers, 473 | arg: &Actor{ 474 | Type: GroupType, 475 | Followers: IRI("https://example.com/c132-333"), 476 | }, 477 | want: IRI("https://example.com/c132-333"), 478 | }, 479 | { 480 | name: "following", 481 | t: Following, 482 | arg: &Actor{ 483 | Type: GroupType, 484 | Following: IRI("https://example.com/c666-333"), 485 | }, 486 | want: IRI("https://example.com/c666-333"), 487 | }, 488 | { 489 | name: "liked", 490 | t: Liked, 491 | arg: &Actor{ 492 | Type: ApplicationType, 493 | Liked: IRI("https://example.com/l666"), 494 | }, 495 | want: IRI("https://example.com/l666"), 496 | }, 497 | { 498 | name: "likes", 499 | t: Likes, 500 | arg: &Object{ 501 | Type: NoteType, 502 | Likes: IRI("https://example.com/l166"), 503 | }, 504 | want: IRI("https://example.com/l166"), 505 | }, 506 | { 507 | name: "shares", 508 | t: Shares, 509 | arg: &Object{ 510 | Type: PageType, 511 | Shares: IRI("https://example.com/s266"), 512 | }, 513 | want: IRI("https://example.com/s266"), 514 | }, 515 | { 516 | name: "replies", 517 | t: Replies, 518 | arg: &Object{ 519 | Type: ArticleType, 520 | Replies: IRI("https://example.com/r466"), 521 | }, 522 | want: IRI("https://example.com/r466"), 523 | }, 524 | } 525 | for _, tt := range tests { 526 | t.Run(tt.name, func(t *testing.T) { 527 | if got := tt.t.Of(tt.arg); !reflect.DeepEqual(got, tt.want) { 528 | t.Errorf("Of() = %v, want %v", got, tt.want) 529 | } 530 | }) 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | // ActivityVocabularyTypes is a type alias for a slice of ActivityVocabularyType elements 4 | type ActivityVocabularyTypes []ActivityVocabularyType 5 | 6 | // Types contains all valid types in the ActivityPub vocabulary 7 | var Types = ActivityVocabularyTypes{ 8 | LinkType, 9 | MentionType, 10 | 11 | ArticleType, 12 | AudioType, 13 | DocumentType, 14 | EventType, 15 | ImageType, 16 | NoteType, 17 | PageType, 18 | PlaceType, 19 | ProfileType, 20 | RelationshipType, 21 | TombstoneType, 22 | VideoType, 23 | 24 | QuestionType, 25 | 26 | CollectionType, 27 | OrderedCollectionType, 28 | CollectionPageType, 29 | OrderedCollectionPageType, 30 | 31 | ApplicationType, 32 | GroupType, 33 | OrganizationType, 34 | PersonType, 35 | ServiceType, 36 | 37 | AcceptType, 38 | AddType, 39 | AnnounceType, 40 | BlockType, 41 | CreateType, 42 | DeleteType, 43 | DislikeType, 44 | FlagType, 45 | FollowType, 46 | IgnoreType, 47 | InviteType, 48 | JoinType, 49 | LeaveType, 50 | LikeType, 51 | ListenType, 52 | MoveType, 53 | OfferType, 54 | RejectType, 55 | ReadType, 56 | RemoveType, 57 | TentativeRejectType, 58 | TentativeAcceptType, 59 | UndoType, 60 | UpdateType, 61 | ViewType, 62 | 63 | ArriveType, 64 | TravelType, 65 | QuestionType, 66 | } 67 | -------------------------------------------------------------------------------- /validation.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | // ValidationErrors is an aggregated error interface that allows 4 | // a Validator implementation to return all possible errors. 5 | type ValidationErrors interface { 6 | error 7 | Errors() []error 8 | Add(error) 9 | } 10 | 11 | // Validator is the interface that needs to be implemented by objects that 12 | // provide a validation mechanism for incoming ActivityPub Objects or IRIs 13 | // against an external set of rules. 14 | type Validator interface { 15 | Validate(receiver IRI, incoming Item) (bool, ValidationErrors) 16 | } 17 | -------------------------------------------------------------------------------- /validation_test.go: -------------------------------------------------------------------------------- 1 | package activitypub 2 | 3 | import "testing" 4 | 5 | func TestDefaultValidator_Validate(t *testing.T) { 6 | t.Skipf("TODO") 7 | } 8 | --------------------------------------------------------------------------------