From fd8140a86c8ece5b39c8c979f3df8f94d3ad80a6 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 14 Feb 2024 20:48:31 +0600 Subject: [PATCH 01/14] :bug: Fixed page resource loading --- internal/cmd/home/home.go | 10 ++++++---- internal/resource/repository.go | 3 +++ internal/resource/repository/fs/fs_resource.go | 4 ++++ internal/resource/usecase/resource_ucase.go | 4 +--- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/internal/cmd/home/home.go b/internal/cmd/home/home.go index 391ac58..4e5c600 100644 --- a/internal/cmd/home/home.go +++ b/internal/cmd/home/home.go @@ -152,7 +152,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { // NOTE(toby3d): client request '/:lang/...', try to // understand which language code in subdir is requested. - lang = domain.NewLanguage(head) + if lang = domain.NewLanguage(head); lang != domain.LanguageUnd { + r.URL.Path = tail + } // NOTE(toby3d): get localized site config for requested // subdir if exists. @@ -165,9 +167,9 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { if res, err := resourcer.Do(r.Context(), r.URL.Path); err == nil { // TODO(toby3d) : ugly workaround, must be refactored - resFile, err := contentDir.Open(res.File.Path()) + resFile, err := contentDir.Open(res.File.Filename()) if err != nil { - http.Error(w, "cannot open: "+err.Error(), http.StatusInternalServerError) + http.Error(w, "cannot open resource: "+err.Error(), http.StatusInternalServerError) return } @@ -175,7 +177,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { resBytes, err := io.ReadAll(resFile) if err != nil { - http.Error(w, "cannot read all: "+err.Error(), http.StatusInternalServerError) + http.Error(w, "cannot read resource: "+err.Error(), http.StatusInternalServerError) return } diff --git a/internal/resource/repository.go b/internal/resource/repository.go index 8258f13..7368052 100644 --- a/internal/resource/repository.go +++ b/internal/resource/repository.go @@ -2,6 +2,7 @@ package resource import ( "context" + "errors" "source.toby3d.me/toby3d/home/internal/domain" ) @@ -13,3 +14,5 @@ type Repository interface { // Fetch returns all resources from dir recursevly. Fetch(ctx context.Context, pattern string) (domain.Resources, int, error) } + +var ErrIsDir error = errors.New("resource is a directory") diff --git a/internal/resource/repository/fs/fs_resource.go b/internal/resource/repository/fs/fs_resource.go index ab25d3d..506847e 100644 --- a/internal/resource/repository/fs/fs_resource.go +++ b/internal/resource/repository/fs/fs_resource.go @@ -25,6 +25,10 @@ func (repo *fileServerResourceRepository) Get(ctx context.Context, p string) (*d return nil, fmt.Errorf("cannot stat resource on path '%s': %w", p, err) } + if info.IsDir() { + return nil, fmt.Errorf("cannot open resource on path '%s': %w", p, resource.ErrIsDir) + } + f, err := repo.root.Open(p) if err != nil { return nil, fmt.Errorf("cannot open resource on path '%s': %w", p, err) diff --git a/internal/resource/usecase/resource_ucase.go b/internal/resource/usecase/resource_ucase.go index 649a5d9..5a8e4a9 100644 --- a/internal/resource/usecase/resource_ucase.go +++ b/internal/resource/usecase/resource_ucase.go @@ -21,9 +21,7 @@ func NewResourceUseCase(resources resource.Repository) resource.UseCase { } func (ucase *resourceUseCase) Do(ctx context.Context, p string) (*domain.Resource, error) { - p = strings.TrimPrefix(path.Clean(p), "/") - - r, err := ucase.resources.Get(ctx, p) + r, err := ucase.resources.Get(ctx, strings.TrimPrefix(path.Clean(p), "/")) if err != nil { return nil, fmt.Errorf("cannot get resource file: %w", err) } From 0c38b587b4c7870386ea4f988d919ef749f4f99f Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 14 Feb 2024 21:23:06 +0600 Subject: [PATCH 02/14] :technologist: Created entry handler with basic ActivityPub support --- go.mod | 8 + go.sum | 10 ++ internal/domain/entry.go | 20 +++ internal/domain/path.go | 17 +- internal/entry/delivery/http/entry_http.go | 153 ++++++++++++++++++ .../entry/delivery/http/entry_http_test.go | 44 +++++ .../TestHandler_ServeHTTP/note.golden | 1 + .../TestHandler_ServeHTTP/person.golden | 1 + internal/entry/usecase/page_ucase.go | 26 ++- internal/theme/usecase/theme_ucase.go | 20 ++- 10 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 internal/entry/delivery/http/entry_http.go create mode 100644 internal/entry/delivery/http/entry_http_test.go create mode 100755 internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/note.golden create mode 100755 internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/person.golden diff --git a/go.mod b/go.mod index 9f5ae20..5a7d5ed 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,16 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require ( + git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect + github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect + github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 // indirect + github.com/valyala/fastjson v1.6.4 // indirect +) + require ( github.com/BurntSushi/toml v1.3.2 // indirect + github.com/go-ap/activitypub v0.0.0-20240211124657-820024a66b78 github.com/valyala/bytebufferpool v1.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 45c6743..3c04fc9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= +git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= @@ -7,6 +9,12 @@ github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu github.com/andybalholm/brotli v1.0.3/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA= github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18= +github.com/go-ap/activitypub v0.0.0-20240211124657-820024a66b78 h1:M3QPsOk2J/AyyIODQQf2Jm9vsp6Jor0NQWyIBzI3oSM= +github.com/go-ap/activitypub v0.0.0-20240211124657-820024a66b78/go.mod h1:cJ9Ye0ZNSMN7RzZDBRY3E+8M3Bpf/R1JX22Ir9yX6WI= +github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 h1:I2nuhyVI/48VXoRCCZR2hYBgnSXa+EuDJf/VyX06TC0= +github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7/go.mod h1:5x8a6P/dhmMGFxWLcyYlyOuJ2lRNaHGhRv+yu8BaTSI= +github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 h1:GMKIYXyXPGIp+hYiWOhfqK4A023HdgisDT4YGgf99mw= +github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5LaADntW+UEsMjl3IlIwk+DxlYNsbofQkGA= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -15,6 +23,8 @@ github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.30.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/quicktemplate v1.7.0 h1:LUPTJmlVcb46OOUY3IeD9DojFpAVbsG+5WFTcjMJzCM= github.com/valyala/quicktemplate v1.7.0/go.mod h1:sqKJnoaOF88V07vkO+9FL8fb9uZg/VPSJnLYn+LmLk8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= diff --git a/internal/domain/entry.go b/internal/domain/entry.go index c7b6015..9ce4073 100644 --- a/internal/domain/entry.go +++ b/internal/domain/entry.go @@ -1,5 +1,10 @@ package domain +import ( + "path" + "testing" +) + type Entry struct { Language Language Params map[string]any @@ -18,3 +23,18 @@ func (e Entry) IsHome() bool { func (e Entry) IsTranslated() bool { return 1 < len(e.Translations) } + +func TestEntry(tb testing.TB) *Entry { + tb.Helper() + + return &Entry{ + Language: NewLanguage("en"), + Params: make(map[string]any), + File: NewPath(path.Join("sample-page.en.md")), + Description: "do not use in production", + Title: "Sample Page", + Content: []byte("Hello, world!"), + Resources: make([]*Resource, 0), + Translations: make([]*Entry, 0), + } +} diff --git a/internal/domain/path.go b/internal/domain/path.go index b5af346..0fd0d68 100644 --- a/internal/domain/path.go +++ b/internal/domain/path.go @@ -2,6 +2,7 @@ package domain import ( "crypto/md5" + "path" "path/filepath" "strings" ) @@ -19,16 +20,16 @@ type Path struct { uniqueId string } -func NewPath(path string) Path { +func NewPath(p string) Path { out := Path{ Language: LanguageUnd, baseFileName: "", contentBaseName: "", - dir: filepath.Dir(path), - ext: strings.TrimPrefix(filepath.Ext(path), "."), - filename: path, - logicalName: filepath.Base(path), - path: path, + dir: filepath.Dir(p), + ext: strings.TrimPrefix(filepath.Ext(p), "."), + filename: p, + logicalName: filepath.Base(p), + path: "", translationBaseName: "", uniqueId: "", } @@ -54,6 +55,8 @@ func NewPath(path string) Path { out.contentBaseName = filepath.Base(out.dir) } + out.path = path.Join(out.Language.code, out.dir, out.contentBaseName) + hash := md5.New() _, _ = hash.Write([]byte(out.path)) out.uniqueId = string(hash.Sum(nil)) @@ -99,6 +102,7 @@ func (p Path) Ext() string { return p.ext } +// Filename returns the file path relative to $HOME_CONTENT_DIR. func (p Path) Filename() string { return p.filename } @@ -113,6 +117,7 @@ func (p Path) LogicalName() string { return p.logicalName } +// Path returns the relative URL path to the file. func (p Path) Path() string { return p.path } diff --git a/internal/entry/delivery/http/entry_http.go b/internal/entry/delivery/http/entry_http.go new file mode 100644 index 0000000..4a7b6d4 --- /dev/null +++ b/internal/entry/delivery/http/entry_http.go @@ -0,0 +1,153 @@ +package http + +import ( + "encoding/json" + "errors" + "mime" + "net/http" + "strings" + "time" + + "github.com/go-ap/activitypub" + + "source.toby3d.me/toby3d/home/internal/common" + "source.toby3d.me/toby3d/home/internal/domain" + "source.toby3d.me/toby3d/home/internal/entry" + "source.toby3d.me/toby3d/home/internal/site" + "source.toby3d.me/toby3d/home/internal/theme" + "source.toby3d.me/toby3d/home/internal/urlutil" +) + +type ( + Handler struct { + entries entry.UseCase + sites site.UseCase + themes theme.UseCase + } + + Profile struct { + *activitypub.Person + + // Mastodon related + + // Pinned posts. See [Featured collection]. + // + // [Featured collection]: https://docs.joinmastodon.org/spec/activitypub/#featured + Featured []interface{} `json:"featured"` + + // Required for Move activity. + AlsoKnownAs []string `json:"alsoKnownAs"` + + // Will be shown as a locked account. + ManuallyApprovesFollowers bool `json:"manuallyApprovesFollowers"` + + // Will be shown in the profile directory. + // See [Discoverability flag]. + // + // [Discoverability flag]: https://docs.joinmastodon.org/spec/activitypub/#discoverable + Discoverable bool `json:"discoverable"` + } +) + +func NewHandler(sites site.UseCase, entries entry.UseCase, themes theme.UseCase) *Handler { + return &Handler{ + sites: sites, + entries: entries, + themes: themes, + } +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + head, tail := urlutil.ShiftPath(r.URL.Path) + + lang := domain.NewLanguage(head) + + s, err := h.sites.Do(r.Context(), lang) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + contentType, params, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept)) + + switch contentType { + case common.MIMEApplicationLdJSON: // NOTE(toby3d): show entry as ActivityPub object. + if profile, ok := params["profile"]; ok && profile != "https://www.w3.org/ns/activitystreams" { + http.Error(w, "got '"+profile+"' profile value, want 'https://www.w3.org/ns/activitystreams'", + http.StatusBadRequest) + + return + } + + w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8) + encoder := json.NewEncoder(w) + + if head == "" { // NOTE(toby3d): base URL point to owner Profile. + langRef := activitypub.LangRef(lang.Lang()) + person := activitypub.PersonNew(activitypub.IRI(s.BaseURL.String())) + person.URL = person.ID + person.Name.Add(activitypub.LangRefValueNew(langRef, "Maxim Lebedev")) + person.Summary.Add(activitypub.LangRefValueNew(langRef, "Creative dude from russia")) + person.PreferredUsername.Add(activitypub.LangRefValueNew(langRef, "toby3d")) + person.Published = time.Date(2009, time.February, 0, 0, 0, 0, 0, time.UTC) + + _ = encoder.Encode(person) + + return + } + + e, err := h.entries.Do(r.Context(), lang, tail) + if err != nil { + if errors.Is(err, entry.ErrNotExist) { + http.NotFound(w, r) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + return + } + + resp := activitypub.ObjectNew(activitypub.NoteType) + resp.ID = activitypub.ID(s.BaseURL.JoinPath(e.File.Path()).String()) + resp.Content.Add(activitypub.LangRefValueNew(activitypub.NilLangRef, string(e.Content))) + + _ = encoder.Encode(resp) + + return + } + + // NOTE(toby3d): search entry for requested URL and language + // code in subdir. + e, err := h.entries.Do(r.Context(), lang, tail) + if err != nil { + if errors.Is(err, entry.ErrNotExist) { + http.NotFound(w, r) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + return + } + + // NOTE(toby3d): wrap founded entry into theme template and + // answer to client. + contentLanguage := make([]string, len(e.Translations)) + for i := range e.Translations { + contentLanguage[i] = e.Translations[i].Language.Code() + } + + w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", ")) + + template, err := h.themes.Do(r.Context(), s, e) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + + w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) + if err = template(w); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} diff --git a/internal/entry/delivery/http/entry_http_test.go b/internal/entry/delivery/http/entry_http_test.go new file mode 100644 index 0000000..8c06c80 --- /dev/null +++ b/internal/entry/delivery/http/entry_http_test.go @@ -0,0 +1,44 @@ +package http_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "source.toby3d.me/toby3d/home/internal/common" + "source.toby3d.me/toby3d/home/internal/domain" + delivery "source.toby3d.me/toby3d/home/internal/entry/delivery/http" + entryucase "source.toby3d.me/toby3d/home/internal/entry/usecase" + siteucase "source.toby3d.me/toby3d/home/internal/site/usecase" + "source.toby3d.me/toby3d/home/internal/testutil" + themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase" +) + +func TestHandler_ServeHTTP(t *testing.T) { + t.Parallel() + + testEntry := domain.TestEntry(t) + + for name, path := range map[string]string{ + "person": "/", + "note": testEntry.File.Path(), + } { + name, path := name, path + + t.Run(name, func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest(http.MethodGet, "https://example.com/"+path, nil) + req.Header.Set(common.HeaderAccept, common.MIMEApplicationLdJSONProfile) + + w := httptest.NewRecorder() + + delivery.NewHandler( + siteucase.NewStubSiteUseCase(domain.TestSite(t), nil), + entryucase.NewStubEntryUseCase(domain.TestEntry(t), nil), + themeucase.NewDummyThemeUseCase(), + ).ServeHTTP(w, req) + testutil.GoldenEqual(t, w.Result().Body) + }) + } +} diff --git a/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/note.golden b/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/note.golden new file mode 100755 index 0000000..7bea216 --- /dev/null +++ b/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/note.golden @@ -0,0 +1 @@ +{"id":"https://example.com/en/sample-page","type":"Note","content":"Hello, world!"} diff --git a/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/person.golden b/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/person.golden new file mode 100755 index 0000000..90f87fb --- /dev/null +++ b/internal/entry/delivery/http/testdata/TestHandler_ServeHTTP/person.golden @@ -0,0 +1 @@ +{"id":"https://example.com/","type":"Person","name":"Maxim Lebedev","summary":"Creative dude from russia","url":"https://example.com/","published":"2009-01-31T00:00:00Z","preferredUsername":"toby3d"} diff --git a/internal/entry/usecase/page_ucase.go b/internal/entry/usecase/page_ucase.go index ca768e7..a8d351c 100644 --- a/internal/entry/usecase/page_ucase.go +++ b/internal/entry/usecase/page_ucase.go @@ -12,10 +12,17 @@ import ( "source.toby3d.me/toby3d/home/internal/urlutil" ) -type entryUseCase struct { - entries entry.Repository - resources resource.Repository -} +type ( + entryUseCase struct { + entries entry.Repository + resources resource.Repository + } + + stubEntryUseCase struct { + entry *domain.Entry + err error + } +) func NewEntryUseCase(entries entry.Repository, resources resource.Repository) entry.UseCase { return &entryUseCase{ @@ -78,3 +85,14 @@ func (ucase *entryUseCase) Do(ctx context.Context, lang domain.Language, p strin return nil, fmt.Errorf("cannot find page on path '%s': %w", p, entry.ErrNotExist) } + +func NewStubEntryUseCase(entry *domain.Entry, err error) entry.UseCase { + return &stubEntryUseCase{ + entry: entry, + err: err, + } +} + +func (ucase *stubEntryUseCase) Do(_ context.Context, _ domain.Language, _ string) (*domain.Entry, error) { + return ucase.entry, ucase.err +} diff --git a/internal/theme/usecase/theme_ucase.go b/internal/theme/usecase/theme_ucase.go index 2022e4c..12f9373 100644 --- a/internal/theme/usecase/theme_ucase.go +++ b/internal/theme/usecase/theme_ucase.go @@ -11,10 +11,14 @@ import ( "source.toby3d.me/toby3d/home/internal/theme" ) -type themeUseCase struct { - partials fs.FS - themes theme.Repository -} +type ( + themeUseCase struct { + partials fs.FS + themes theme.Repository + } + + dummyThemeUseCase struct{} +) func NewThemeUseCase(partials fs.FS, themes theme.Repository) theme.UseCase { return &themeUseCase{ @@ -36,3 +40,11 @@ func (ucase *themeUseCase) Do(ctx context.Context, s *domain.Site, e *domain.Ent }) }, nil } + +func NewDummyThemeUseCase() theme.UseCase { + return dummyThemeUseCase{} +} + +func (dummyThemeUseCase) Do(_ context.Context, _ *domain.Site, _ *domain.Entry) (theme.Writer, error) { + return nil, nil +} From a52f83378bf4057e04ff185604afff5abea44652 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 14 Feb 2024 21:24:20 +0600 Subject: [PATCH 03/14] :building_construction: Connect and use entry handler in home cmd --- internal/cmd/home/home.go | 123 +++++++++----------------------------- 1 file changed, 27 insertions(+), 96 deletions(-) diff --git a/internal/cmd/home/home.go b/internal/cmd/home/home.go index 4e5c600..3aec5d9 100644 --- a/internal/cmd/home/home.go +++ b/internal/cmd/home/home.go @@ -3,11 +3,11 @@ package home import ( "bytes" "context" - "errors" "fmt" "io" "io/fs" "log" + "mime" "net" "net/http" "os" @@ -18,7 +18,7 @@ import ( "source.toby3d.me/toby3d/home/internal/common" "source.toby3d.me/toby3d/home/internal/domain" - "source.toby3d.me/toby3d/home/internal/entry" + entryhttpdelivery "source.toby3d.me/toby3d/home/internal/entry/delivery/http" pagefsrepo "source.toby3d.me/toby3d/home/internal/entry/repository/fs" pageucase "source.toby3d.me/toby3d/home/internal/entry/usecase" "source.toby3d.me/toby3d/home/internal/middleware" @@ -62,31 +62,40 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { serverer := servercase.NewServerUseCase(sites) webfingerer := webfingerucase.NewWebFingerUseCase(sites) webfingerHandler := webfingerhttpdelivery.NewHandler(webfingerer) + entryHandler := entryhttpdelivery.NewHandler(siter, entrier, themer) handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // NOTE(toby3d): any static file is public and unprotected by // design, so it's safe to search it first before deep down to // any page or it's resource which might be protected by // middleware or something else. - if static, err := staticer.Do(r.Context(), strings.TrimPrefix(r.URL.Path, "/")); err == nil { + if static, err := staticer.Do(r.Context(), r.URL.Path); err == nil { http.ServeContent(w, r, static.Name(), static.ModTime(), static) return } head, tail := urlutil.ShiftPath(r.URL.Path) - lang := domain.LanguageUnd - - // NOTE(toby3d): read $HOME_CONTENT_DIR/index.md as a source of - // truth and global settings for any child entry. - s, err := siter.Do(r.Context(), lang) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } switch strings.ToLower(head) { case "": + contentType, _, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept)) + + switch contentType { + case common.MIMEApplicationLdJSON: // NOTE(toby3d): show entry as ActivityPub object. + entryHandler.ServeHTTP(w, r) + + return + } + + // NOTE(toby3d): read $HOME_CONTENT_DIR/index.md as a source of + // truth and global settings for any child entry. + s, err := siter.Do(r.Context(), domain.LanguageUnd) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + + return + } + if s.IsMultiLingual() { supported := make([]language.Tag, len(s.Languages)) for i := range s.Languages { @@ -105,37 +114,11 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { } matched, _, _ := language.NewMatcher(supported).Match(requested...) - lang = domain.NewLanguage(matched.String()) - - http.Redirect(w, r, "/"+lang.Lang()+"/", http.StatusSeeOther) + http.Redirect(w, r, "/"+domain.NewLanguage(matched.String()).Lang()+"/", + http.StatusSeeOther) return } - - e, err := entrier.Do(r.Context(), lang, r.URL.Path) - if err != nil { - if errors.Is(err, entry.ErrNotExist) { - http.NotFound(w, r) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - return - } - - template, err := themer.Do(r.Context(), s, e) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - - w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) - if err = template(w); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - return case ".well-known": switch strings.ToLower(tail) { case "/webfinger": @@ -145,27 +128,7 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { } } - if s.IsMultiLingual() { - // NOTE(toby3d): $HOME_CONTENT_DIR contains at least two - // index.md with different language codes. - head, tail = urlutil.ShiftPath(r.URL.Path) - - // NOTE(toby3d): client request '/:lang/...', try to - // understand which language code in subdir is requested. - if lang = domain.NewLanguage(head); lang != domain.LanguageUnd { - r.URL.Path = tail - } - - // NOTE(toby3d): get localized site config for requested - // subdir if exists. - if s, err = siter.Do(r.Context(), lang); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - } - - if res, err := resourcer.Do(r.Context(), r.URL.Path); err == nil { + if res, err := resourcer.Do(r.Context(), tail); err == nil { // TODO(toby3d) : ugly workaround, must be refactored resFile, err := contentDir.Open(res.File.Filename()) if err != nil { @@ -188,42 +151,10 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) { return } - // NOTE(toby3d): search entry for requested URL and language - // code in subdir. - e, err := entrier.Do(r.Context(), lang, tail) - if err != nil { - if errors.Is(err, entry.ErrNotExist) { - http.NotFound(w, r) - } else { - http.Error(w, err.Error(), http.StatusInternalServerError) - } - - return - } - - // NOTE(toby3d): wrap founded entry into theme template and - // answer to client. - contentLanguage := make([]string, len(e.Translations)) - for i := range e.Translations { - contentLanguage[i] = e.Translations[i].Language.Code() - } - - w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", ")) - - template, err := themer.Do(r.Context(), s, e) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - - return - } - - w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8) - if err = template(w); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + entryHandler.ServeHTTP(w, r) }) chain := middleware.Chain{ - middleware.LogFmt(), + // middleware.LogFmt(), middleware.Redirect(middleware.RedirectConfig{Serverer: serverer}), middleware.Header(middleware.HeaderConfig{Serverer: serverer}), } From 8e84569af8f8605f593e750eef741cb199de1262 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 14 Feb 2024 21:24:42 +0600 Subject: [PATCH 04/14] :art: Format static use case method code --- internal/static/usecase/static_ucase.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/static/usecase/static_ucase.go b/internal/static/usecase/static_ucase.go index 68c8ff6..fa91c4a 100644 --- a/internal/static/usecase/static_ucase.go +++ b/internal/static/usecase/static_ucase.go @@ -21,9 +21,7 @@ func NewStaticUseCase(store static.Repository) static.UseCase { } func (ucase *staticUseCase) Do(ctx context.Context, p string) (*domain.Static, error) { - p = strings.TrimPrefix(path.Clean(p), "/") - - s, err := ucase.store.Get(ctx, p) + s, err := ucase.store.Get(ctx, strings.TrimPrefix(path.Clean(p), "/")) if err != nil { return nil, fmt.Errorf("cannot get static file: %w", err) } From 1ee30e4d18af7eb747e44428f486f77371f6ffe6 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Wed, 14 Feb 2024 21:25:09 +0600 Subject: [PATCH 05/14] :pushpin: Upgraded and vendored go modules --- .../~mariusor/go-xsd-duration/LICENSE | 21 + .../~mariusor/go-xsd-duration/xsd_duration.go | 244 +++ .../github.com/go-ap/activitypub/.build.yml | 15 + .../github.com/go-ap/activitypub/.gitignore | 14 + vendor/github.com/go-ap/activitypub/LICENSE | 21 + vendor/github.com/go-ap/activitypub/Makefile | 29 + vendor/github.com/go-ap/activitypub/README.md | 80 + .../github.com/go-ap/activitypub/activity.go | 950 ++++++++++++ vendor/github.com/go-ap/activitypub/actor.go | 622 ++++++++ .../go-ap/activitypub/collection.go | 438 ++++++ .../go-ap/activitypub/collection_page.go | 435 ++++++ vendor/github.com/go-ap/activitypub/copy.go | 234 +++ .../go-ap/activitypub/decoding_gob.go | 722 +++++++++ .../go-ap/activitypub/decoding_json.go | 662 ++++++++ .../go-ap/activitypub/encoding_gob.go | 792 ++++++++++ .../go-ap/activitypub/encoding_json.go | 387 +++++ .../github.com/go-ap/activitypub/flatten.go | 127 ++ .../github.com/go-ap/activitypub/helpers.go | 532 +++++++ .../activitypub/intransitive_activity.go | 384 +++++ vendor/github.com/go-ap/activitypub/iri.go | 429 +++++ vendor/github.com/go-ap/activitypub/item.go | 162 ++ .../go-ap/activitypub/item_collection.go | 290 ++++ vendor/github.com/go-ap/activitypub/link.go | 165 ++ .../activitypub/natural_language_values.go | 812 ++++++++++ vendor/github.com/go-ap/activitypub/object.go | 999 ++++++++++++ .../github.com/go-ap/activitypub/object_id.go | 20 + .../go-ap/activitypub/ordered_collection.go | 427 +++++ .../activitypub/ordered_collection_page.go | 393 +++++ vendor/github.com/go-ap/activitypub/place.go | 310 ++++ .../github.com/go-ap/activitypub/profile.go | 282 ++++ .../github.com/go-ap/activitypub/question.go | 276 ++++ .../go-ap/activitypub/relationship.go | 290 ++++ .../github.com/go-ap/activitypub/tombstone.go | 290 ++++ vendor/github.com/go-ap/activitypub/typer.go | 397 +++++ vendor/github.com/go-ap/activitypub/types.go | 66 + .../go-ap/activitypub/validation.go | 16 + vendor/github.com/go-ap/errors/.build.yml | 16 + vendor/github.com/go-ap/errors/.gitignore | 14 + vendor/github.com/go-ap/errors/LICENSE | 21 + vendor/github.com/go-ap/errors/Makefile | 22 + vendor/github.com/go-ap/errors/README.md | 6 + vendor/github.com/go-ap/errors/decoding.go | 82 + vendor/github.com/go-ap/errors/errors.go | 165 ++ vendor/github.com/go-ap/errors/http.go | 944 +++++++++++ vendor/github.com/go-ap/errors/stack.go | 219 +++ vendor/github.com/go-ap/jsonld/.build.yml | 18 + vendor/github.com/go-ap/jsonld/.gitignore | 14 + vendor/github.com/go-ap/jsonld/LICENSE | 21 + vendor/github.com/go-ap/jsonld/Makefile | 18 + vendor/github.com/go-ap/jsonld/README.md | 15 + vendor/github.com/go-ap/jsonld/context.go | 219 +++ vendor/github.com/go-ap/jsonld/decode.go | 1253 +++++++++++++++ vendor/github.com/go-ap/jsonld/encode.go | 1377 +++++++++++++++++ vendor/github.com/go-ap/jsonld/fold.go | 143 ++ vendor/github.com/go-ap/jsonld/scanner.go | 632 ++++++++ vendor/github.com/go-ap/jsonld/tables.go | 218 +++ vendor/github.com/go-ap/jsonld/tags.go | 44 + vendor/github.com/valyala/fastjson/.gitignore | 1 + .../github.com/valyala/fastjson/.travis.yml | 19 + vendor/github.com/valyala/fastjson/LICENSE | 22 + vendor/github.com/valyala/fastjson/README.md | 227 +++ vendor/github.com/valyala/fastjson/arena.go | 126 ++ vendor/github.com/valyala/fastjson/doc.go | 9 + .../valyala/fastjson/fastfloat/parse.go | 515 ++++++ vendor/github.com/valyala/fastjson/fuzz.go | 22 + vendor/github.com/valyala/fastjson/handy.go | 170 ++ vendor/github.com/valyala/fastjson/parser.go | 976 ++++++++++++ vendor/github.com/valyala/fastjson/pool.go | 52 + vendor/github.com/valyala/fastjson/scanner.go | 94 ++ vendor/github.com/valyala/fastjson/update.go | 110 ++ vendor/github.com/valyala/fastjson/util.go | 30 + .../github.com/valyala/fastjson/validate.go | 308 ++++ vendor/modules.txt | 16 + 73 files changed, 20491 insertions(+) create mode 100644 vendor/git.sr.ht/~mariusor/go-xsd-duration/LICENSE create mode 100644 vendor/git.sr.ht/~mariusor/go-xsd-duration/xsd_duration.go create mode 100644 vendor/github.com/go-ap/activitypub/.build.yml create mode 100644 vendor/github.com/go-ap/activitypub/.gitignore create mode 100644 vendor/github.com/go-ap/activitypub/LICENSE create mode 100644 vendor/github.com/go-ap/activitypub/Makefile create mode 100644 vendor/github.com/go-ap/activitypub/README.md create mode 100644 vendor/github.com/go-ap/activitypub/activity.go create mode 100644 vendor/github.com/go-ap/activitypub/actor.go create mode 100644 vendor/github.com/go-ap/activitypub/collection.go create mode 100644 vendor/github.com/go-ap/activitypub/collection_page.go create mode 100644 vendor/github.com/go-ap/activitypub/copy.go create mode 100644 vendor/github.com/go-ap/activitypub/decoding_gob.go create mode 100644 vendor/github.com/go-ap/activitypub/decoding_json.go create mode 100644 vendor/github.com/go-ap/activitypub/encoding_gob.go create mode 100644 vendor/github.com/go-ap/activitypub/encoding_json.go create mode 100644 vendor/github.com/go-ap/activitypub/flatten.go create mode 100644 vendor/github.com/go-ap/activitypub/helpers.go create mode 100644 vendor/github.com/go-ap/activitypub/intransitive_activity.go create mode 100644 vendor/github.com/go-ap/activitypub/iri.go create mode 100644 vendor/github.com/go-ap/activitypub/item.go create mode 100644 vendor/github.com/go-ap/activitypub/item_collection.go create mode 100644 vendor/github.com/go-ap/activitypub/link.go create mode 100644 vendor/github.com/go-ap/activitypub/natural_language_values.go create mode 100644 vendor/github.com/go-ap/activitypub/object.go create mode 100644 vendor/github.com/go-ap/activitypub/object_id.go create mode 100644 vendor/github.com/go-ap/activitypub/ordered_collection.go create mode 100644 vendor/github.com/go-ap/activitypub/ordered_collection_page.go create mode 100644 vendor/github.com/go-ap/activitypub/place.go create mode 100644 vendor/github.com/go-ap/activitypub/profile.go create mode 100644 vendor/github.com/go-ap/activitypub/question.go create mode 100644 vendor/github.com/go-ap/activitypub/relationship.go create mode 100644 vendor/github.com/go-ap/activitypub/tombstone.go create mode 100644 vendor/github.com/go-ap/activitypub/typer.go create mode 100644 vendor/github.com/go-ap/activitypub/types.go create mode 100644 vendor/github.com/go-ap/activitypub/validation.go create mode 100644 vendor/github.com/go-ap/errors/.build.yml create mode 100644 vendor/github.com/go-ap/errors/.gitignore create mode 100644 vendor/github.com/go-ap/errors/LICENSE create mode 100644 vendor/github.com/go-ap/errors/Makefile create mode 100644 vendor/github.com/go-ap/errors/README.md create mode 100644 vendor/github.com/go-ap/errors/decoding.go create mode 100644 vendor/github.com/go-ap/errors/errors.go create mode 100644 vendor/github.com/go-ap/errors/http.go create mode 100644 vendor/github.com/go-ap/errors/stack.go create mode 100644 vendor/github.com/go-ap/jsonld/.build.yml create mode 100644 vendor/github.com/go-ap/jsonld/.gitignore create mode 100644 vendor/github.com/go-ap/jsonld/LICENSE create mode 100644 vendor/github.com/go-ap/jsonld/Makefile create mode 100644 vendor/github.com/go-ap/jsonld/README.md create mode 100644 vendor/github.com/go-ap/jsonld/context.go create mode 100644 vendor/github.com/go-ap/jsonld/decode.go create mode 100644 vendor/github.com/go-ap/jsonld/encode.go create mode 100644 vendor/github.com/go-ap/jsonld/fold.go create mode 100644 vendor/github.com/go-ap/jsonld/scanner.go create mode 100644 vendor/github.com/go-ap/jsonld/tables.go create mode 100644 vendor/github.com/go-ap/jsonld/tags.go create mode 100644 vendor/github.com/valyala/fastjson/.gitignore create mode 100644 vendor/github.com/valyala/fastjson/.travis.yml create mode 100644 vendor/github.com/valyala/fastjson/LICENSE create mode 100644 vendor/github.com/valyala/fastjson/README.md create mode 100644 vendor/github.com/valyala/fastjson/arena.go create mode 100644 vendor/github.com/valyala/fastjson/doc.go create mode 100644 vendor/github.com/valyala/fastjson/fastfloat/parse.go create mode 100644 vendor/github.com/valyala/fastjson/fuzz.go create mode 100644 vendor/github.com/valyala/fastjson/handy.go create mode 100644 vendor/github.com/valyala/fastjson/parser.go create mode 100644 vendor/github.com/valyala/fastjson/pool.go create mode 100644 vendor/github.com/valyala/fastjson/scanner.go create mode 100644 vendor/github.com/valyala/fastjson/update.go create mode 100644 vendor/github.com/valyala/fastjson/util.go create mode 100644 vendor/github.com/valyala/fastjson/validate.go diff --git a/vendor/git.sr.ht/~mariusor/go-xsd-duration/LICENSE b/vendor/git.sr.ht/~mariusor/go-xsd-duration/LICENSE new file mode 100644 index 0000000..24f6a9e --- /dev/null +++ b/vendor/git.sr.ht/~mariusor/go-xsd-duration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Go xsd:duration + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/git.sr.ht/~mariusor/go-xsd-duration/xsd_duration.go b/vendor/git.sr.ht/~mariusor/go-xsd-duration/xsd_duration.go new file mode 100644 index 0000000..1027350 --- /dev/null +++ b/vendor/git.sr.ht/~mariusor/go-xsd-duration/xsd_duration.go @@ -0,0 +1,244 @@ +// Handles xsd:duration to time.Duration conversion + +package xsd + +import ( + "bytes" + "fmt" + "strconv" + "time" +) + +// Extending time constants with the values for day, week, month, year +const ( + Day = time.Hour * 24 + // These values are not precise, probably why the time package does not implement them + // We need them here because xsd:duration uses them + Monthish = Day * 30 + Yearish = Day * 356 +) + +// durationPair holds information about a pair of (uint/ufloat)(PeriodTag) values in a xsd:duration string +type durationPair struct { + v time.Duration + typ byte +} + +func getTimeBaseDuration(b byte) time.Duration { + switch b { + case tagHour: + return time.Hour + case tagMinute: + return time.Minute + case tagSecond: + return time.Second + } + return 0 +} + +func getDateBaseDuration(b byte) time.Duration { + switch b { + case tagYear: + return Yearish + case tagMonth: + return Monthish + case tagDay: + return Day + } + return 0 +} + +// validByteForFloats checks +func validByteForFloats(b byte) bool { + // + , - . 0 1 2 3 4 5 6 7 8 9 + return (b >= 43 && b <= 46) || (b >= 48 && b <= 57) +} + +func parseTagWithValue(data []byte, start, tagPos int, isTime bool) (*durationPair, error) { + d := new(durationPair) + d.typ = data[tagPos] + if d.typ == tagSecond { + // seconds can be represented in float, we need to parse accordingly + if v, err := strconv.ParseFloat(string(data[start:tagPos]), 32); err == nil { + d.v = time.Duration(float64(time.Second) * v) + } + } else { + if v, err := strconv.ParseInt(string(data[start:tagPos]), 10, 32); err == nil { + if isTime { + d.v = getTimeBaseDuration(d.typ) * time.Duration(v) + } else { + d.v = getDateBaseDuration(d.typ) * time.Duration(v) + } + } + } + if d.v < 0 { + return nil, fmt.Errorf("the minus sign must appear first in the duration") + } + return d, nil +} + +// loadUintVal receives the data and position at which to try to load the duration element +// isTime is used for distinguishing between P1M - 1 month, and PT1M - 1 minute +// it returns a duration pair if it can read one at the start of data, +// and the number of bytes read from data +func loadUintVal(data []byte, start int, isTime bool) (*durationPair, int, error) { + for i := start; i < len(data); i++ { + chr := data[i] + if validTag(data[i]) { + d, err := parseTagWithValue(data, start, i, isTime) + return d, i, err + } + if validByteForFloats(chr) { + continue + } + return nil, i, fmt.Errorf("invalid character %c at pos %d", chr, i) + } + return nil, len(data), fmt.Errorf("unable to recognize any duration value") +} + +const ( + tagDuration = 'P' + tagTime = 'T' + tagYear = 'Y' + tagMonth = 'M' + tagDay = 'D' + tagHour = 'H' + tagMinute = 'M' + tagSecond = 'S' +) + +func validTag(b byte) bool { + return b == tagYear || b == tagMonth || b == tagDay || b == tagHour || b == tagMinute || b == tagSecond +} + +// Unmarshal takes a byte array and unmarshals it to a time.Duration value +// It is used to parse values in the following format: -PuYuMuDTuHuMufS, where: +// * - shows if the duration is negative +// * P is the duration tag +// * T is the time tag separator +// * Y,M,D,H,M,S are tags for year, month, day, hour, minute, second values +// * u is an unsigned integer value +// * uf is an unsigned float value (just for seconds) +func Unmarshal(data []byte, d *time.Duration) error { + if len(data) == 0 { + return fmt.Errorf("invalid xsd:duration: empty value") + } + pos := 0 + // loading if the value is negative + negative := data[pos] == '-' + if negative { + // skipping over the minus + pos++ + } + if data[pos] != tagDuration { + return fmt.Errorf("invalid xsd:duration: first character must be %q", 'P') + } + // skipping over the "P" + pos++ + + onePastEnd := len(data) + if pos >= onePastEnd { + return fmt.Errorf("invalid xsd:duration: at least one number and designator are required") + } + isTime := false + duration := time.Duration(0) + for { + if data[pos] == tagTime { + pos++ + isTime = true + } + p, cnt, err := loadUintVal(data, pos, isTime) + if err != nil { + return fmt.Errorf("invalid xsd:duration: %w", err) + } + duration += p.v + pos = cnt + 1 + if pos+1 >= onePastEnd { + break + } + } + if negative { + duration *= -1 + } + if onePastEnd > pos+1 { + return fmt.Errorf("data contains more bytes than we are able to parse") + } + if d == nil { + return fmt.Errorf("unable to store time.Duration to nil pointer") + } + *d = duration + return nil +} + +func Days(d time.Duration) float64 { + dd := d / Day + h := d % Day + return float64(dd) + float64(h)/(24*60*60*1e9) +} + +func Months(d time.Duration) float64 { + m := d / Monthish + w := d % Monthish + return float64(m) + float64(w)/(4*7*24*60*60*1e9) +} + +func Years(d time.Duration) float64 { + y := d / Yearish + m := d % Yearish + return float64(y) + float64(m)/(12*4*7*24*60*60*1e9) +} + +func Marshal(d time.Duration) ([]byte, error) { + if d == 0 { + return []byte{tagDuration, tagTime, '0', tagSecond}, nil + } + + neg := d < 0 + if neg { + d = -d + } + y := Years(d) + d -= time.Duration(y) * Yearish + m := Months(d) + d -= time.Duration(m) * Monthish + dd := Days(d) + d -= time.Duration(dd) * Day + H := d.Hours() + d -= time.Duration(H) * time.Hour + M := d.Minutes() + d -= time.Duration(M) * time.Minute + s := d.Seconds() + d -= time.Duration(s) * time.Second + b := bytes.Buffer{} + if neg { + b.Write([]byte{'-'}) + } + b.Write([]byte{'P'}) + if int(y) > 0 { + b.WriteString(fmt.Sprintf("%d%c", int64(y), tagYear)) + } + if int(m) > 0 { + b.WriteString(fmt.Sprintf("%d%c", int64(m), tagMonth)) + } + if int(dd) > 0 { + b.WriteString(fmt.Sprintf("%d%c", int64(dd), tagDay)) + } + + if H+M+s > 0 { + b.Write([]byte{tagTime}) + if int(H) > 0 { + b.WriteString(fmt.Sprintf("%d%c", int64(H), tagHour)) + } + if int(M) > 0 { + b.WriteString(fmt.Sprintf("%d%c", int64(M), tagMinute)) + } + if int(s) > 0 { + if s-float64(int(s)) < 0.01 { + b.WriteString(fmt.Sprintf("%d%c", int(s), tagSecond)) + } else { + b.WriteString(fmt.Sprintf("%.1f%c", s, tagSecond)) + } + } + } + return b.Bytes(), nil +} diff --git a/vendor/github.com/go-ap/activitypub/.build.yml b/vendor/github.com/go-ap/activitypub/.build.yml new file mode 100644 index 0000000..c5e57f1 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/.build.yml @@ -0,0 +1,15 @@ +image: archlinux +packages: + - go +sources: + - https://github.com/go-ap/activitypub +environment: + GO111MODULE: 'on' +tasks: + - tests: | + cd activitypub + make test + make TEST_TARGET=./tests test + - coverage: | + set -a +x + cd activitypub && make coverage diff --git a/vendor/github.com/go-ap/activitypub/.gitignore b/vendor/github.com/go-ap/activitypub/.gitignore new file mode 100644 index 0000000..42d9ac4 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/.gitignore @@ -0,0 +1,14 @@ +# Gogland +.idea/ + +# Binaries for programs and plugins +*.so + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tools +*.out +*.coverprofile + +*pkg diff --git a/vendor/github.com/go-ap/activitypub/LICENSE b/vendor/github.com/go-ap/activitypub/LICENSE new file mode 100644 index 0000000..be7ff3e --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Golang ActitvityPub + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/go-ap/activitypub/Makefile b/vendor/github.com/go-ap/activitypub/Makefile new file mode 100644 index 0000000..bacea87 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/Makefile @@ -0,0 +1,29 @@ +SHELL := bash +.ONESHELL: +.SHELLFLAGS := -eu -o pipefail -c +.DELETE_ON_ERROR: +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +GO ?= go +TEST := $(GO) test +TEST_FLAGS ?= -v +TEST_TARGET ?= . +GO111MODULE = on +PROJECT_NAME := $(shell basename $(PWD)) + +.PHONY: test coverage clean download + +download: + $(GO) mod download all + $(GO) mod tidy + +test: download + $(TEST) $(TEST_FLAGS) $(TEST_TARGET) + +coverage: TEST_TARGET := . +coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile +coverage: test + +clean: + $(RM) -v *.coverprofile diff --git a/vendor/github.com/go-ap/activitypub/README.md b/vendor/github.com/go-ap/activitypub/README.md new file mode 100644 index 0000000..0b611ef --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/README.md @@ -0,0 +1,80 @@ +# About GoActivityPub: Vocabulary + +[![MIT Licensed](https://img.shields.io/github/license/go-ap/activitypub.svg)](https://raw.githubusercontent.com/go-ap/activitypub/master/LICENSE) +[![Build Status](https://builds.sr.ht/~mariusor/activitypub.svg)](https://builds.sr.ht/~mariusor/activitypub) +[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/activitypub.svg)](https://codecov.io/gh/go-ap/activitypub) +[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/activitypub)](https://goreportcard.com/report/github.com/go-ap/activitypub) + +This project is part of the [GoActivityPub](https://github.com/go-ap) library which helps with creating ActivityPub applications using the Go programming language. + +It contains data types for most of the [Activity Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) and the [ActivityPub](https://www.w3.org/TR/activitypub/) extension. +They are documented accordingly with annotations from these specifications. + +You can find an expanded documentation about the whole library [on SourceHut](https://man.sr.ht/~mariusor/go-activitypub/go-ap/index.md). + +For discussions about the projects you can write to the discussions mailing list: [~mariusor/go-activitypub-discuss@lists.sr.ht](mailto:~mariusor/go-activitypub-discuss@lists.sr.ht) + +For patches and bug reports please use the dev mailing list: [~mariusor/go-activitypub-dev@lists.sr.ht](mailto:~mariusor/go-activitypub-dev@lists.sr.ht) + +## Usage + +```go +import vocab "github.com/go-ap/activitypub" + +follow := vocab.Activity{ + Type: vocab.FollowType, + Actor: vocab.IRI("https://example.com/alice"), + Object: vocab.IRI("https://example.com/janedoe"), +} + +``` + +## Note about generics + +The module contains helper functions which make it simpler to deal with the `vocab.Item` +interfaces and they come in two flavours: explicit `OnXXX` and `ToXXX` functions corresponding +to each type and, a generic pair of functions `On[T]` and `To[T]`. + +```go +import ( + "fmt" + + vocab "github.com/go-ap/activitypub" +) + +var it vocab.Item = ... // an ActivityPub object unmarshaled from a request + +err := vocab.OnActivity(it, func(act *vocab.Activity) error { + if vocab.ContentManagementActivityTypes.Contains(act.Type) { + fmt.Printf("This is a Content Management type activity: %q", act.Type) + } + return nil +}) + +err := vocab.On[vocab.Activity](it, func(act *vocab.Activity) error { + if vocab.ReactionsActivityTypes.Contains(act.Type) { + fmt.Printf("This is a Reaction type activity: %q", act.Type) + } + return nil +}) + +``` + +Before using the generic versions you should consider that they come with a pretty heavy performance penalty: + +``` +goos: linux +goarch: amd64 +pkg: github.com/go-ap/activitypub +cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz +Benchmark_OnT_vs_On_T/OnObject-8 752387791 1.633 ns/op +Benchmark_OnT_vs_On_T/On_T_Object-8 4656264 261.8 ns/op +Benchmark_OnT_vs_On_T/OnActor-8 739833261 1.596 ns/op +Benchmark_OnT_vs_On_T/On_T_Actor-8 4035148 301.9 ns/op +Benchmark_OnT_vs_On_T/OnActivity-8 751173854 1.604 ns/op +Benchmark_OnT_vs_On_T/On_T_Activity-8 4062598 285.9 ns/op +Benchmark_OnT_vs_On_T/OnIntransitiveActivity-8 675824500 1.640 ns/op +Benchmark_OnT_vs_On_T/On_T_IntransitiveActivity-8 4372798 274.1 ns/op +PASS +ok github.com/go-ap/activitypub 11.350s +``` diff --git a/vendor/github.com/go-ap/activitypub/activity.go b/vendor/github.com/go-ap/activitypub/activity.go new file mode 100644 index 0000000..2a79643 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/activity.go @@ -0,0 +1,950 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "reflect" + "strings" + "time" + + "github.com/valyala/fastjson" +) + +// Activity Types +const ( + AcceptType ActivityVocabularyType = "Accept" + AddType ActivityVocabularyType = "Add" + AnnounceType ActivityVocabularyType = "Announce" + ArriveType ActivityVocabularyType = "Arrive" + BlockType ActivityVocabularyType = "Block" + CreateType ActivityVocabularyType = "Create" + DeleteType ActivityVocabularyType = "Delete" + DislikeType ActivityVocabularyType = "Dislike" + FlagType ActivityVocabularyType = "Flag" + FollowType ActivityVocabularyType = "Follow" + IgnoreType ActivityVocabularyType = "Ignore" + InviteType ActivityVocabularyType = "Invite" + JoinType ActivityVocabularyType = "Join" + LeaveType ActivityVocabularyType = "Leave" + LikeType ActivityVocabularyType = "Like" + ListenType ActivityVocabularyType = "Listen" + MoveType ActivityVocabularyType = "Move" + OfferType ActivityVocabularyType = "Offer" + QuestionType ActivityVocabularyType = "Question" + RejectType ActivityVocabularyType = "Reject" + ReadType ActivityVocabularyType = "Read" + RemoveType ActivityVocabularyType = "Remove" + TentativeRejectType ActivityVocabularyType = "TentativeReject" + TentativeAcceptType ActivityVocabularyType = "TentativeAccept" + TravelType ActivityVocabularyType = "Travel" + UndoType ActivityVocabularyType = "Undo" + UpdateType ActivityVocabularyType = "Update" + ViewType ActivityVocabularyType = "View" +) + +func (a ActivityVocabularyTypes) Contains(typ ActivityVocabularyType) bool { + for _, v := range a { + if strings.EqualFold(string(v), string(typ)) { + return true + } + } + return false +} + +// ContentManagementActivityTypes use case primarily deals with activities that involve the creation, modification or +// deletion of content. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-crud +// +// This includes, for instance, activities such as "John created a new note", "Sally updated an article", and +// "Joe deleted the photo". +var ContentManagementActivityTypes = ActivityVocabularyTypes{ + CreateType, + DeleteType, + UpdateType, +} + +// CollectionManagementActivityTypes use case primarily deals with activities involving the management of content within +// collections. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-collection +// +// Examples of collections include things like folders, albums, friend lists, etc. +// This includes, for instance, activities such as "Sally added a file to Folder A", "John moved the file from Folder A +// to Folder B", etc. +var CollectionManagementActivityTypes = ActivityVocabularyTypes{ + AddType, + MoveType, + RemoveType, +} + +// ReactionsActivityTypes use case primarily deals with reactions to content. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-reactions +// +// This can include activities such as liking or disliking content, ignoring updates, flagging content as being +// inappropriate, accepting or rejecting objects, etc. +var ReactionsActivityTypes = ActivityVocabularyTypes{ + AcceptType, + BlockType, + DislikeType, + FlagType, + IgnoreType, + LikeType, + RejectType, + TentativeAcceptType, + TentativeRejectType, +} + +// EventRSVPActivityTypes use case primarily deals with invitations to events and RSVP type responses. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-rsvp +var EventRSVPActivityTypes = ActivityVocabularyTypes{ + AcceptType, + IgnoreType, + InviteType, + RejectType, + TentativeAcceptType, + TentativeRejectType, +} + +// GroupManagementActivityTypes use case primarily deals with management of groups. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-group +// +// It can include, for instance, activities such as "John added Sally to Group A", "Sally joined Group A", +// "Joe left Group A", etc. +var GroupManagementActivityTypes = ActivityVocabularyTypes{ + AddType, + JoinType, + LeaveType, + RemoveType, +} + +// ContentExperienceActivityTypes use case primarily deals with describing activities involving listening to, reading, +// or viewing content. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-experience +// +// For instance, "Sally read the article", "Joe listened to the song". +var ContentExperienceActivityTypes = ActivityVocabularyTypes{ + ListenType, + ReadType, + ViewType, +} + +// GeoSocialEventsActivityTypes use case primarily deals with activities involving geo-tagging type activities. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-geo +// +// For instance, it can include activities such as "Joe arrived at work", "Sally left work", and +// "John is travel from home to work". +var GeoSocialEventsActivityTypes = ActivityVocabularyTypes{ + ArriveType, + LeaveType, + TravelType, +} + +// NotificationActivityTypes use case primarily deals with calling attention to particular objects or notifications. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-notification +var NotificationActivityTypes = ActivityVocabularyTypes{ + AnnounceType, +} + +// QuestionActivityTypes use case primarily deals with representing inquiries of any type. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-questions +// +// See 5.4 Representing Questions for more information. +// https://www.w3.org/TR/activitystreams-vocabulary/#questions +var QuestionActivityTypes = ActivityVocabularyTypes{ + QuestionType, +} + +// RelationshipManagementActivityTypes use case primarily deals with representing activities involving the management of +// interpersonal and social relationships +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-relationships +// +// (e.g. friend requests, management of social network, etc). See 5.2 Representing Relationships Between Entities +// for more information. +// https://www.w3.org/TR/activitystreams-vocabulary/#connections +var RelationshipManagementActivityTypes = ActivityVocabularyTypes{ + AcceptType, + AddType, + BlockType, + CreateType, + DeleteType, + FollowType, + IgnoreType, + InviteType, + RejectType, +} + +// NegatingActivityTypes use case primarily deals with the ability to redact previously completed activities. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-undo +// +// See 5.5 Inverse Activities and "Undo" for more information. +// https://www.w3.org/TR/activitystreams-vocabulary/#inverse +var NegatingActivityTypes = ActivityVocabularyTypes{ + UndoType, +} + +// OffersActivityTypes use case deals with activities involving offering one object to another. +// +// https://www.w3.org/TR/activitystreams-vocabulary/#motivations-offers +// +// It can include, for instance, activities such as "Company A is offering a discount on purchase of Product Z to Sally", +// "Sally is offering to add a File to Folder A", etc. +var OffersActivityTypes = ActivityVocabularyTypes{ + OfferType, +} + +var IntransitiveActivityTypes = ActivityVocabularyTypes{ + ArriveType, + TravelType, + QuestionType, +} + +var ActivityTypes = ActivityVocabularyTypes{ + AcceptType, + AddType, + AnnounceType, + BlockType, + CreateType, + DeleteType, + DislikeType, + FlagType, + FollowType, + IgnoreType, + InviteType, + JoinType, + LeaveType, + LikeType, + ListenType, + MoveType, + OfferType, + RejectType, + ReadType, + RemoveType, + TentativeRejectType, + TentativeAcceptType, + UndoType, + UpdateType, + ViewType, +} + +// HasRecipients is an interface implemented by activities to return their audience +// for further propagation +// +// Please take care to the fact that the de-duplication functionality requires a pointer receiver +// therefore a valid Item interface that wraps around an Object struct, can not be type asserted +// to HasRecipients. +type HasRecipients interface { + // Recipients is a method that should do a recipients de-duplication step and then return + // the remaining recipients. + Recipients() ItemCollection + // Clean is a method that removes BCC/Bto recipients in preparation for public consumption of + // the Object. + Clean() +} + +type Activities interface { + Activity +} + +// Activity is a subtype of Object that describes some form of action that may happen, +// is currently happening, or has already happened. +// The Activity type itself serves as an abstract base type for all types of activities. +// It is important to note that the Activity type itself does not carry any specific semantics +// about the kind of action being taken. +type Activity struct { + // ID provides the globally unique identifier for anActivity Pub Object or Link. + ID ID `jsonld:"id,omitempty"` + // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // Name a simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // Attachment identifies a resource attached or related to an object that potentially requires special handling. + // The intent is to provide a model that is at least semantically similar to attachments in email. + Attachment Item `jsonld:"attachment,omitempty"` + // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. + // For instance, an object might be attributed to the completion of another activity. + AttributedTo Item `jsonld:"attributedTo,omitempty"` + // Audience identifies one or more entities that represent the total population of entities + // for which the object can considered to be relevant. + Audience ItemCollection `jsonld:"audience,omitempty"` + // Content or textual representation of the Activity Pub Object encoded as a JSON string. + // By default, the value of content is HTML. + // The mediaType property can be used in the object to indicate a different content type. + // (The content MAY be expressed using multiple language-tagged values.) + Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` + // Context identifies the context within which the object exists or an activity was performed. + // The notion of "context" used is intentionally vague. + // The intended function is to serve as a means of grouping objects and activities that share a + // common originating context or purpose. An example could be all activities relating to a common project or event. + Context Item `jsonld:"context,omitempty"` + // MediaType when used on an Object, identifies the MIME media type of the value of the content property. + // If not specified, the content property is assumed to contain text/html content. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // EndTime the date and time describing the actual or expected ending time of the object. + // When used with an Activity object, for instance, the endTime property specifies the moment + // the activity concluded or is expected to conclude. + EndTime time.Time `jsonld:"endTime,omitempty"` + // Generator identifies the entity (e.g. an application) that generated the object. + Generator Item `jsonld:"generator,omitempty"` + // Icon indicates an entity that describes an icon for this object. + // The image should have an aspect ratio of one (horizontal) to one (vertical) + // and should be suitable for presentation at a small size. + Icon Item `jsonld:"icon,omitempty"` + // Image indicates an entity that describes an image for this object. + // Unlike the icon property, there are no aspect ratio or display size limitations assumed. + Image Item `jsonld:"image,omitempty"` + // InReplyTo indicates one or more entities for which this object is considered a response. + InReplyTo Item `jsonld:"inReplyTo,omitempty"` + // Location indicates one or more physical or logical locations associated with the object. + Location Item `jsonld:"location,omitempty"` + // Preview identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // Published the date and time at which the object was published + Published time.Time `jsonld:"published,omitempty"` + // Replies identifies a Collection containing objects considered to be responses to this object. + Replies Item `jsonld:"replies,omitempty"` + // StartTime the date and time describing the actual or expected starting time of the object. + // When used with an Activity object, for instance, the startTime property specifies + // the moment the activity began or is scheduled to begin. + StartTime time.Time `jsonld:"startTime,omitempty"` + // Summary a natural language summarization of the object encoded as HTML. + // *Multiple language tagged summaries may be provided.) + Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` + // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. + // The key difference between attachment and tag is that the former implies association by inclusion, + // while the latter implies associated by reference. + Tag ItemCollection `jsonld:"tag,omitempty"` + // Updated the date and time at which the object was updated + Updated time.Time `jsonld:"updated,omitempty"` + // URL identifies one or more links to representations of the object + URL Item `jsonld:"url,omitempty"` + // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object + To ItemCollection `jsonld:"to,omitempty"` + // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. + Bto ItemCollection `jsonld:"bto,omitempty"` + // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. + CC ItemCollection `jsonld:"cc,omitempty"` + // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. + BCC ItemCollection `jsonld:"bcc,omitempty"` + // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, + // the duration property indicates the object's approximate duration. + // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], + // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). + Duration time.Duration `jsonld:"duration,omitempty"` + // This is a list of all Like activities with this object as the object property, added as a side effect. + // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Likes Item `jsonld:"likes,omitempty"` + // This is a list of all Announce activities with this object as the object property, added as a side effect. + // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Shares Item `jsonld:"shares,omitempty"` + // Source property is intended to convey some sort of source from which the content markup was derived, + // as a form of provenance, or to support future editing by clients. + // In general, clients do the conversion from source to content, not the other way around. + Source Source `jsonld:"source,omitempty"` + // CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity. + // Any single activity can have multiple actors. The actor may be specified using an indirect Link. + Actor Item `jsonld:"actor,omitempty"` + // Target describes the indirect object, or target, of the activity. + // The precise meaning of the target is largely dependent on the type of action being described + // but will often be the object of the English preposition "to". + // For instance, in the activity "John added a movie to his wishlist", + // the target of the activity is John's wishlist. An activity can have more than one target. + Target Item `jsonld:"target,omitempty"` + // Result describes the result of the activity. For instance, if a particular action results in the creation + // of a new resource, the result property can be used to describe that new resource. + Result Item `jsonld:"result,omitempty"` + // Origin describes an indirect object of the activity from which the activity is directed. + // The precise meaning of the origin is the object of the English preposition "from". + // For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A". + Origin Item `jsonld:"origin,omitempty"` + // Instrument identifies one or more objects used (or to be used) in the completion of an Activity. + Instrument Item `jsonld:"instrument,omitempty"` + // Object When used within an Activity, describes the direct object of the activity. + // For instance, in the activity "John added a movie to his wishlist", + // the object of the activity is the movie added. + // When used within a Relationship describes the entity to which the subject is related. + Object Item `jsonld:"object,omitempty"` +} + +// GetType returns the ActivityVocabulary type of the current Activity +func (a Activity) GetType() ActivityVocabularyType { + return a.Type +} + +// IsLink returns false for Activity objects +func (a Activity) IsLink() bool { + return false +} + +// GetID returns the ID corresponding to the Activity object +func (a Activity) GetID() ID { + return a.ID +} + +// GetLink returns the IRI corresponding to the Activity object +func (a Activity) GetLink() IRI { + return IRI(a.ID) +} + +// IsObject returns true for Activity objects +func (a Activity) IsObject() bool { + return true +} + +// IsCollection returns false for Activity objects +func (a Activity) IsCollection() bool { + return false +} + +func removeFromCollection(col ItemCollection, items ...Item) ItemCollection { + result := make(ItemCollection, 0) + if len(items) == 0 { + return col + } + for _, ob := range col { + found := false + for _, it := range items { + if ob.GetID().Equals(it.GetID(), false) { + found = true + break + } + } + if !found { + result = append(result, ob) + } + } + return result +} + +func removeFromAudience(a *Activity, items ...Item) error { + if a.To != nil { + a.To = removeFromCollection(a.To, items...) + } + if a.Bto != nil { + a.Bto = removeFromCollection(a.Bto, items...) + } + if a.CC != nil { + a.CC = removeFromCollection(a.CC, items...) + } + if a.BCC != nil { + a.BCC = removeFromCollection(a.BCC, items...) + } + if a.Audience != nil { + a.Audience = removeFromCollection(a.Audience, items...) + } + return nil +} + +// Recipients performs recipient de-duplication on the Activity's To, Bto, CC and BCC properties +func (a *Activity) Recipients() ItemCollection { + var alwaysRemove ItemCollection + if a.GetType() == BlockType && a.Object != nil { + alwaysRemove = append(alwaysRemove, a.Object) + } + if a.Actor != nil { + alwaysRemove = append(alwaysRemove, a.Actor) + } + if len(alwaysRemove) > 0 { + _ = removeFromAudience(a, alwaysRemove...) + } + return ItemCollectionDeduplication(&a.To, &a.Bto, &a.CC, &a.BCC, &a.Audience) +} + +// CleanRecipients checks if the "it" Item has recipients and cleans them if it does +func CleanRecipients(it Item) Item { + if IsNil(it) { + return nil + } + if s, ok := it.(HasRecipients); ok { + s.Clean() + } + return it +} + +// Clean removes Bto and BCC properties +func (a *Activity) Clean() { + _ = OnObject(a, func(o *Object) error { + o.Clean() + return nil + }) + CleanRecipients(a.Object) + CleanRecipients(a.Actor) + CleanRecipients(a.Target) +} + +type ( + // Accept indicates that the actor accepts the object. The target property can be used in certain circumstances to indicate + // the context into which the object has been accepted. + Accept = Activity + + // Add indicates that the actor has added the object to the target. If the target property is not explicitly specified, + // the target would need to be determined implicitly by context. + // The origin can be used to identify the context from which the object originated. + Add = Activity + + // Announce indicates that the actor is calling the target's attention the object. + // The origin typically has no defined meaning. + Announce = Activity + + // Block indicates that the actor is blocking the object. Blocking is a stronger form of Ignore. + // The typical use is to support social systems that allow one user to block activities or content of other users. + // The target and origin typically have no defined meaning. + Block = Ignore + + // Create indicates that the actor has created the object. + Create = Activity + + // Delete indicates that the actor has deleted the object. + // If specified, the origin indicates the context from which the object was deleted. + Delete = Activity + + // Dislike indicates that the actor dislikes the object. + Dislike = Activity + + // Flag indicates that the actor is "flagging" the object. + // Flagging is defined in the sense common to many social platforms as reporting content as being + // inappropriate for any number of reasons. + Flag = Activity + + // Follow indicates that the actor is "following" the object. Following is defined in the sense typically used within + // Social systems in which the actor is interested in any activity performed by or on the object. + // The target and origin typically have no defined meaning. + Follow = Activity + + // Ignore indicates that the actor is ignoring the object. The target and origin typically have no defined meaning. + Ignore = Activity + + // Invite is a specialization of Offer in which the actor is extending an invitation for the object to the target. + Invite = Offer + + // Join indicates that the actor has joined the object. The target and origin typically have no defined meaning. + Join = Activity + + // Leave indicates that the actor has left the object. The target and origin typically have no meaning. + Leave = Activity + + // Like indicates that the actor likes, recommends or endorses the object. + // The target and origin typically have no defined meaning. + Like = Activity + + // Listen inherits all properties from Activity. + Listen = Activity + + // Move indicates that the actor has moved object from origin to target. + // If the origin or target are not specified, either can be determined by context. + Move = Activity + + // Offer indicates that the actor is offering the object. + // If specified, the target indicates the entity to which the object is being offered. + Offer = Activity + + // Reject indicates that the actor is rejecting the object. The target and origin typically have no defined meaning. + Reject = Activity + + // Read indicates that the actor has read the object. + Read = Activity + + // Remove indicates that the actor is removing the object. If specified, + // the origin indicates the context from which the object is being removed. + Remove = Activity + + // TentativeReject is a specialization of Reject in which the rejection is considered tentative. + TentativeReject = Reject + + // TentativeAccept is a specialization of Accept indicating that the acceptance is tentative. + TentativeAccept = Accept + + // Undo indicates that the actor is undoing the object. In most cases, the object will be an Activity describing + // some previously performed action (for instance, a person may have previously "liked" an article but, + // for whatever reason, might choose to undo that like at some later point in time). + // The target and origin typically have no defined meaning. + Undo = Activity + + // Update indicates that the actor has updated the object. Note, however, that this vocabulary does not define a mechanism + // for describing the actual set of modifications made to object. + // The target and origin typically have no defined meaning. + Update = Activity + + // View indicates that the actor has viewed the object. + View = Activity +) + +// AcceptNew initializes an Accept activity +func AcceptNew(id ID, ob Item) *Accept { + a := ActivityNew(id, AcceptType, ob) + o := Accept(*a) + return &o +} + +// AddNew initializes an Add activity +func AddNew(id ID, ob, trgt Item) *Add { + a := ActivityNew(id, AddType, ob) + o := Add(*a) + o.Target = trgt + return &o +} + +// AnnounceNew initializes an Announce activity +func AnnounceNew(id ID, ob Item) *Announce { + a := ActivityNew(id, AnnounceType, ob) + o := Announce(*a) + return &o +} + +// BlockNew initializes a Block activity +func BlockNew(id ID, ob Item) *Block { + a := ActivityNew(id, BlockType, ob) + o := Block(*a) + return &o +} + +// CreateNew initializes a Create activity +func CreateNew(id ID, ob Item) *Create { + a := ActivityNew(id, CreateType, ob) + o := Create(*a) + return &o +} + +// DeleteNew initializes a Delete activity +func DeleteNew(id ID, ob Item) *Delete { + a := ActivityNew(id, DeleteType, ob) + o := Delete(*a) + return &o +} + +// DislikeNew initializes a Dislike activity +func DislikeNew(id ID, ob Item) *Dislike { + a := ActivityNew(id, DislikeType, ob) + o := Dislike(*a) + return &o +} + +// FlagNew initializes a Flag activity +func FlagNew(id ID, ob Item) *Flag { + a := ActivityNew(id, FlagType, ob) + o := Flag(*a) + return &o +} + +// FollowNew initializes a Follow activity +func FollowNew(id ID, ob Item) *Follow { + a := ActivityNew(id, FollowType, ob) + o := Follow(*a) + return &o +} + +// IgnoreNew initializes an Ignore activity +func IgnoreNew(id ID, ob Item) *Ignore { + a := ActivityNew(id, IgnoreType, ob) + o := Ignore(*a) + return &o +} + +// InviteNew initializes an Invite activity +func InviteNew(id ID, ob Item) *Invite { + a := ActivityNew(id, InviteType, ob) + o := Invite(*a) + return &o +} + +// JoinNew initializes a Join activity +func JoinNew(id ID, ob Item) *Join { + a := ActivityNew(id, JoinType, ob) + o := Join(*a) + return &o +} + +// LeaveNew initializes a Leave activity +func LeaveNew(id ID, ob Item) *Leave { + a := ActivityNew(id, LeaveType, ob) + o := Leave(*a) + return &o +} + +// LikeNew initializes a Like activity +func LikeNew(id ID, ob Item) *Like { + a := ActivityNew(id, LikeType, ob) + o := Like(*a) + return &o +} + +// ListenNew initializes a Listen activity +func ListenNew(id ID, ob Item) *Listen { + a := ActivityNew(id, ListenType, ob) + o := Listen(*a) + return &o +} + +// MoveNew initializes a Move activity +func MoveNew(id ID, ob Item) *Move { + a := ActivityNew(id, MoveType, ob) + o := Move(*a) + return &o +} + +// OfferNew initializes an Offer activity +func OfferNew(id ID, ob Item) *Offer { + a := ActivityNew(id, OfferType, ob) + o := Offer(*a) + return &o +} + +// RejectNew initializes a Reject activity +func RejectNew(id ID, ob Item) *Reject { + a := ActivityNew(id, RejectType, ob) + o := Reject(*a) + return &o +} + +// ReadNew initializes a Read activity +func ReadNew(id ID, ob Item) *Read { + a := ActivityNew(id, ReadType, ob) + o := Read(*a) + return &o +} + +// RemoveNew initializes a Remove activity +func RemoveNew(id ID, ob, trgt Item) *Remove { + a := ActivityNew(id, RemoveType, ob) + o := Remove(*a) + o.Target = trgt + return &o +} + +// TentativeRejectNew initializes a TentativeReject activity +func TentativeRejectNew(id ID, ob Item) *TentativeReject { + a := ActivityNew(id, TentativeRejectType, ob) + o := TentativeReject(*a) + return &o +} + +// TentativeAcceptNew initializes a TentativeAccept activity +func TentativeAcceptNew(id ID, ob Item) *TentativeAccept { + a := ActivityNew(id, TentativeAcceptType, ob) + o := TentativeAccept(*a) + return &o +} + +// UndoNew initializes an Undo activity +func UndoNew(id ID, ob Item) *Undo { + a := ActivityNew(id, UndoType, ob) + o := Undo(*a) + return &o +} + +// UpdateNew initializes an Update activity +func UpdateNew(id ID, ob Item) *Update { + a := ActivityNew(id, UpdateType, ob) + u := Update(*a) + return &u +} + +// ViewNew initializes a View activity +func ViewNew(id ID, ob Item) *View { + a := ActivityNew(id, ViewType, ob) + o := View(*a) + return &o +} + +// ActivityNew initializes a basic activity +func ActivityNew(id ID, typ ActivityVocabularyType, ob Item) *Activity { + if !ActivityTypes.Contains(typ) { + typ = ActivityType + } + a := Activity{ID: id, Type: typ} + a.Name = NaturalLanguageValuesNew() + a.Content = NaturalLanguageValuesNew() + + a.Object = ob + + return &a +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (a *Activity) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadActivity(val, a) +} + +func fmtActivityProps(w io.Writer) func(*Activity) error { + return func(a *Activity) error { + if !IsNil(a.Object) { + _, _ = fmt.Fprintf(w, " object: %s", a.Object) + } + return OnIntransitiveActivity(a, fmtIntransitiveActivityProps(w)) + } +} + +func (a Activity) Format(s fmt.State, verb rune) { + switch verb { + case 's': + if a.Type != "" && a.ID != "" { + _, _ = fmt.Fprintf(s, "%T[%s]( %s )", a, a.Type, a.ID) + } else if a.ID != "" { + _, _ = fmt.Fprintf(s, "%T( %s )", a, a.ID) + } else { + _, _ = fmt.Fprintf(s, "%T[%p]", a, &a) + } + case 'v': + _, _ = fmt.Fprintf(s, "%T[%s] {", a, a.Type) + fmtActivityProps(s)(&a) + _, _ = io.WriteString(s, " }") + } +} + +// ToActivity +func ToActivity(it Item) (*Activity, error) { + switch i := it.(type) { + case *Activity: + return i, nil + case Activity: + return &i, nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(Activity)) + if reflect.TypeOf(it).ConvertibleTo(typ) { + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Activity); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[Activity](it) +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (a Activity) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + JSONWrite(&b, '{') + + if !JSONWriteActivityValue(&b, a) { + return nil, nil + } + JSONWrite(&b, '}') + return b, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (a *Activity) UnmarshalBinary(data []byte) error { + return a.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (a Activity) MarshalBinary() ([]byte, error) { + return a.GobEncode() +} + +func mapIntransitiveActivityProperties(mm map[string][]byte, a *IntransitiveActivity) (hasData bool, err error) { + err = OnObject(a, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if a.Actor != nil { + if mm["actor"], err = gobEncodeItem(a.Actor); err != nil { + return hasData, err + } + hasData = true + } + if a.Target != nil { + if mm["target"], err = gobEncodeItem(a.Target); err != nil { + return hasData, err + } + hasData = true + } + if a.Result != nil { + if mm["result"], err = gobEncodeItem(a.Result); err != nil { + return hasData, err + } + hasData = true + } + if a.Instrument != nil { + if mm["instrument"], err = gobEncodeItem(a.Instrument); err != nil { + return hasData, err + } + hasData = true + } + return hasData, err +} + +func mapActivityProperties(mm map[string][]byte, a *Activity) (hasData bool, err error) { + err = OnIntransitiveActivity(a, func(a *IntransitiveActivity) error { + hasData, err = mapIntransitiveActivityProperties(mm, a) + return err + }) + if a.Object != nil { + if mm["object"], err = gobEncodeItem(a.Object); err != nil { + return hasData, err + } + hasData = true + } + return hasData, err +} + +// GobEncode +func (a Activity) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapActivityProperties(mm, &a) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +// GobDecode +func (a *Activity) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapActivityProperties(mm, a) +} + +// Equals verifies if our receiver Object is equals with the "with" Object +func (a Activity) Equals(with Item) bool { + result := true + err := OnActivity(with, func(w *Activity) error { + _ = OnIntransitiveActivity(a, func(oi *IntransitiveActivity) error { + result = oi.Equals(w) + return nil + }) + if w.Object != nil { + if !ItemsEqual(a.Object, w.Object) { + result = false + return nil + } + } + return nil + }) + if err != nil { + result = false + } + return result +} diff --git a/vendor/github.com/go-ap/activitypub/actor.go b/vendor/github.com/go-ap/activitypub/actor.go new file mode 100644 index 0000000..1bbe5c6 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/actor.go @@ -0,0 +1,622 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/valyala/fastjson" +) + +// CanReceiveActivities Types +const ( + ApplicationType ActivityVocabularyType = "Application" + GroupType ActivityVocabularyType = "Group" + OrganizationType ActivityVocabularyType = "Organization" + PersonType ActivityVocabularyType = "Person" + ServiceType ActivityVocabularyType = "Service" +) + +// ActorTypes represent the valid Actor types. +var ActorTypes = ActivityVocabularyTypes{ + ApplicationType, + GroupType, + OrganizationType, + PersonType, + ServiceType, +} + +// CanReceiveActivities is generally one of the ActivityStreams Actor Types, but they don't have to be. +// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension. +// Actors are retrieved like any other Object in ActivityPub. +// Like other ActivityStreams objects, actors have an id, which is a URI. +type CanReceiveActivities Item + +type Actors interface { + Actor +} + +// Actor is generally one of the ActivityStreams actor Types, but they don't have to be. +// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension. +// Actors are retrieved like any other Object in ActivityPub. +// Like other ActivityStreams objects, actors have an id, which is a URI. +type Actor struct { + // ID provides the globally unique identifier for anActivity Pub Object or Link. + ID ID `jsonld:"id,omitempty"` + // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // Name a simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // Attachment identifies a resource attached or related to an object that potentially requires special handling. + // The intent is to provide a model that is at least semantically similar to attachments in email. + Attachment Item `jsonld:"attachment,omitempty"` + // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. + // For instance, an object might be attributed to the completion of another activity. + AttributedTo Item `jsonld:"attributedTo,omitempty"` + // Audience identifies one or more entities that represent the total population of entities + // for which the object can considered to be relevant. + Audience ItemCollection `jsonld:"audience,omitempty"` + // Content or textual representation of the Activity Pub Object encoded as a JSON string. + // By default, the value of content is HTML. + // The mediaType property can be used in the object to indicate a different content type. + // (The content MAY be expressed using multiple language-tagged values.) + Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` + // Context identifies the context within which the object exists or an activity was performed. + // The notion of "context" used is intentionally vague. + // The intended function is to serve as a means of grouping objects and activities that share a + // common originating context or purpose. An example could be all activities relating to a common project or event. + Context Item `jsonld:"context,omitempty"` + // MediaType when used on an Object, identifies the MIME media type of the value of the content property. + // If not specified, the content property is assumed to contain text/html content. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // EndTime the date and time describing the actual or expected ending time of the object. + // When used with an Activity object, for instance, the endTime property specifies the moment + // the activity concluded or is expected to conclude. + EndTime time.Time `jsonld:"endTime,omitempty"` + // Generator identifies the entity (e.g. an application) that generated the object. + Generator Item `jsonld:"generator,omitempty"` + // Icon indicates an entity that describes an icon for this object. + // The image should have an aspect ratio of one (horizontal) to one (vertical) + // and should be suitable for presentation at a small size. + Icon Item `jsonld:"icon,omitempty"` + // Image indicates an entity that describes an image for this object. + // Unlike the icon property, there are no aspect ratio or display size limitations assumed. + Image Item `jsonld:"image,omitempty"` + // InReplyTo indicates one or more entities for which this object is considered a response. + InReplyTo Item `jsonld:"inReplyTo,omitempty"` + // Location indicates one or more physical or logical locations associated with the object. + Location Item `jsonld:"location,omitempty"` + // Preview identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // Published the date and time at which the object was published + Published time.Time `jsonld:"published,omitempty"` + // Replies identifies a Collection containing objects considered to be responses to this object. + Replies Item `jsonld:"replies,omitempty"` + // StartTime the date and time describing the actual or expected starting time of the object. + // When used with an Activity object, for instance, the startTime property specifies + // the moment the activity began or is scheduled to begin. + StartTime time.Time `jsonld:"startTime,omitempty"` + // Summary a natural language summarization of the object encoded as HTML. + // *Multiple language tagged summaries may be provided.) + Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` + // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. + // The key difference between attachment and tag is that the former implies association by inclusion, + // while the latter implies associated by reference. + Tag ItemCollection `jsonld:"tag,omitempty"` + // Updated the date and time at which the object was updated + Updated time.Time `jsonld:"updated,omitempty"` + // URL identifies one or more links to representations of the object + URL Item `jsonld:"url,omitempty"` + // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object + To ItemCollection `jsonld:"to,omitempty"` + // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. + Bto ItemCollection `jsonld:"bto,omitempty"` + // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. + CC ItemCollection `jsonld:"cc,omitempty"` + // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. + BCC ItemCollection `jsonld:"bcc,omitempty"` + // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, + // the duration property indicates the object's approximate duration. + // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], + // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). + Duration time.Duration `jsonld:"duration,omitempty"` + // This is a list of all Like activities with this object as the object property, added as a side effect. + // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Likes Item `jsonld:"likes,omitempty"` + // This is a list of all Announce activities with this object as the object property, added as a side effect. + // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Shares Item `jsonld:"shares,omitempty"` + // Source property is intended to convey some sort of source from which the content markup was derived, + // as a form of provenance, or to support future editing by clients. + // In general, clients do the conversion from source to content, not the other way around. + Source Source `jsonld:"source,omitempty"` + // A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor; + // see 5.2 Inbox. + Inbox Item `jsonld:"inbox,omitempty"` + // An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor; + // see 5.1 outbox. + Outbox Item `jsonld:"outbox,omitempty"` + // A link to an [ActivityStreams] collection of the actors that this actor is following; + // see 5.4 Following Collection + Following Item `jsonld:"following,omitempty"` + // A link to an [ActivityStreams] collection of the actors that follow this actor; + // see 5.3 Followers Collection. + Followers Item `jsonld:"followers,omitempty"` + // A link to an [ActivityStreams] collection of objects this actor has liked; + // see 5.5 Liked Collection. + Liked Item `jsonld:"liked,omitempty"` + // A short username which may be used to refer to the actor, with no uniqueness guarantees. + PreferredUsername NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"` + // A json object which maps additional (typically server/domain-wide) endpoints which may be useful either + // for this actor or someone referencing this actor. + // This mapping may be nested inside the actor document as the value or may be a link + // to a JSON-LD document with these properties. + Endpoints *Endpoints `jsonld:"endpoints,omitempty"` + // A list of supplementary Collections which may be of interest. + Streams ItemCollection `jsonld:"streams,omitempty"` + PublicKey PublicKey `jsonld:"publicKey,omitempty"` +} + +// GetID returns the ID corresponding to the current Actor +func (a Actor) GetID() ID { + return a.ID +} + +// GetLink returns the IRI corresponding to the current Actor +func (a Actor) GetLink() IRI { + return IRI(a.ID) +} + +// GetType returns the type of the current Actor +func (a Actor) GetType() ActivityVocabularyType { + return a.Type +} + +// IsLink validates if currentActivity Pub Actor is a Link +func (a Actor) IsLink() bool { + return false +} + +// IsObject validates if currentActivity Pub Actor is an Object +func (a Actor) IsObject() bool { + return true +} + +// IsCollection returns false for Actor Objects +func (a Actor) IsCollection() bool { + return false +} + +// PublicKey holds the ActivityPub compatible public key data +// The document reference can be found at: +// https://web-payments.org/vocabs/security#publicKey +type PublicKey struct { + ID ID `jsonld:"id,omitempty"` + Owner IRI `jsonld:"owner,omitempty"` + PublicKeyPem string `jsonld:"publicKeyPem,omitempty"` +} + +func (p *PublicKey) UnmarshalJSON(data []byte) error { + par := fastjson.Parser{} + val, err := par.ParseBytes(data) + if err != nil { + return err + } + + return JSONLoadPublicKey(val, p) +} + +func (p PublicKey) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := true + JSONWrite(&b, '{') + if v, err := p.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = !JSONWriteProp(&b, "id", v) + } + if len(p.Owner) > 0 { + notEmpty = JSONWriteIRIProp(&b, "owner", p.Owner) || notEmpty + } + if len(p.PublicKeyPem) > 0 { + if pem, err := json.Marshal(p.PublicKeyPem); err == nil { + notEmpty = JSONWriteProp(&b, "publicKeyPem", pem) || notEmpty + } + } + + if notEmpty { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (a *Actor) UnmarshalBinary(data []byte) error { + return a.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (a Actor) MarshalBinary() ([]byte, error) { + return a.GobEncode() +} + +func (a Actor) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapActorProperties(mm, &a) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (a *Actor) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapActorProperties(mm, a) +} + +type ( + // Application describes a software application. + Application = Actor + + // Group represents a formal or informal collective of Actors. + Group = Actor + + // Organization represents an organization. + Organization = Actor + + // Person represents an individual person. + Person = Actor + + // Service represents a service of any kind. + Service = Actor +) + +// ActorNew initializes an CanReceiveActivities type actor +func ActorNew(id ID, typ ActivityVocabularyType) *Actor { + if !ActorTypes.Contains(typ) { + typ = ActorType + } + + a := Actor{ID: id, Type: typ} + a.Name = NaturalLanguageValuesNew() + a.Content = NaturalLanguageValuesNew() + a.Summary = NaturalLanguageValuesNew() + a.PreferredUsername = NaturalLanguageValuesNew() + + return &a +} + +// ApplicationNew initializes an Application type actor +func ApplicationNew(id ID) *Application { + a := ActorNew(id, ApplicationType) + o := Application(*a) + return &o +} + +// GroupNew initializes a Group type actor +func GroupNew(id ID) *Group { + a := ActorNew(id, GroupType) + o := Group(*a) + return &o +} + +// OrganizationNew initializes an Organization type actor +func OrganizationNew(id ID) *Organization { + a := ActorNew(id, OrganizationType) + o := Organization(*a) + return &o +} + +// PersonNew initializes a Person type actor +func PersonNew(id ID) *Person { + a := ActorNew(id, PersonType) + o := Person(*a) + return &o +} + +// ServiceNew initializes a Service type actor +func ServiceNew(id ID) *Service { + a := ActorNew(id, ServiceType) + o := Service(*a) + return &o +} + +func (a *Actor) Recipients() ItemCollection { + return ItemCollectionDeduplication(&a.To, &a.Bto, &a.CC, &a.BCC, &a.Audience) +} + +func (a *Actor) Clean() { + _ = OnObject(a, func(o *Object) error { + o.Clean() + return nil + }) +} + +func (a *Actor) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadActor(val, a) +} + +func (a Actor) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + JSONWrite(&b, '{') + + OnObject(a, func(o *Object) error { + notEmpty = JSONWriteObjectValue(&b, *o) + return nil + }) + if a.Inbox != nil { + notEmpty = JSONWriteItemProp(&b, "inbox", a.Inbox) || notEmpty + } + if a.Outbox != nil { + notEmpty = JSONWriteItemProp(&b, "outbox", a.Outbox) || notEmpty + } + if a.Following != nil { + notEmpty = JSONWriteItemProp(&b, "following", a.Following) || notEmpty + } + if a.Followers != nil { + notEmpty = JSONWriteItemProp(&b, "followers", a.Followers) || notEmpty + } + if a.Liked != nil { + notEmpty = JSONWriteItemProp(&b, "liked", a.Liked) || notEmpty + } + if a.PreferredUsername != nil { + notEmpty = JSONWriteNaturalLanguageProp(&b, "preferredUsername", a.PreferredUsername) || notEmpty + } + if a.Endpoints != nil { + if v, err := a.Endpoints.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(&b, "endpoints", v) || notEmpty + } + } + if len(a.Streams) > 0 { + notEmpty = JSONWriteItemCollectionProp(&b, "streams", a.Streams, false) + } + if len(a.PublicKey.PublicKeyPem)+len(a.PublicKey.ID) > 0 { + if v, err := a.PublicKey.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(&b, "publicKey", v) || notEmpty + } + } + + if notEmpty { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +func (a Actor) Format(s fmt.State, verb rune) { + switch verb { + case 's': + if a.Type != "" && a.ID != "" { + _, _ = fmt.Fprintf(s, "%T[%s]( %s )", a, a.Type, a.ID) + } else if a.ID != "" { + _, _ = fmt.Fprintf(s, "%T( %s )", a, a.ID) + } else { + _, _ = fmt.Fprintf(s, "%T[%p]", a, &a) + } + case 'v': + _, _ = fmt.Fprintf(s, "%T[%s] { }", a, a.Type) + } +} + +// Endpoints a json object which maps additional (typically server/domain-wide) +// endpoints which may be useful either for this actor or someone referencing this actor. +// This mapping may be nested inside the actor document as the value or may be a link to +// a JSON-LD document with these properties. +type Endpoints struct { + // UploadMedia Upload endpoint URI for this user for binary data. + UploadMedia Item `jsonld:"uploadMedia,omitempty"` + // OauthAuthorizationEndpoint Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication + // to access. To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being + // the id of the requested ActivityStreams object. + OauthAuthorizationEndpoint Item `jsonld:"oauthAuthorizationEndpoint,omitempty"` + // OauthTokenEndpoint If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions, + // this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant. + OauthTokenEndpoint Item `jsonld:"oauthTokenEndpoint,omitempty"` + // ProvideClientKey If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions, + // this endpoint specifies a URI at which a client may acquire an access token. + ProvideClientKey Item `jsonld:"provideClientKey,omitempty"` + // SignClientKey If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization, + // this endpoint specifies a URI at which browser-authenticated users may authorize a client's public + // key for client to server interactions. + SignClientKey Item `jsonld:"signClientKey,omitempty"` + // SharedInbox An optional endpoint used for wide delivery of publicly addressed activities and activities sent to followers. + // SharedInbox endpoints SHOULD also be publicly readable OrderedCollection objects containing objects addressed to the + // Public special collection. Reading from the sharedInbox endpoint MUST NOT present objects which are not addressed to the Public endpoint. + SharedInbox Item `jsonld:"sharedInbox,omitempty"` +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (e *Endpoints) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + e.OauthAuthorizationEndpoint = JSONGetItem(val, "oauthAuthorizationEndpoint") + e.OauthTokenEndpoint = JSONGetItem(val, "oauthTokenEndpoint") + e.UploadMedia = JSONGetItem(val, "uploadMedia") + e.ProvideClientKey = JSONGetItem(val, "provideClientKey") + e.SignClientKey = JSONGetItem(val, "signClientKey") + e.SharedInbox = JSONGetItem(val, "sharedInbox") + return nil +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (e Endpoints) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + + JSONWrite(&b, '{') + if e.OauthAuthorizationEndpoint != nil { + notEmpty = JSONWriteItemProp(&b, "oauthAuthorizationEndpoint", e.OauthAuthorizationEndpoint) || notEmpty + } + if e.OauthTokenEndpoint != nil { + notEmpty = JSONWriteItemProp(&b, "oauthTokenEndpoint", e.OauthTokenEndpoint) || notEmpty + } + if e.ProvideClientKey != nil { + notEmpty = JSONWriteItemProp(&b, "provideClientKey", e.ProvideClientKey) || notEmpty + } + if e.SignClientKey != nil { + notEmpty = JSONWriteItemProp(&b, "signClientKey", e.SignClientKey) || notEmpty + } + if e.SharedInbox != nil { + notEmpty = JSONWriteItemProp(&b, "sharedInbox", e.SharedInbox) || notEmpty + } + if e.UploadMedia != nil { + notEmpty = JSONWriteItemProp(&b, "uploadMedia", e.UploadMedia) || notEmpty + } + if notEmpty { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +// ToActor +func ToActor(it Item) (*Actor, error) { + switch i := it.(type) { + case *Actor: + return i, nil + case Actor: + return &i, nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(Actor)) + if reflect.TypeOf(it).ConvertibleTo(typ) { + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Actor); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[Actor](it) +} + +// Equals verifies if our receiver Object is equals with the "with" Object +func (a Actor) Equals(with Item) bool { + result := true + err := OnActor(with, func(w *Actor) error { + _ = OnObject(a, func(oa *Object) error { + result = oa.Equals(w) + return nil + }) + if w.Inbox != nil { + if !ItemsEqual(a.Inbox, w.Inbox) { + result = false + return nil + } + } + if w.Outbox != nil { + if !ItemsEqual(a.Outbox, w.Outbox) { + result = false + return nil + } + } + if w.Liked != nil { + if !ItemsEqual(a.Liked, w.Liked) { + result = false + return nil + } + } + if w.PreferredUsername != nil { + if !a.PreferredUsername.Equals(w.PreferredUsername) { + result = false + return nil + } + } + return nil + }) + if err != nil { + result = false + } + return result +} + +func (e Endpoints) GobEncode() ([]byte, error) { + return nil, nil +} + +func (e *Endpoints) GobDecode(data []byte) error { + return nil +} + +func (p PublicKey) GobEncode() ([]byte, error) { + var ( + mm = make(map[string][]byte) + err error + hasData bool + ) + if len(p.ID) > 0 { + if mm["id"], err = p.ID.GobEncode(); err != nil { + return nil, err + } + hasData = true + } + if len(p.PublicKeyPem) > 0 { + mm["publicKeyPem"] = []byte(p.PublicKeyPem) + hasData = true + } + if len(p.Owner) > 0 { + if mm["owner"], err = gobEncodeItem(p.Owner); err != nil { + return nil, err + } + hasData = true + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (p *PublicKey) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + if raw, ok := mm["id"]; ok { + if err = p.ID.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["owner"]; ok { + if err = p.Owner.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["publicKeyPem"]; ok { + p.PublicKeyPem = string(raw) + } + return nil +} diff --git a/vendor/github.com/go-ap/activitypub/collection.go b/vendor/github.com/go-ap/activitypub/collection.go new file mode 100644 index 0000000..4883d2b --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/collection.go @@ -0,0 +1,438 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "reflect" + "time" + "unsafe" + + "github.com/valyala/fastjson" +) + +const CollectionOfIRIs ActivityVocabularyType = "IRICollection" +const CollectionOfItems ActivityVocabularyType = "ItemCollection" + +var CollectionTypes = ActivityVocabularyTypes{ + CollectionOfItems, + CollectionType, + OrderedCollectionType, + CollectionPageType, + OrderedCollectionPageType, +} + +// Collections +// +// https://www.w3.org/TR/activitypub/#collections +// +// [ActivityStreams] defines the collection concept; ActivityPub defines several collections with special behavior. +// +// Note that ActivityPub makes use of ActivityStreams paging to traverse large sets of objects. +// +// Note that some of these collections are specified to be of type OrderedCollection specifically, +// while others are permitted to be either a Collection or an OrderedCollection. +// An OrderedCollection MUST be presented consistently in reverse chronological order. +// +// NOTE +// What property is used to determine the reverse chronological order is intentionally left as an implementation detail. +// For example, many SQL-style databases use an incrementing integer as an identifier, which can be reasonably used for +// handling insertion order in most cases. In other databases, an insertion time timestamp may be preferred. +// What is used isn't important, but the ordering of elements must remain intact, with newer items first. +// A property which changes regularly, such a "last updated" timestamp, should not be used. +type Collections interface { + Collection | CollectionPage | OrderedCollection | OrderedCollectionPage | ItemCollection | IRIs +} + +type CollectionInterface interface { + ObjectOrLink + Collection() ItemCollection + Append(ob ...Item) error + Count() uint + Contains(Item) bool +} + +// Collection is a subtype of Activity Pub Object that represents ordered or unordered sets of Activity Pub Object or Link instances. +type Collection struct { + // ID provides the globally unique identifier for anActivity Pub Object or Link. + ID ID `jsonld:"id,omitempty"` + // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // Name a simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // Attachment identifies a resource attached or related to an object that potentially requires special handling. + // The intent is to provide a model that is at least semantically similar to attachments in email. + Attachment Item `jsonld:"attachment,omitempty"` + // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. + // For instance, an object might be attributed to the completion of another activity. + AttributedTo Item `jsonld:"attributedTo,omitempty"` + // Audience identifies one or more entities that represent the total population of entities + // for which the object can considered to be relevant. + Audience ItemCollection `jsonld:"audience,omitempty"` + // Content or textual representation of the Activity Pub Object encoded as a JSON string. + // By default, the value of content is HTML. + // The mediaType property can be used in the object to indicate a different content type. + // (The content MAY be expressed using multiple language-tagged values.) + Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` + // Context identifies the context within which the object exists or an activity was performed. + // The notion of "context" used is intentionally vague. + // The intended function is to serve as a means of grouping objects and activities that share a + // common originating context or purpose. An example could be all activities relating to a common project or event. + Context Item `jsonld:"context,omitempty"` + // MediaType when used on an Object, identifies the MIME media type of the value of the content property. + // If not specified, the content property is assumed to contain text/html content. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // EndTime the date and time describing the actual or expected ending time of the object. + // When used with an Activity object, for instance, the endTime property specifies the moment + // the activity concluded or is expected to conclude. + EndTime time.Time `jsonld:"endTime,omitempty"` + // Generator identifies the entity (e.g. an application) that generated the object. + Generator Item `jsonld:"generator,omitempty"` + // Icon indicates an entity that describes an icon for this object. + // The image should have an aspect ratio of one (horizontal) to one (vertical) + // and should be suitable for presentation at a small size. + Icon Item `jsonld:"icon,omitempty"` + // Image indicates an entity that describes an image for this object. + // Unlike the icon property, there are no aspect ratio or display size limitations assumed. + Image Item `jsonld:"image,omitempty"` + // InReplyTo indicates one or more entities for which this object is considered a response. + InReplyTo Item `jsonld:"inReplyTo,omitempty"` + // Location indicates one or more physical or logical locations associated with the object. + Location Item `jsonld:"location,omitempty"` + // Preview identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // Published the date and time at which the object was published + Published time.Time `jsonld:"published,omitempty"` + // Replies identifies a Collection containing objects considered to be responses to this object. + Replies Item `jsonld:"replies,omitempty"` + // StartTime the date and time describing the actual or expected starting time of the object. + // When used with an Activity object, for instance, the startTime property specifies + // the moment the activity began or is scheduled to begin. + StartTime time.Time `jsonld:"startTime,omitempty"` + // Summary a natural language summarization of the object encoded as HTML. + // *Multiple language tagged summaries may be provided.) + Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` + // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. + // The key difference between attachment and tag is that the former implies association by inclusion, + // while the latter implies associated by reference. + Tag ItemCollection `jsonld:"tag,omitempty"` + // Updated the date and time at which the object was updated + Updated time.Time `jsonld:"updated,omitempty"` + // URL identifies one or more links to representations of the object + URL Item `jsonld:"url,omitempty"` + // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object + To ItemCollection `jsonld:"to,omitempty"` + // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. + Bto ItemCollection `jsonld:"bto,omitempty"` + // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. + CC ItemCollection `jsonld:"cc,omitempty"` + // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. + BCC ItemCollection `jsonld:"bcc,omitempty"` + // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, + // the duration property indicates the object's approximate duration. + // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], + // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). + Duration time.Duration `jsonld:"duration,omitempty"` + // This is a list of all Like activities with this object as the object property, added as a side effect. + // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Likes Item `jsonld:"likes,omitempty"` + // This is a list of all Announce activities with this object as the object property, added as a side effect. + // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Shares Item `jsonld:"shares,omitempty"` + // Source property is intended to convey some sort of source from which the content markup was derived, + // as a form of provenance, or to support future editing by clients. + // In general, clients do the conversion from source to content, not the other way around. + Source Source `jsonld:"source,omitempty"` + // In a paged Collection, indicates the page that contains the most recently updated member items. + Current ObjectOrLink `jsonld:"current,omitempty"` + // In a paged Collection, indicates the furthest preceding page of items in the collection. + First ObjectOrLink `jsonld:"first,omitempty"` + // In a paged Collection, indicates the furthest proceeding page of the collection. + Last ObjectOrLink `jsonld:"last,omitempty"` + // A non-negative integer specifying the total number of objects contained by the logical view of the collection. + // This number might not reflect the actual number of items serialized within the Collection object instance. + TotalItems uint `jsonld:"totalItems"` + // Identifies the items contained in a collection. The items might be ordered or unordered. + Items ItemCollection `jsonld:"items,omitempty"` +} + +type ( + // FollowersCollection is a collection of followers + FollowersCollection = Collection + + // FollowingCollection is a list of everybody that the actor has followed, added as a side effect. + // The following collection MUST be either an OrderedCollection or a Collection and MAY + // be filtered on privileges of an authenticated user or as appropriate when no authentication is given. + FollowingCollection = Collection +) + +// CollectionNew initializes a new Collection +func CollectionNew(id ID) *Collection { + c := Collection{ID: id, Type: CollectionType} + c.Name = NaturalLanguageValuesNew() + c.Content = NaturalLanguageValuesNew() + c.Summary = NaturalLanguageValuesNew() + return &c +} + +// OrderedCollectionNew initializes a new OrderedCollection +func OrderedCollectionNew(id ID) *OrderedCollection { + o := OrderedCollection{ID: id, Type: OrderedCollectionType} + o.Name = NaturalLanguageValuesNew() + o.Content = NaturalLanguageValuesNew() + + return &o +} + +// GetID returns the ID corresponding to the Collection object +func (c Collection) GetID() ID { + return c.ID +} + +// GetType returns the Collection's type +func (c Collection) GetType() ActivityVocabularyType { + return c.Type +} + +// IsLink returns false for a Collection object +func (c Collection) IsLink() bool { + return false +} + +// IsObject returns true for a Collection object +func (c Collection) IsObject() bool { + return true +} + +// IsCollection returns true for Collection objects +func (c Collection) IsCollection() bool { + return true +} + +// GetLink returns the IRI corresponding to the Collection object +func (c Collection) GetLink() IRI { + return IRI(c.ID) +} + +// Collection returns the Collection's items +func (c Collection) Collection() ItemCollection { + return c.Items +} + +// Append adds an element to a Collection +func (c *Collection) Append(it ...Item) error { + for _, ob := range it { + if c.Items.Contains(ob) { + continue + } + c.Items = append(c.Items, ob) + } + return nil +} + +// Count returns the maximum between the length of Items in collection and its TotalItems property +func (c *Collection) Count() uint { + if c == nil { + return 0 + } + return uint(len(c.Items)) +} + +// Contains verifies if Collection array contains the received one +func (c Collection) Contains(r Item) bool { + if len(c.Items) == 0 { + return false + } + for _, it := range c.Items { + if ItemsEqual(it, r) { + return true + } + } + return false +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (c *Collection) UnmarshalJSON(data []byte) error { + par := fastjson.Parser{} + val, err := par.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadCollection(val, c) +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (c Collection) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + JSONWrite(&b, '{') + + OnObject(c, func(o *Object) error { + notEmpty = JSONWriteObjectValue(&b, *o) + return nil + }) + if c.Current != nil { + notEmpty = JSONWriteItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + notEmpty = JSONWriteItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + notEmpty = JSONWriteItemProp(&b, "last", c.Last) || notEmpty + } + notEmpty = JSONWriteIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty + if c.Items != nil { + notEmpty = JSONWriteItemCollectionProp(&b, "items", c.Items, false) || notEmpty + } + if notEmpty { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (c *Collection) UnmarshalBinary(data []byte) error { + return c.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (c Collection) MarshalBinary() ([]byte, error) { + return c.GobEncode() +} + +func (c Collection) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapCollectionProperties(mm, c) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (c *Collection) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapCollectionProperties(mm, c) +} + +func (c Collection) Format(s fmt.State, verb rune) { + switch verb { + case 's', 'v': + _, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", c, c.Type, c.TotalItems) + } +} + +// ToCollection +func ToCollection(it Item) (*Collection, error) { + switch i := it.(type) { + case *Collection: + return i, nil + case Collection: + return &i, nil + case *CollectionPage: + return (*Collection)(unsafe.Pointer(i)), nil + case CollectionPage: + return (*Collection)(unsafe.Pointer(&i)), nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(Collection)) + val := reflect.ValueOf(it) + if val.IsValid() && typ.Elem().Name() == val.Type().Elem().Name() { + conv := val.Convert(typ) + if i, ok := conv.Interface().(*Collection); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[Collection](it) +} + +// ItemsMatch +func (c Collection) ItemsMatch(col ...Item) bool { + for _, it := range col { + if match := c.Items.Contains(it); !match { + return false + } + } + return true +} + +// Equals +func (c Collection) Equals(with Item) bool { + if IsNil(with) { + return false + } + if !with.IsCollection() { + return false + } + result := true + _ = OnCollection(with, func(w *Collection) error { + _ = OnObject(w, func(wo *Object) error { + if !wo.Equals(c) { + result = false + return nil + } + return nil + }) + if w.TotalItems > 0 { + if w.TotalItems != c.TotalItems { + result = false + return nil + } + } + if w.Current != nil { + if !ItemsEqual(c.Current, w.Current) { + result = false + return nil + } + } + if w.First != nil { + if !ItemsEqual(c.First, w.First) { + result = false + return nil + } + } + if w.Last != nil { + if !ItemsEqual(c.Last, w.Last) { + result = false + return nil + } + } + if w.Items != nil { + if !c.Items.Equals(w.Items) { + result = false + return nil + } + } + return nil + }) + return result +} + +func (c *Collection) Recipients() ItemCollection { + return ItemCollectionDeduplication(&c.To, &c.Bto, &c.CC, &c.BCC, &c.Audience) +} + +func (c *Collection) Clean() { + _ = OnObject(c, func(o *Object) error { + o.Clean() + return nil + }) +} diff --git a/vendor/github.com/go-ap/activitypub/collection_page.go b/vendor/github.com/go-ap/activitypub/collection_page.go new file mode 100644 index 0000000..93dcf87 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/collection_page.go @@ -0,0 +1,435 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "reflect" + "time" + + "github.com/valyala/fastjson" +) + +// CollectionPage is a Collection that contains a large number of items and when it becomes impractical +// for an implementation to serialize every item contained by a Collection using the items +// property alone. In such cases, the items within a Collection can be divided into distinct subsets or "pages". +type CollectionPage struct { + // ID provides the globally unique identifier for anActivity Pub Object or Link. + ID ID `jsonld:"id,omitempty"` + // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // Name a simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // Attachment identifies a resource attached or related to an object that potentially requires special handling. + // The intent is to provide a model that is at least semantically similar to attachments in email. + Attachment Item `jsonld:"attachment,omitempty"` + // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. + // For instance, an object might be attributed to the completion of another activity. + AttributedTo Item `jsonld:"attributedTo,omitempty"` + // Audience identifies one or more entities that represent the total population of entities + // for which the object can considered to be relevant. + Audience ItemCollection `jsonld:"audience,omitempty"` + // Content or textual representation of the Activity Pub Object encoded as a JSON string. + // By default, the value of content is HTML. + // The mediaType property can be used in the object to indicate a different content type. + // (The content MAY be expressed using multiple language-tagged values.) + Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` + // Context identifies the context within which the object exists or an activity was performed. + // The notion of "context" used is intentionally vague. + // The intended function is to serve as a means of grouping objects and activities that share a + // common originating context or purpose. An example could be all activities relating to a common project or event. + Context Item `jsonld:"context,omitempty"` + // MediaType when used on an Object, identifies the MIME media type of the value of the content property. + // If not specified, the content property is assumed to contain text/html content. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // EndTime the date and time describing the actual or expected ending time of the object. + // When used with an Activity object, for instance, the endTime property specifies the moment + // the activity concluded or is expected to conclude. + EndTime time.Time `jsonld:"endTime,omitempty"` + // Generator identifies the entity (e.g. an application) that generated the object. + Generator Item `jsonld:"generator,omitempty"` + // Icon indicates an entity that describes an icon for this object. + // The image should have an aspect ratio of one (horizontal) to one (vertical) + // and should be suitable for presentation at a small size. + Icon Item `jsonld:"icon,omitempty"` + // Image indicates an entity that describes an image for this object. + // Unlike the icon property, there are no aspect ratio or display size limitations assumed. + Image Item `jsonld:"image,omitempty"` + // InReplyTo indicates one or more entities for which this object is considered a response. + InReplyTo Item `jsonld:"inReplyTo,omitempty"` + // Location indicates one or more physical or logical locations associated with the object. + Location Item `jsonld:"location,omitempty"` + // Preview identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // Published the date and time at which the object was published + Published time.Time `jsonld:"published,omitempty"` + // Replies identifies a Collection containing objects considered to be responses to this object. + Replies Item `jsonld:"replies,omitempty"` + // StartTime the date and time describing the actual or expected starting time of the object. + // When used with an Activity object, for instance, the startTime property specifies + // the moment the activity began or is scheduled to begin. + StartTime time.Time `jsonld:"startTime,omitempty"` + // Summary a natural language summarization of the object encoded as HTML. + // *Multiple language tagged summaries may be provided.) + Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` + // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. + // The key difference between attachment and tag is that the former implies association by inclusion, + // while the latter implies associated by reference. + Tag ItemCollection `jsonld:"tag,omitempty"` + // Updated the date and time at which the object was updated + Updated time.Time `jsonld:"updated,omitempty"` + // URL identifies one or more links to representations of the object + URL Item `jsonld:"url,omitempty"` + // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object + To ItemCollection `jsonld:"to,omitempty"` + // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. + Bto ItemCollection `jsonld:"bto,omitempty"` + // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. + CC ItemCollection `jsonld:"cc,omitempty"` + // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. + BCC ItemCollection `jsonld:"bcc,omitempty"` + // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, + // the duration property indicates the object's approximate duration. + // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], + // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). + Duration time.Duration `jsonld:"duration,omitempty"` + // This is a list of all Like activities with this object as the object property, added as a side effect. + // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Likes Item `jsonld:"likes,omitempty"` + // This is a list of all Announce activities with this object as the object property, added as a side effect. + // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Shares Item `jsonld:"shares,omitempty"` + // Source property is intended to convey some sort of source from which the content markup was derived, + // as a form of provenance, or to support future editing by clients. + // In general, clients do the conversion from source to content, not the other way around. + Source Source `jsonld:"source,omitempty"` + // In a paged Collection, indicates the page that contains the most recently updated member items. + Current ObjectOrLink `jsonld:"current,omitempty"` + // In a paged Collection, indicates the furthest preceding page of items in the collection. + First ObjectOrLink `jsonld:"first,omitempty"` + // In a paged Collection, indicates the furthest proceeding page of the collection. + Last ObjectOrLink `jsonld:"last,omitempty"` + // A non-negative integer specifying the total number of objects contained by the logical view of the collection. + // This number might not reflect the actual number of items serialized within the Collection object instance. + TotalItems uint `jsonld:"totalItems"` + // Identifies the items contained in a collection. The items might be unordered. + Items ItemCollection `jsonld:"items,omitempty"` + // Identifies the Collection to which a CollectionPage objects items belong. + PartOf Item `jsonld:"partOf,omitempty"` + // In a paged Collection, indicates the next page of items. + Next Item `jsonld:"next,omitempty"` + // In a paged Collection, identifies the previous page of items. + Prev Item `jsonld:"prev,omitempty"` +} + +// GetID returns the ID corresponding to the CollectionPage object +func (c CollectionPage) GetID() ID { + return c.ID +} + +// GetType returns the CollectionPage's type +func (c CollectionPage) GetType() ActivityVocabularyType { + return c.Type +} + +// IsLink returns false for a CollectionPage object +func (c CollectionPage) IsLink() bool { + return false +} + +// IsObject returns true for a CollectionPage object +func (c CollectionPage) IsObject() bool { + return true +} + +// IsCollection returns true for CollectionPage objects +func (c CollectionPage) IsCollection() bool { + return true +} + +// GetLink returns the IRI corresponding to the CollectionPage object +func (c CollectionPage) GetLink() IRI { + return IRI(c.ID) +} + +// Collection returns the ColleCollectionPagection items +func (c CollectionPage) Collection() ItemCollection { + return c.Items +} + +// Count returns the maximum between the length of Items in the collection page and its TotalItems property +func (c *CollectionPage) Count() uint { + if c == nil { + return 0 + } + return uint(len(c.Items)) +} + +// Append adds an element to a CollectionPage +func (c *CollectionPage) Append(it ...Item) error { + for _, ob := range it { + if c.Items.Contains(ob) { + continue + } + c.Items = append(c.Items, ob) + } + return nil +} + +// Contains verifies if CollectionPage array contains the received one +func (c CollectionPage) Contains(r Item) bool { + if len(c.Items) == 0 { + return false + } + for _, it := range c.Items { + if ItemsEqual(it, r) { + return true + } + } + return false +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (c *CollectionPage) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadCollectionPage(val, c) +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (c CollectionPage) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + notEmpty := false + JSONWrite(&b, '{') + + OnObject(c, func(o *Object) error { + notEmpty = JSONWriteObjectValue(&b, *o) + return nil + }) + if c.PartOf != nil { + notEmpty = JSONWriteItemProp(&b, "partOf", c.PartOf) || notEmpty + } + if c.Current != nil { + notEmpty = JSONWriteItemProp(&b, "current", c.Current) || notEmpty + } + if c.First != nil { + notEmpty = JSONWriteItemProp(&b, "first", c.First) || notEmpty + } + if c.Last != nil { + notEmpty = JSONWriteItemProp(&b, "last", c.Last) || notEmpty + } + if c.Next != nil { + notEmpty = JSONWriteItemProp(&b, "next", c.Next) || notEmpty + } + if c.Prev != nil { + notEmpty = JSONWriteItemProp(&b, "prev", c.Prev) || notEmpty + } + notEmpty = JSONWriteIntProp(&b, "totalItems", int64(c.TotalItems)) || notEmpty + if c.Items != nil { + notEmpty = JSONWriteItemCollectionProp(&b, "items", c.Items, false) || notEmpty + } + if notEmpty { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (c *CollectionPage) UnmarshalBinary(data []byte) error { + return c.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (c CollectionPage) MarshalBinary() ([]byte, error) { + return c.GobEncode() +} + +func (c CollectionPage) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapCollectionPageProperties(mm, c) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (c *CollectionPage) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapCollectionPageProperties(mm, c) +} + +// CollectionNew initializes a new CollectionPage +func CollectionPageNew(parent CollectionInterface) *CollectionPage { + p := CollectionPage{ + PartOf: parent.GetLink(), + } + if pc, ok := parent.(*Collection); ok { + copyCollectionToPage(pc, &p) + } + p.Type = CollectionPageType + return &p +} + +func copyCollectionToPage(c *Collection, p *CollectionPage) error { + p.Type = CollectionPageType + p.Name = c.Name + p.Content = c.Content + p.Summary = c.Summary + p.Context = c.Context + p.URL = c.URL + p.MediaType = c.MediaType + p.Generator = c.Generator + p.AttributedTo = c.AttributedTo + p.Attachment = c.Attachment + p.Location = c.Location + p.Published = c.Published + p.StartTime = c.StartTime + p.EndTime = c.EndTime + p.Duration = c.Duration + p.Icon = c.Icon + p.Preview = c.Preview + p.Image = c.Image + p.Updated = c.Updated + p.InReplyTo = c.InReplyTo + p.To = c.To + p.Audience = c.Audience + p.Bto = c.Bto + p.CC = c.CC + p.BCC = c.BCC + p.Replies = c.Replies + p.Tag = c.Tag + p.TotalItems = c.TotalItems + p.Items = c.Items + p.Current = c.Current + p.First = c.First + p.PartOf = c.GetLink() + return nil +} + +// ToCollectionPage +func ToCollectionPage(it Item) (*CollectionPage, error) { + switch i := it.(type) { + case *CollectionPage: + return i, nil + case CollectionPage: + return &i, nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(CollectionPage)) + val := reflect.ValueOf(it) + if val.IsValid() && typ.Elem().Name() == val.Type().Elem().Name() { + conv := val.Convert(typ) + if i, ok := conv.Interface().(*CollectionPage); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[CollectionPage](it) +} + +// ItemsMatch +func (c CollectionPage) ItemsMatch(col ...Item) bool { + for _, it := range col { + if match := c.Items.Contains(it); !match { + return false + } + } + return true +} + +// Equals +func (c CollectionPage) Equals(with Item) bool { + if IsNil(with) { + return false + } + if !with.IsCollection() { + return false + } + result := true + OnCollectionPage(with, func(w *CollectionPage) error { + OnCollection(w, func(wo *Collection) error { + if !wo.Equals(c) { + result = false + return nil + } + return nil + }) + if w.PartOf != nil { + if !ItemsEqual(c.PartOf, w.PartOf) { + result = false + return nil + } + } + if w.Current != nil { + if !ItemsEqual(c.Current, w.Current) { + result = false + return nil + } + } + if w.First != nil { + if !ItemsEqual(c.First, w.First) { + result = false + return nil + } + } + if w.Last != nil { + if !ItemsEqual(c.Last, w.Last) { + result = false + return nil + } + } + if w.Next != nil { + if !ItemsEqual(c.Next, w.Next) { + result = false + return nil + } + } + if w.Prev != nil { + if !ItemsEqual(c.Prev, w.Prev) { + result = false + return nil + } + } + return nil + }) + return result +} + +func (c CollectionPage) Format(s fmt.State, verb rune) { + switch verb { + case 's', 'v': + _, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", c, c.Type, c.TotalItems) + } +} + +func (c *CollectionPage) Recipients() ItemCollection { + return ItemCollectionDeduplication(&c.To, &c.Bto, &c.CC, &c.BCC, &c.Audience) +} + +func (c *CollectionPage) Clean() { + _ = OnObject(c, func(o *Object) error { + o.Clean() + return nil + }) +} diff --git a/vendor/github.com/go-ap/activitypub/copy.go b/vendor/github.com/go-ap/activitypub/copy.go new file mode 100644 index 0000000..64a51cb --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/copy.go @@ -0,0 +1,234 @@ +package activitypub + +import ( + "fmt" +) + +func CopyOrderedCollectionPageProperties(to, from *OrderedCollectionPage) (*OrderedCollectionPage, error) { + to.PartOf = replaceIfItem(to.PartOf, from.PartOf) + to.Next = replaceIfItem(to.Next, from.Next) + to.Prev = replaceIfItem(to.Prev, from.Prev) + oldCol, _ := ToOrderedCollection(to) + newCol, _ := ToOrderedCollection(from) + _, err := CopyOrderedCollectionProperties(oldCol, newCol) + if err != nil { + return to, err + } + return to, nil +} + +func CopyCollectionPageProperties(to, from *CollectionPage) (*CollectionPage, error) { + to.PartOf = replaceIfItem(to.PartOf, from.PartOf) + to.Next = replaceIfItem(to.Next, from.Next) + to.Prev = replaceIfItem(to.Prev, from.Prev) + toCol, _ := ToCollection(to) + fromCol, _ := ToCollection(from) + _, err := CopyCollectionProperties(toCol, fromCol) + return to, err +} + +func CopyOrderedCollectionProperties(to, from *OrderedCollection) (*OrderedCollection, error) { + to.First = replaceIfItem(to.First, from.First) + to.Last = replaceIfItem(to.Last, from.Last) + to.OrderedItems = replaceIfItemCollection(to.OrderedItems, from.OrderedItems) + if to.TotalItems == 0 { + to.TotalItems = from.TotalItems + } + oldOb, _ := ToObject(to) + newOb, _ := ToObject(from) + _, err := CopyObjectProperties(oldOb, newOb) + return to, err +} + +func CopyCollectionProperties(to, from *Collection) (*Collection, error) { + to.First = replaceIfItem(to.First, from.First) + to.Last = replaceIfItem(to.Last, from.Last) + to.Items = replaceIfItemCollection(to.Items, from.Items) + if to.TotalItems == 0 { + to.TotalItems = from.TotalItems + } + oldOb, _ := ToObject(to) + newOb, _ := ToObject(from) + _, err := CopyObjectProperties(oldOb, newOb) + return to, err +} + +// CopyObjectProperties updates the "old" object properties with the "new's" +// Including ID and Type +func CopyObjectProperties(to, from *Object) (*Object, error) { + to.ID = from.ID + to.Type = from.Type + to.Name = replaceIfNaturalLanguageValues(to.Name, from.Name) + to.Attachment = replaceIfItem(to.Attachment, from.Attachment) + to.AttributedTo = replaceIfItem(to.AttributedTo, from.AttributedTo) + to.Audience = replaceIfItemCollection(to.Audience, from.Audience) + to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content) + to.Context = replaceIfItem(to.Context, from.Context) + if len(from.MediaType) > 0 { + to.MediaType = from.MediaType + } + if !from.EndTime.IsZero() { + to.EndTime = from.EndTime + } + to.Generator = replaceIfItem(to.Generator, from.Generator) + to.Icon = replaceIfItem(to.Icon, from.Icon) + to.Image = replaceIfItem(to.Image, from.Image) + to.InReplyTo = replaceIfItem(to.InReplyTo, from.InReplyTo) + to.Location = replaceIfItem(to.Location, from.Location) + to.Preview = replaceIfItem(to.Preview, from.Preview) + if to.Published.IsZero() && !from.Published.IsZero() { + to.Published = from.Published + } + if to.Updated.IsZero() && !from.Updated.IsZero() { + to.Updated = from.Updated + } + to.Replies = replaceIfItem(to.Replies, from.Replies) + if !from.StartTime.IsZero() { + to.StartTime = from.StartTime + } + to.Summary = replaceIfNaturalLanguageValues(to.Summary, from.Summary) + to.Tag = replaceIfItemCollection(to.Tag, from.Tag) + if from.URL != nil { + to.URL = from.URL + } + to.To = replaceIfItemCollection(to.To, from.To) + to.Bto = replaceIfItemCollection(to.Bto, from.Bto) + to.CC = replaceIfItemCollection(to.CC, from.CC) + to.BCC = replaceIfItemCollection(to.BCC, from.BCC) + if from.Duration == 0 { + to.Duration = from.Duration + } + to.Source = replaceIfSource(to.Source, from.Source) + return to, nil +} + +func copyAllItemProperties(to, from Item) (Item, error) { + if CollectionType == to.GetType() { + o, err := ToCollection(to) + if err != nil { + return o, err + } + n, err := ToCollection(from) + if err != nil { + return o, err + } + return CopyCollectionProperties(o, n) + } + if CollectionPageType == to.GetType() { + o, err := ToCollectionPage(to) + if err != nil { + return o, err + } + n, err := ToCollectionPage(from) + if err != nil { + return o, err + } + return CopyCollectionPageProperties(o, n) + } + if OrderedCollectionType == to.GetType() { + o, err := ToOrderedCollection(to) + if err != nil { + return o, err + } + n, err := ToOrderedCollection(from) + if err != nil { + return o, err + } + return CopyOrderedCollectionProperties(o, n) + } + if OrderedCollectionPageType == to.GetType() { + o, err := ToOrderedCollectionPage(to) + if err != nil { + return o, err + } + n, err := ToOrderedCollectionPage(from) + if err != nil { + return o, err + } + return CopyOrderedCollectionPageProperties(o, n) + } + if ActorTypes.Contains(to.GetType()) { + o, err := ToActor(to) + if err != nil { + return o, err + } + n, err := ToActor(from) + if err != nil { + return o, err + } + return UpdatePersonProperties(o, n) + } + if ObjectTypes.Contains(to.GetType()) || to.GetType() == "" { + o, err := ToObject(to) + if err != nil { + return o, err + } + n, err := ToObject(from) + if err != nil { + return o, err + } + return CopyObjectProperties(o, n) + } + return to, fmt.Errorf("could not process objects with type %s", to.GetType()) +} + +// CopyItemProperties delegates to the correct per type functions for copying +// properties between matching Activity Objects +func CopyItemProperties(to, from Item) (Item, error) { + if to == nil { + return to, fmt.Errorf("nil object to update") + } + if from == nil { + return to, fmt.Errorf("nil object for update") + } + if !to.GetLink().Equals(from.GetLink(), false) { + return to, fmt.Errorf("object IDs don't match") + } + if to.GetType() != "" && to.GetType() != from.GetType() { + return to, fmt.Errorf("invalid object types for update %s(old) and %s(new)", from.GetType(), to.GetType()) + } + return copyAllItemProperties(to, from) +} + +// UpdatePersonProperties +func UpdatePersonProperties(to, from *Actor) (*Actor, error) { + to.Inbox = replaceIfItem(to.Inbox, from.Inbox) + to.Outbox = replaceIfItem(to.Outbox, from.Outbox) + to.Following = replaceIfItem(to.Following, from.Following) + to.Followers = replaceIfItem(to.Followers, from.Followers) + to.Liked = replaceIfItem(to.Liked, from.Liked) + to.PreferredUsername = replaceIfNaturalLanguageValues(to.PreferredUsername, from.PreferredUsername) + oldOb, _ := ToObject(to) + newOb, _ := ToObject(from) + _, err := CopyObjectProperties(oldOb, newOb) + return to, err +} + +func replaceIfItem(old, new Item) Item { + if new == nil { + return old + } + return new +} + +func replaceIfItemCollection(old, new ItemCollection) ItemCollection { + if new == nil { + return old + } + return new +} + +func replaceIfNaturalLanguageValues(old, new NaturalLanguageValues) NaturalLanguageValues { + if new == nil { + return old + } + return new +} + +func replaceIfSource(to, from Source) Source { + if from.MediaType != to.MediaType { + return from + } + to.Content = replaceIfNaturalLanguageValues(to.Content, from.Content) + return to +} diff --git a/vendor/github.com/go-ap/activitypub/decoding_gob.go b/vendor/github.com/go-ap/activitypub/decoding_gob.go new file mode 100644 index 0000000..4568521 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/decoding_gob.go @@ -0,0 +1,722 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "errors" + "time" +) + +func GobDecode(data []byte) (Item, error) { + return gobDecodeItem(data) +} + +func gobDecodeUint(i *uint, data []byte) error { + g := gob.NewDecoder(bytes.NewReader(data)) + return g.Decode(i) +} + +func gobDecodeFloat64(f *float64, data []byte) error { + g := gob.NewDecoder(bytes.NewReader(data)) + return g.Decode(f) +} + +func gobDecodeInt64(i *int64, data []byte) error { + g := gob.NewDecoder(bytes.NewReader(data)) + return g.Decode(i) +} + +func gobDecodeBool(b *bool, data []byte) error { + g := gob.NewDecoder(bytes.NewReader(data)) + return g.Decode(b) +} + +func unmapActorProperties(mm map[string][]byte, a *Actor) error { + err := OnObject(a, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["inbox"]; ok { + if a.Inbox, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["outbox"]; ok { + if a.Outbox, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["following"]; ok { + if a.Following, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["followers"]; ok { + if a.Followers, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["liked"]; ok { + if a.Liked, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["preferredUsername"]; ok { + if a.PreferredUsername, err = gobDecodeNaturalLanguageValues(raw); err != nil { + return err + } + } + if raw, ok := mm["endpoints"]; ok { + if err = a.Endpoints.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["streams"]; ok { + if a.Streams, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["publicKey"]; ok { + if err = a.PublicKey.GobDecode(raw); err != nil { + return err + } + } + return nil +} + +func unmapIntransitiveActivityProperties(mm map[string][]byte, act *IntransitiveActivity) error { + err := OnObject(act, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["actor"]; ok { + if act.Actor, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["target"]; ok { + if act.Target, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["result"]; ok { + if act.Result, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["origin"]; ok { + if act.Origin, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["instrument"]; ok { + if act.Instrument, err = gobDecodeItem(raw); err != nil { + return err + } + } + return nil +} + +func unmapActivityProperties(mm map[string][]byte, act *Activity) error { + err := OnIntransitiveActivity(act, func(act *IntransitiveActivity) error { + return unmapIntransitiveActivityProperties(mm, act) + }) + if err != nil { + return err + } + if raw, ok := mm["object"]; ok { + if act.Object, err = gobDecodeItem(raw); err != nil { + return err + } + } + return nil +} + +func unmapLinkProperties(mm map[string][]byte, l *Link) error { + if raw, ok := mm["id"]; ok { + if err := l.ID.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["type"]; ok { + if err := l.Type.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["mediaType"]; ok { + if err := l.MediaType.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["href"]; ok { + if err := l.Href.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["hrefLang"]; ok { + if err := l.HrefLang.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["name"]; ok { + if err := l.Name.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["rel"]; ok { + if err := l.Rel.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["width"]; ok { + if err := gobDecodeUint(&l.Width, raw); err != nil { + return err + } + } + if raw, ok := mm["height"]; ok { + if err := gobDecodeUint(&l.Height, raw); err != nil { + return err + } + } + return nil +} + +func unmapObjectProperties(mm map[string][]byte, o *Object) error { + var err error + if raw, ok := mm["id"]; ok { + if err = o.ID.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["type"]; ok { + if err = o.Type.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["name"]; ok { + if err = o.Name.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["attachment"]; ok { + if o.Attachment, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["attributedTo"]; ok { + if o.AttributedTo, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["audience"]; ok { + if o.Audience, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["content"]; ok { + if o.Content, err = gobDecodeNaturalLanguageValues(raw); err != nil { + return err + } + } + if raw, ok := mm["context"]; ok { + if o.Context, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["mediaType"]; ok { + if err = o.MediaType.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["endTime"]; ok { + if err = o.EndTime.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["generator"]; ok { + if o.Generator, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["icon"]; ok { + if o.Icon, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["image"]; ok { + if o.Image, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["inReplyTo"]; ok { + if o.InReplyTo, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["location"]; ok { + if o.Location, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["preview"]; ok { + if o.Preview, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["published"]; ok { + if err = o.Published.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["replies"]; ok { + if o.Replies, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["startTime"]; ok { + if err = o.StartTime.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["summary"]; ok { + if o.Summary, err = gobDecodeNaturalLanguageValues(raw); err != nil { + return err + } + } + if raw, ok := mm["tag"]; ok { + if o.Tag, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["updated"]; ok { + if err = o.Updated.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["url"]; ok { + if o.URL, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["to"]; ok { + if o.To, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["bto"]; ok { + if o.Bto, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["cc"]; ok { + if o.CC, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["bcc"]; ok { + if o.BCC, err = gobDecodeItems(raw); err != nil { + return err + } + } + if raw, ok := mm["duration"]; ok { + if o.Duration, err = gobDecodeDuration(raw); err != nil { + return err + } + } + if raw, ok := mm["likes"]; ok { + if o.Likes, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["shares"]; ok { + if o.Shares, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["source"]; ok { + if err := o.Source.GobDecode(raw); err != nil { + return err + } + } + return nil +} + +func tryDecodeItems(items *ItemCollection, data []byte) error { + tt := make([][]byte, 0) + g := gob.NewDecoder(bytes.NewReader(data)) + if err := g.Decode(&tt); err != nil { + return err + } + for _, it := range tt { + ob, err := gobDecodeItem(it) + if err != nil { + return err + } + *items = append(*items, ob) + } + return nil +} + +func tryDecodeIRIs(iris *IRIs, data []byte) error { + return iris.GobDecode(data) +} + +func tryDecodeIRI(iri *IRI, data []byte) error { + return iri.GobDecode(data) +} + +func gobDecodeDuration(data []byte) (time.Duration, error) { + var d time.Duration + err := gob.NewDecoder(bytes.NewReader(data)).Decode(&d) + return d, err +} + +func gobDecodeNaturalLanguageValues(data []byte) (NaturalLanguageValues, error) { + n := make(NaturalLanguageValues, 0) + err := n.GobDecode(data) + return n, err +} + +func gobDecodeItems(data []byte) (ItemCollection, error) { + items := make(ItemCollection, 0) + if err := tryDecodeItems(&items, data); err != nil { + return nil, err + } + return items, nil +} + +func gobDecodeItem(data []byte) (Item, error) { + items := make(ItemCollection, 0) + if err := tryDecodeItems(&items, data); err == nil { + return items, nil + } + iris := make(IRIs, 0) + if err := tryDecodeIRIs(&iris, data); err == nil { + return iris, nil + } + isObject := false + typ := ActivityVocabularyType("") + mm, err := gobDecodeObjectAsMap(data) + if err == nil { + var sTyp []byte + sTyp, isObject = mm["type"] + if isObject { + typ = ActivityVocabularyType(sTyp) + } else { + _, isObject = mm["id"] + } + } + if isObject { + it, err := ItemTyperFunc(typ) + if err != nil { + return nil, err + } + switch it.GetType() { + case IRIType: + case "", ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: + err = OnObject(it, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + case LinkType, MentionType: + err = OnLink(it, func(l *Link) error { + return unmapLinkProperties(mm, l) + }) + case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, + FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, + RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: + err = OnActivity(it, func(act *Activity) error { + return unmapActivityProperties(mm, act) + }) + case IntransitiveActivityType, ArriveType, TravelType: + err = OnIntransitiveActivity(it, func(act *IntransitiveActivity) error { + return unmapIntransitiveActivityProperties(mm, act) + }) + case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: + err = OnActor(it, func(a *Actor) error { + return unmapActorProperties(mm, a) + }) + case CollectionType: + err = OnCollection(it, func(c *Collection) error { + return unmapCollectionProperties(mm, c) + }) + case OrderedCollectionType: + err = OnOrderedCollection(it, func(c *OrderedCollection) error { + return unmapOrderedCollectionProperties(mm, c) + }) + case CollectionPageType: + err = OnCollectionPage(it, func(p *CollectionPage) error { + return unmapCollectionPageProperties(mm, p) + }) + case OrderedCollectionPageType: + err = OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error { + return unmapOrderedCollectionPageProperties(mm, p) + }) + case PlaceType: + err = OnPlace(it, func(p *Place) error { + return unmapPlaceProperties(mm, p) + }) + case ProfileType: + err = OnProfile(it, func(p *Profile) error { + return unmapProfileProperties(mm, p) + }) + case RelationshipType: + err = OnRelationship(it, func(r *Relationship) error { + return unmapRelationshipProperties(mm, r) + }) + case TombstoneType: + err = OnTombstone(it, func(t *Tombstone) error { + return unmapTombstoneProperties(mm, t) + }) + case QuestionType: + err = OnQuestion(it, func(q *Question) error { + return unmapQuestionProperties(mm, q) + }) + } + return it, err + } + iri := IRI("") + if err := tryDecodeIRI(&iri, data); err == nil { + return iri, err + } + + return nil, errors.New("unable to gob decode to any known ActivityPub types") +} + +func gobDecodeObjectAsMap(data []byte) (map[string][]byte, error) { + mm := make(map[string][]byte) + g := gob.NewDecoder(bytes.NewReader(data)) + if err := g.Decode(&mm); err != nil { + return nil, err + } + return mm, nil +} + +func unmapIncompleteCollectionProperties(mm map[string][]byte, c *Collection) error { + err := OnObject(c, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["current"]; ok { + if c.Current, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["first"]; ok { + if c.First, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["last"]; ok { + if c.Last, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["totalItems"]; ok { + if err = gobDecodeUint(&c.TotalItems, raw); err != nil { + return err + } + } + return nil +} + +func unmapCollectionProperties(mm map[string][]byte, c *Collection) error { + err := unmapIncompleteCollectionProperties(mm, c) + if err != nil { + return err + } + if raw, ok := mm["items"]; ok { + if c.Items, err = gobDecodeItems(raw); err != nil { + return err + } + } + return err +} + +func unmapCollectionPageProperties(mm map[string][]byte, c *CollectionPage) error { + err := OnCollection(c, func(c *Collection) error { + return unmapCollectionProperties(mm, c) + }) + if err != nil { + return err + } + if raw, ok := mm["partOf"]; ok { + if c.PartOf, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["next"]; ok { + if c.Next, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["prev"]; ok { + if c.Prev, err = gobDecodeItem(raw); err != nil { + return err + } + } + return err +} + +func unmapOrderedCollectionProperties(mm map[string][]byte, o *OrderedCollection) error { + err := OnCollection(o, func(c *Collection) error { + return unmapIncompleteCollectionProperties(mm, c) + }) + if err != nil { + return err + } + if raw, ok := mm["orderedItems"]; ok { + if o.OrderedItems, err = gobDecodeItems(raw); err != nil { + return err + } + } + return err +} + +func unmapOrderedCollectionPageProperties(mm map[string][]byte, c *OrderedCollectionPage) error { + err := OnOrderedCollection(c, func(c *OrderedCollection) error { + return unmapOrderedCollectionProperties(mm, c) + }) + if err != nil { + return err + } + if raw, ok := mm["partOf"]; ok { + if c.PartOf, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["next"]; ok { + if c.Next, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["prev"]; ok { + if c.Prev, err = gobDecodeItem(raw); err != nil { + return err + } + } + return err +} + +func unmapPlaceProperties(mm map[string][]byte, p *Place) error { + err := OnObject(p, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["accuracy"]; ok { + if err = gobDecodeFloat64(&p.Accuracy, raw); err != nil { + return err + } + } + if raw, ok := mm["altitude"]; ok { + if err = gobDecodeFloat64(&p.Altitude, raw); err != nil { + return err + } + } + if raw, ok := mm["latitude"]; ok { + if err = gobDecodeFloat64(&p.Latitude, raw); err != nil { + return err + } + } + if raw, ok := mm["radius"]; ok { + if err = gobDecodeInt64(&p.Radius, raw); err != nil { + return err + } + } + if raw, ok := mm["units"]; ok { + p.Units = string(raw) + } + return nil +} + +func unmapProfileProperties(mm map[string][]byte, p *Profile) error { + err := OnObject(p, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["Describes"]; ok { + if p.Describes, err = gobDecodeItem(raw); err != nil { + return err + } + } + return nil +} + +func unmapRelationshipProperties(mm map[string][]byte, r *Relationship) error { + err := OnObject(r, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["subject"]; ok { + if r.Subject, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["object"]; ok { + if r.Object, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["relationship"]; ok { + if r.Relationship, err = gobDecodeItem(raw); err != nil { + return err + } + } + return nil +} + +func unmapTombstoneProperties(mm map[string][]byte, t *Tombstone) error { + err := OnObject(t, func(ob *Object) error { + return unmapObjectProperties(mm, ob) + }) + if err != nil { + return err + } + if raw, ok := mm["formerType"]; ok { + if err = t.FormerType.GobDecode(raw); err != nil { + return err + } + } + if raw, ok := mm["deleted"]; ok { + if err = t.Deleted.GobDecode(raw); err != nil { + return err + } + } + return nil +} + +func unmapQuestionProperties(mm map[string][]byte, q *Question) error { + err := OnIntransitiveActivity(q, func(act *IntransitiveActivity) error { + return unmapIntransitiveActivityProperties(mm, act) + }) + if err != nil { + return err + } + if raw, ok := mm["oneOf"]; ok { + if q.OneOf, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["anyOf"]; ok { + if q.AnyOf, err = gobDecodeItem(raw); err != nil { + return err + } + } + if raw, ok := mm["closed"]; ok { + if err = gobDecodeBool(&q.Closed, raw); err != nil { + return err + } + } + return nil +} diff --git a/vendor/github.com/go-ap/activitypub/decoding_json.go b/vendor/github.com/go-ap/activitypub/decoding_json.go new file mode 100644 index 0000000..38bb1d3 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/decoding_json.go @@ -0,0 +1,662 @@ +package activitypub + +import ( + "encoding" + "encoding/json" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/valyala/fastjson" +) + +var ( + apUnmarshalerType = reflect.TypeOf(new(Item)).Elem() + unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem() + textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() +) + +// ItemTyperFunc will return an instance of a struct that implements activitypub.Item +// The default for this package is GetItemByType but can be overwritten +var ItemTyperFunc TyperFn = GetItemByType + +// JSONItemUnmarshal can be set externally to populate a custom object based on its type +var JSONItemUnmarshal JSONUnmarshalerFn = nil + +// IsNotEmpty checks if an object is empty +var IsNotEmpty NotEmptyCheckerFn = NotEmpty + +// TyperFn is the type of the function which returns an Item struct instance +// for a specific ActivityVocabularyType +type TyperFn func(ActivityVocabularyType) (Item, error) + +// JSONUnmarshalerFn is the type of the function that will load the data from a fastjson.Value into an Item +// that the current package doesn't know about. +type JSONUnmarshalerFn func(ActivityVocabularyType, *fastjson.Value, Item) error + +// NotEmptyCheckerFn is the type of the function that checks if an object is empty +type NotEmptyCheckerFn func(Item) bool + +func JSONGetID(val *fastjson.Value) ID { + i := val.Get("id").GetStringBytes() + return ID(i) +} + +func JSONGetType(val *fastjson.Value) ActivityVocabularyType { + t := val.Get("type").GetStringBytes() + return ActivityVocabularyType(t) +} + +func JSONGetMimeType(val *fastjson.Value, prop string) MimeType { + if !val.Exists(prop) { + return "" + } + t := val.GetStringBytes(prop) + return MimeType(t) +} + +func JSONGetInt(val *fastjson.Value, prop string) int64 { + if !val.Exists(prop) { + return 0 + } + i := val.Get(prop).GetInt64() + return i +} + +func JSONGetFloat(val *fastjson.Value, prop string) float64 { + if !val.Exists(prop) { + return 0.0 + } + f := val.Get(prop).GetFloat64() + return f +} + +func JSONGetString(val *fastjson.Value, prop string) string { + if !val.Exists(prop) { + return "" + } + s := val.Get(prop).GetStringBytes() + return string(s) +} + +func JSONGetBytes(val *fastjson.Value, prop string) []byte { + if !val.Exists(prop) { + return nil + } + s := val.Get(prop).GetStringBytes() + return s +} + +func JSONGetBoolean(val *fastjson.Value, prop string) bool { + if !val.Exists(prop) { + return false + } + t, _ := val.Get(prop).Bool() + return t +} + +func JSONGetNaturalLanguageField(val *fastjson.Value, prop string) NaturalLanguageValues { + n := NaturalLanguageValues{} + if val == nil { + return n + } + v := val.Get(prop) + if v == nil { + return nil + } + switch v.Type() { + case fastjson.TypeObject: + ob, _ := v.Object() + ob.Visit(func(key []byte, v *fastjson.Value) { + l := LangRefValue{} + l.Ref = LangRef(key) + if err := l.Value.UnmarshalJSON(v.GetStringBytes()); err == nil { + if l.Ref != NilLangRef || len(l.Value) > 0 { + n = append(n, l) + } + } + }) + case fastjson.TypeString: + l := LangRefValue{} + if err := l.UnmarshalJSON(v.GetStringBytes()); err == nil { + n = append(n, l) + } + } + + return n +} + +func JSONGetTime(val *fastjson.Value, prop string) time.Time { + t := time.Time{} + if val == nil { + return t + } + + if str := val.Get(prop).GetStringBytes(); len(str) > 0 { + t.UnmarshalText(str) + return t.UTC() + } + return t +} + +func JSONGetDuration(val *fastjson.Value, prop string) time.Duration { + if str := val.Get(prop).GetStringBytes(); len(str) > 0 { + // TODO(marius): this needs to be replaced to be compatible with xsd:duration + d, _ := time.ParseDuration(string(str)) + return d + } + return 0 +} + +func JSONGetPublicKey(val *fastjson.Value, prop string) PublicKey { + key := PublicKey{} + if val == nil { + return key + } + val = val.Get(prop) + if val == nil { + return key + } + JSONLoadPublicKey(val, &key) + return key +} + +func JSONItemsFn(val *fastjson.Value) (Item, error) { + if val.Type() == fastjson.TypeArray { + it := val.GetArray() + items := make(ItemCollection, 0) + for _, v := range it { + if it, _ := JSONLoadItem(v); it != nil { + items.Append(it) + } + } + return items, nil + } + return JSONLoadItem(val) +} + +func JSONLoadItem(val *fastjson.Value) (Item, error) { + typ := JSONGetType(val) + if typ == "" && val.Type() == fastjson.TypeString { + // try to see if it's an IRI + if i, ok := asIRI(val); ok { + return i, nil + } + } + i, err := ItemTyperFunc(typ) + if err != nil || IsNil(i) { + return nil, nil + } + var empty = func(i Item) bool { return !IsNotEmpty(i) } + + switch typ { + case "": + // NOTE(marius): this handles Tags which usually don't have types + fallthrough + case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: + err = OnObject(i, func(ob *Object) error { + return JSONLoadObject(val, ob) + }) + case LinkType, MentionType: + err = OnLink(i, func(l *Link) error { + return JSONLoadLink(val, l) + }) + case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, + FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, + RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: + err = OnActivity(i, func(act *Activity) error { + return JSONLoadActivity(val, act) + }) + case IntransitiveActivityType, ArriveType, TravelType: + err = OnIntransitiveActivity(i, func(act *IntransitiveActivity) error { + return JSONLoadIntransitiveActivity(val, act) + }) + case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: + err = OnActor(i, func(a *Actor) error { + return JSONLoadActor(val, a) + }) + case CollectionType: + err = OnCollection(i, func(c *Collection) error { + return JSONLoadCollection(val, c) + }) + case OrderedCollectionType: + err = OnOrderedCollection(i, func(c *OrderedCollection) error { + return JSONLoadOrderedCollection(val, c) + }) + case CollectionPageType: + err = OnCollectionPage(i, func(p *CollectionPage) error { + return JSONLoadCollectionPage(val, p) + }) + case OrderedCollectionPageType: + err = OnOrderedCollectionPage(i, func(p *OrderedCollectionPage) error { + return JSONLoadOrderedCollectionPage(val, p) + }) + case PlaceType: + err = OnPlace(i, func(p *Place) error { + return JSONLoadPlace(val, p) + }) + case ProfileType: + err = OnProfile(i, func(p *Profile) error { + return JSONLoadProfile(val, p) + }) + case RelationshipType: + err = OnRelationship(i, func(r *Relationship) error { + return JSONLoadRelationship(val, r) + }) + case TombstoneType: + err = OnTombstone(i, func(t *Tombstone) error { + return JSONLoadTombstone(val, t) + }) + case QuestionType: + err = OnQuestion(i, func(q *Question) error { + return JSONLoadQuestion(val, q) + }) + default: + if JSONItemUnmarshal == nil { + return nil, fmt.Errorf("unable to unmarshal custom type %s, you need to set a correct function for JSONItemUnmarshal", typ) + } + err = JSONItemUnmarshal(typ, val, i) + } + if err != nil { + return nil, err + } + if empty(i) { + return nil, nil + } + + return i, nil +} + +func JSONUnmarshalToItem(val *fastjson.Value) Item { + var ( + i Item + err error + ) + switch val.Type() { + case fastjson.TypeArray: + i, err = JSONItemsFn(val) + case fastjson.TypeObject: + i, err = JSONLoadItem(val) + case fastjson.TypeString: + if iri, ok := asIRI(val); ok { + // try to see if it's an IRI + i = iri + } + } + if err != nil { + return nil + } + return i +} + +func asIRI(val *fastjson.Value) (IRI, bool) { + if val == nil { + return NilIRI, true + } + s := strings.Trim(val.String(), `"`) + u, err := url.ParseRequestURI(s) + if err == nil && len(u.Scheme) > 0 && len(u.Host) > 0 { + // try to see if it's an IRI + return IRI(s), true + } + return EmptyIRI, false +} + +func JSONGetItem(val *fastjson.Value, prop string) Item { + if val == nil { + return nil + } + if val = val.Get(prop); val == nil { + return nil + } + switch val.Type() { + case fastjson.TypeString: + if i, ok := asIRI(val); ok { + // try to see if it's an IRI + return i + } + case fastjson.TypeArray: + it, _ := JSONItemsFn(val) + return it + case fastjson.TypeObject: + it, _ := JSONLoadItem(val) + return it + case fastjson.TypeNumber: + fallthrough + case fastjson.TypeNull: + fallthrough + default: + return nil + } + return nil +} + +func JSONGetURIItem(val *fastjson.Value, prop string) Item { + if val == nil { + return nil + } + if val = val.Get(prop); val == nil { + return nil + } + switch val.Type() { + case fastjson.TypeObject: + if it, _ := JSONLoadItem(val); it != nil { + return it + } + case fastjson.TypeArray: + if it, _ := JSONItemsFn(val); it != nil { + return it + } + case fastjson.TypeString: + return IRI(val.GetStringBytes()) + } + + return nil +} + +func JSONGetItems(val *fastjson.Value, prop string) ItemCollection { + if val == nil { + return nil + } + if val = val.Get(prop); val == nil { + return nil + } + + it := make(ItemCollection, 0) + switch val.Type() { + case fastjson.TypeArray: + for _, v := range val.GetArray() { + if i, _ := JSONLoadItem(v); i != nil { + it.Append(i) + } + } + case fastjson.TypeObject: + if i := JSONGetItem(val, prop); i != nil { + it.Append(i) + } + case fastjson.TypeString: + if iri := val.GetStringBytes(); len(iri) > 0 { + it.Append(IRI(iri)) + } + } + if len(it) == 0 { + return nil + } + return it +} + +func JSONGetLangRefField(val *fastjson.Value, prop string) LangRef { + s := val.Get(prop).GetStringBytes() + return LangRef(s) +} + +func JSONGetIRI(val *fastjson.Value, prop string) IRI { + s := val.Get(prop).GetStringBytes() + return IRI(s) +} + +// UnmarshalJSON tries to detect the type of the object in the json data and then outputs a matching +// ActivityStreams object, if possible +func UnmarshalJSON(data []byte) (Item, error) { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return nil, err + } + return JSONUnmarshalToItem(val), nil +} + +func GetItemByType(typ ActivityVocabularyType) (Item, error) { + switch typ { + case ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: + return ObjectNew(typ), nil + case LinkType, MentionType: + return &Link{Type: typ}, nil + case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, + FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, + RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: + return &Activity{Type: typ}, nil + case IntransitiveActivityType, ArriveType, TravelType: + return &IntransitiveActivity{Type: typ}, nil + case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: + return &Actor{Type: typ}, nil + case CollectionType: + return &Collection{Type: typ}, nil + case OrderedCollectionType: + return &OrderedCollection{Type: typ}, nil + case CollectionPageType: + return &CollectionPage{Type: typ}, nil + case OrderedCollectionPageType: + return &OrderedCollectionPage{Type: typ}, nil + case PlaceType: + return &Place{Type: typ}, nil + case ProfileType: + return &Profile{Type: typ}, nil + case RelationshipType: + return &Relationship{Type: typ}, nil + case TombstoneType: + return &Tombstone{Type: typ}, nil + case QuestionType: + return &Question{Type: typ}, nil + case "": + fallthrough + default: + // when no type is available use a plain Object + return &Object{}, nil + } + return nil, fmt.Errorf("empty ActivityStreams type") +} + +func JSONGetActorEndpoints(val *fastjson.Value, prop string) *Endpoints { + if val == nil { + return nil + } + if val = val.Get(prop); val == nil { + return nil + } + + e := Endpoints{} + e.UploadMedia = JSONGetURIItem(val, "uploadMedia") + e.OauthAuthorizationEndpoint = JSONGetURIItem(val, "oauthAuthorizationEndpoint") + e.OauthTokenEndpoint = JSONGetURIItem(val, "oauthTokenEndpoint") + e.SharedInbox = JSONGetURIItem(val, "sharedInbox") + e.ProvideClientKey = JSONGetURIItem(val, "provideClientKey") + e.SignClientKey = JSONGetURIItem(val, "signClientKey") + + return &e +} + +func JSONLoadObject(val *fastjson.Value, o *Object) error { + o.ID = JSONGetID(val) + o.Type = JSONGetType(val) + o.Name = JSONGetNaturalLanguageField(val, "name") + o.Content = JSONGetNaturalLanguageField(val, "content") + o.Summary = JSONGetNaturalLanguageField(val, "summary") + o.Context = JSONGetItem(val, "context") + o.URL = JSONGetURIItem(val, "url") + o.MediaType = JSONGetMimeType(val, "mediaType") + o.Generator = JSONGetItem(val, "generator") + o.AttributedTo = JSONGetItem(val, "attributedTo") + o.Attachment = JSONGetItem(val, "attachment") + o.Location = JSONGetItem(val, "location") + o.Published = JSONGetTime(val, "published") + o.StartTime = JSONGetTime(val, "startTime") + o.EndTime = JSONGetTime(val, "endTime") + o.Duration = JSONGetDuration(val, "duration") + o.Icon = JSONGetItem(val, "icon") + o.Preview = JSONGetItem(val, "preview") + o.Image = JSONGetItem(val, "image") + o.Updated = JSONGetTime(val, "updated") + o.InReplyTo = JSONGetItem(val, "inReplyTo") + o.To = JSONGetItems(val, "to") + o.Audience = JSONGetItems(val, "audience") + o.Bto = JSONGetItems(val, "bto") + o.CC = JSONGetItems(val, "cc") + o.BCC = JSONGetItems(val, "bcc") + o.Replies = JSONGetItem(val, "replies") + o.Tag = JSONGetItems(val, "tag") + o.Likes = JSONGetItem(val, "likes") + o.Shares = JSONGetItem(val, "shares") + o.Source = GetAPSource(val) + return nil +} + +func JSONLoadIntransitiveActivity(val *fastjson.Value, i *IntransitiveActivity) error { + i.Actor = JSONGetItem(val, "actor") + i.Target = JSONGetItem(val, "target") + i.Result = JSONGetItem(val, "result") + i.Origin = JSONGetItem(val, "origin") + i.Instrument = JSONGetItem(val, "instrument") + return OnObject(i, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadActivity(val *fastjson.Value, a *Activity) error { + a.Object = JSONGetItem(val, "object") + return OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + return JSONLoadIntransitiveActivity(val, i) + }) +} + +func JSONLoadQuestion(val *fastjson.Value, q *Question) error { + q.OneOf = JSONGetItem(val, "oneOf") + q.AnyOf = JSONGetItem(val, "anyOf") + q.Closed = JSONGetBoolean(val, "closed") + return OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + return JSONLoadIntransitiveActivity(val, i) + }) +} + +func JSONLoadActor(val *fastjson.Value, a *Actor) error { + a.PreferredUsername = JSONGetNaturalLanguageField(val, "preferredUsername") + a.Followers = JSONGetItem(val, "followers") + a.Following = JSONGetItem(val, "following") + a.Inbox = JSONGetItem(val, "inbox") + a.Outbox = JSONGetItem(val, "outbox") + a.Liked = JSONGetItem(val, "liked") + a.Endpoints = JSONGetActorEndpoints(val, "endpoints") + a.Streams = JSONGetItems(val, "streams") + a.PublicKey = JSONGetPublicKey(val, "publicKey") + return OnObject(a, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadCollection(val *fastjson.Value, c *Collection) error { + c.Current = JSONGetItem(val, "current") + c.First = JSONGetItem(val, "first") + c.Last = JSONGetItem(val, "last") + c.TotalItems = uint(JSONGetInt(val, "totalItems")) + c.Items = JSONGetItems(val, "items") + return OnObject(c, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadCollectionPage(val *fastjson.Value, c *CollectionPage) error { + c.Next = JSONGetItem(val, "next") + c.Prev = JSONGetItem(val, "prev") + c.PartOf = JSONGetItem(val, "partOf") + return OnCollection(c, func(c *Collection) error { + return JSONLoadCollection(val, c) + }) +} + +func JSONLoadOrderedCollection(val *fastjson.Value, c *OrderedCollection) error { + c.Current = JSONGetItem(val, "current") + c.First = JSONGetItem(val, "first") + c.Last = JSONGetItem(val, "last") + c.TotalItems = uint(JSONGetInt(val, "totalItems")) + c.OrderedItems = JSONGetItems(val, "orderedItems") + return OnObject(c, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadOrderedCollectionPage(val *fastjson.Value, c *OrderedCollectionPage) error { + c.Next = JSONGetItem(val, "next") + c.Prev = JSONGetItem(val, "prev") + c.PartOf = JSONGetItem(val, "partOf") + c.StartIndex = uint(JSONGetInt(val, "startIndex")) + return OnOrderedCollection(c, func(c *OrderedCollection) error { + return JSONLoadOrderedCollection(val, c) + }) +} + +func JSONLoadPlace(val *fastjson.Value, p *Place) error { + p.Accuracy = JSONGetFloat(val, "accuracy") + p.Altitude = JSONGetFloat(val, "altitude") + p.Latitude = JSONGetFloat(val, "latitude") + p.Longitude = JSONGetFloat(val, "longitude") + p.Radius = JSONGetInt(val, "radius") + p.Units = JSONGetString(val, "units") + return OnObject(p, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadProfile(val *fastjson.Value, p *Profile) error { + p.Describes = JSONGetItem(val, "describes") + return OnObject(p, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadRelationship(val *fastjson.Value, r *Relationship) error { + r.Subject = JSONGetItem(val, "subject") + r.Object = JSONGetItem(val, "object") + r.Relationship = JSONGetItem(val, "relationship") + return OnObject(r, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadTombstone(val *fastjson.Value, t *Tombstone) error { + t.FormerType = ActivityVocabularyType(JSONGetString(val, "formerType")) + t.Deleted = JSONGetTime(val, "deleted") + return OnObject(t, func(o *Object) error { + return JSONLoadObject(val, o) + }) +} + +func JSONLoadLink(val *fastjson.Value, l *Link) error { + l.ID = JSONGetID(val) + l.Type = JSONGetType(val) + l.MediaType = JSONGetMimeType(val, "mediaType") + l.Preview = JSONGetItem(val, "preview") + if h := JSONGetInt(val, "height"); h != 0 { + l.Height = uint(h) + } + if w := JSONGetInt(val, "width"); w != 0 { + l.Width = uint(w) + } + l.Name = JSONGetNaturalLanguageField(val, "name") + if hrefLang := JSONGetLangRefField(val, "hrefLang"); len(hrefLang) > 0 { + l.HrefLang = hrefLang + } + if href := JSONGetURIItem(val, "href"); href != nil { + ll := href.GetLink() + if len(ll) > 0 { + l.Href = ll + } + } + if rel := JSONGetURIItem(val, "rel"); rel != nil { + rr := rel.GetLink() + if len(rr) > 0 { + l.Rel = rr + } + } + return nil +} + +func JSONLoadPublicKey(val *fastjson.Value, p *PublicKey) error { + p.ID = JSONGetID(val) + p.Owner = JSONGetIRI(val, "owner") + if pub := val.GetStringBytes("publicKeyPem"); len(pub) > 0 { + p.PublicKeyPem = string(pub) + } + return nil +} diff --git a/vendor/github.com/go-ap/activitypub/encoding_gob.go b/vendor/github.com/go-ap/activitypub/encoding_gob.go new file mode 100644 index 0000000..fca6d2d --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/encoding_gob.go @@ -0,0 +1,792 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" +) + +func GobEncode(it Item) ([]byte, error) { + return gobEncodeItem(it) +} + +// TODO(marius): when migrating to go1.18, use a numeric constraint for this +func gobEncodeInt64(i int64) ([]byte, error) { + b := bytes.Buffer{} + gg := gob.NewEncoder(&b) + if err := gg.Encode(i); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// TODO(marius): when migrating to go1.18, use a numeric constraint for this +func gobEncodeUint(i uint) ([]byte, error) { + b := bytes.Buffer{} + gg := gob.NewEncoder(&b) + if err := gg.Encode(i); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func gobEncodeFloat64(f float64) ([]byte, error) { + b := bytes.Buffer{} + gg := gob.NewEncoder(&b) + if err := gg.Encode(f); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func gobEncodeBool(t bool) ([]byte, error) { + b := bytes.Buffer{} + gg := gob.NewEncoder(&b) + if err := gg.Encode(t); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +func gobEncodeStringLikeType(g *gob.Encoder, s []byte) error { + if err := g.Encode(s); err != nil { + return err + } + return nil +} + +func gobEncodeItems(col ItemCollection) ([]byte, error) { + b := bytes.Buffer{} + tt := make([][]byte, 0) + for _, it := range col.Collection() { + single, err := gobEncodeItem(it) + if err != nil { + return nil, err + } + tt = append(tt, single) + } + err := gob.NewEncoder(&b).Encode(tt) + return b.Bytes(), err +} + +func gobEncodeIRIs(col IRIs) ([]byte, error) { + b := bytes.Buffer{} + err := gob.NewEncoder(&b).Encode(col) + return b.Bytes(), err +} + +func gobEncodeItemOrLink(it LinkOrIRI) ([]byte, error) { + if ob, ok := it.(Item); ok { + return gobEncodeItem(ob) + } + b := bytes.Buffer{} + err := OnLink(it, func(l *Link) error { + bytes, err := l.GobEncode() + b.Write(bytes) + return err + }) + return b.Bytes(), err +} + +func gobEncodeItem(it Item) ([]byte, error) { + if IsIRI(it) { + if i, ok := it.(IRI); ok { + return []byte(i), nil + } + return []byte{}, nil + } + b := bytes.Buffer{} + var err error + if IsIRIs(it) { + err = OnIRIs(it, func(iris *IRIs) error { + bytes, err := gobEncodeIRIs(*iris) + b.Write(bytes) + return err + }) + } + if IsItemCollection(it) { + err = OnItemCollection(it, func(col *ItemCollection) error { + bytes, err := gobEncodeItems(*col) + b.Write(bytes) + return err + }) + } + if IsObject(it) { + switch it.GetType() { + case IRIType: + var bytes []byte + bytes, err = it.(IRI).GobEncode() + b.Write(bytes) + case "", ObjectType, ArticleType, AudioType, DocumentType, EventType, ImageType, NoteType, PageType, VideoType: + err = OnObject(it, func(ob *Object) error { + bytes, err := ob.GobEncode() + b.Write(bytes) + return err + }) + case LinkType, MentionType: + // TODO(marius): this shouldn't work, as Link does not implement Item? (or rather, should not) + err = OnLink(it, func(l *Link) error { + bytes, err := l.GobEncode() + b.Write(bytes) + return err + }) + case ActivityType, AcceptType, AddType, AnnounceType, BlockType, CreateType, DeleteType, DislikeType, + FlagType, FollowType, IgnoreType, InviteType, JoinType, LeaveType, LikeType, ListenType, MoveType, OfferType, + RejectType, ReadType, RemoveType, TentativeRejectType, TentativeAcceptType, UndoType, UpdateType, ViewType: + err = OnActivity(it, func(act *Activity) error { + bytes, err := act.GobEncode() + b.Write(bytes) + return err + }) + case IntransitiveActivityType, ArriveType, TravelType: + err = OnIntransitiveActivity(it, func(act *IntransitiveActivity) error { + bytes, err := act.GobEncode() + b.Write(bytes) + return err + }) + case ActorType, ApplicationType, GroupType, OrganizationType, PersonType, ServiceType: + err = OnActor(it, func(a *Actor) error { + bytes, err := a.GobEncode() + b.Write(bytes) + return err + }) + case CollectionType: + err = OnCollection(it, func(c *Collection) error { + bytes, err := c.GobEncode() + b.Write(bytes) + return err + }) + case OrderedCollectionType: + err = OnOrderedCollection(it, func(c *OrderedCollection) error { + bytes, err := c.GobEncode() + b.Write(bytes) + return err + }) + case CollectionPageType: + err = OnCollectionPage(it, func(p *CollectionPage) error { + bytes, err := p.GobEncode() + b.Write(bytes) + return err + }) + case OrderedCollectionPageType: + err = OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error { + bytes, err := p.GobEncode() + b.Write(bytes) + return err + }) + case PlaceType: + err = OnPlace(it, func(p *Place) error { + bytes, err := p.GobEncode() + b.Write(bytes) + return err + }) + case ProfileType: + err = OnProfile(it, func(p *Profile) error { + bytes, err := p.GobEncode() + b.Write(bytes) + return err + }) + case RelationshipType: + err = OnRelationship(it, func(r *Relationship) error { + bytes, err := r.GobEncode() + b.Write(bytes) + return err + }) + case TombstoneType: + err = OnTombstone(it, func(t *Tombstone) error { + bytes, err := t.GobEncode() + b.Write(bytes) + return err + }) + case QuestionType: + err = OnQuestion(it, func(q *Question) error { + bytes, err := q.GobEncode() + b.Write(bytes) + return err + }) + } + } + return b.Bytes(), err +} + +func mapObjectProperties(mm map[string][]byte, o *Object) (hasData bool, err error) { + if len(o.ID) > 0 { + if mm["id"], err = o.ID.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if len(o.Type) > 0 { + if mm["type"], err = o.Type.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if len(o.MediaType) > 0 { + if mm["mediaType"], err = o.MediaType.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if len(o.Name) > 0 { + if mm["name"], err = o.Name.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Attachment != nil { + if mm["attachment"], err = gobEncodeItem(o.Attachment); err != nil { + return hasData, err + } + hasData = true + } + if o.AttributedTo != nil { + if mm["attributedTo"], err = gobEncodeItem(o.AttributedTo); err != nil { + return hasData, err + } + hasData = true + } + if o.Audience != nil { + if mm["audience"], err = gobEncodeItem(o.Audience); err != nil { + return hasData, err + } + hasData = true + } + if o.Content != nil { + if mm["content"], err = o.Content.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Context != nil { + if mm["context"], err = gobEncodeItem(o.Context); err != nil { + return hasData, err + } + hasData = true + } + if len(o.MediaType) > 0 { + if mm["mediaType"], err = o.MediaType.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if !o.EndTime.IsZero() { + if mm["endTime"], err = o.EndTime.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Generator != nil { + if mm["generator"], err = gobEncodeItem(o.Generator); err != nil { + return hasData, err + } + hasData = true + } + if o.Icon != nil { + if mm["icon"], err = gobEncodeItem(o.Icon); err != nil { + return hasData, err + } + hasData = true + } + if o.Image != nil { + if mm["image"], err = gobEncodeItem(o.Image); err != nil { + return hasData, err + } + hasData = true + } + if o.InReplyTo != nil { + if mm["inReplyTo"], err = gobEncodeItem(o.InReplyTo); err != nil { + return hasData, err + } + hasData = true + } + if o.Location != nil { + if mm["location"], err = gobEncodeItem(o.Location); err != nil { + return hasData, err + } + hasData = true + } + if o.Preview != nil { + if mm["preview"], err = gobEncodeItem(o.Preview); err != nil { + return hasData, err + } + hasData = true + } + if !o.Published.IsZero() { + if mm["published"], err = o.Published.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Replies != nil { + if mm["replies"], err = gobEncodeItem(o.Replies); err != nil { + return hasData, err + } + hasData = true + } + if !o.StartTime.IsZero() { + if mm["startTime"], err = o.StartTime.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if len(o.Summary) > 0 { + if mm["summary"], err = o.Summary.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Tag != nil { + if mm["tag"], err = gobEncodeItem(o.Tag); err != nil { + return hasData, err + } + hasData = true + } + if !o.Updated.IsZero() { + if mm["updated"], err = o.Updated.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.Tag != nil { + if mm["tag"], err = gobEncodeItem(o.Tag); err != nil { + return hasData, err + } + hasData = true + } + if !o.Updated.IsZero() { + if mm["updated"], err = o.Updated.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if o.URL != nil { + if mm["url"], err = gobEncodeItemOrLink(o.URL); err != nil { + return hasData, err + } + hasData = true + } + if o.To != nil { + if mm["to"], err = gobEncodeItem(o.To); err != nil { + return hasData, err + } + hasData = true + } + if o.Bto != nil { + if mm["bto"], err = gobEncodeItem(o.Bto); err != nil { + return hasData, err + } + hasData = true + } + if o.CC != nil { + if mm["cc"], err = gobEncodeItem(o.CC); err != nil { + return hasData, err + } + hasData = true + } + if o.BCC != nil { + if mm["bcc"], err = gobEncodeItem(o.BCC); err != nil { + return hasData, err + } + hasData = true + } + if o.Duration > 0 { + if mm["duration"], err = gobEncodeInt64(int64(o.Duration)); err != nil { + return hasData, err + } + hasData = true + } + if o.Likes != nil { + if mm["likes"], err = gobEncodeItem(o.Likes); err != nil { + return hasData, err + } + hasData = true + } + if o.Shares != nil { + if mm["shares"], err = gobEncodeItem(o.Shares); err != nil { + return hasData, err + } + hasData = true + } + if o.Shares != nil { + if mm["shares"], err = gobEncodeItem(o.Shares); err != nil { + return hasData, err + } + hasData = true + } + if len(o.Source.MediaType)+len(o.Source.Content) > 0 { + if mm["source"], err = o.Source.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + + return hasData, nil +} + +func mapActorProperties(mm map[string][]byte, a *Actor) (hasData bool, err error) { + err = OnObject(a, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if a.Inbox != nil { + if mm["inbox"], err = gobEncodeItem(a.Inbox); err != nil { + return hasData, err + } + hasData = true + } + if a.Inbox != nil { + if mm["inbox"], err = gobEncodeItem(a.Inbox); err != nil { + return hasData, err + } + hasData = true + } + if a.Outbox != nil { + if mm["outbox"], err = gobEncodeItem(a.Outbox); err != nil { + return hasData, err + } + hasData = true + } + if a.Following != nil { + if mm["following"], err = gobEncodeItem(a.Following); err != nil { + return hasData, err + } + hasData = true + } + if a.Followers != nil { + if mm["followers"], err = gobEncodeItem(a.Followers); err != nil { + return hasData, err + } + hasData = true + } + if a.Liked != nil { + if mm["liked"], err = gobEncodeItem(a.Liked); err != nil { + return hasData, err + } + hasData = true + } + if len(a.PreferredUsername) > 0 { + if mm["preferredUsername"], err = a.PreferredUsername.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if a.Endpoints != nil { + if mm["endpoints"], err = a.Endpoints.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if len(a.Streams) > 0 { + if mm["streams"], err = gobEncodeItems(a.Streams); err != nil { + return hasData, err + } + hasData = true + } + if len(a.PublicKey.PublicKeyPem)+len(a.PublicKey.ID) > 0 { + if mm["publicKey"], err = a.PublicKey.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + return hasData, err +} + +func mapIncompleteCollectionProperties(mm map[string][]byte, c Collection) (hasData bool, err error) { + err = OnObject(c, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if c.Current != nil { + if mm["current"], err = gobEncodeItem(c.Current); err != nil { + return hasData, err + } + hasData = true + } + if c.First != nil { + if mm["first"], err = gobEncodeItem(c.First); err != nil { + return hasData, err + } + hasData = true + } + if c.Last != nil { + if mm["last"], err = gobEncodeItem(c.Last); err != nil { + return hasData, err + } + hasData = true + } + if c.TotalItems > 0 { + hasData = true + } + if mm["totalItems"], err = gobEncodeUint(c.TotalItems); err != nil { + return hasData, err + } + return +} + +func mapCollectionProperties(mm map[string][]byte, c Collection) (hasData bool, err error) { + hasData, err = mapIncompleteCollectionProperties(mm, c) + if err != nil { + return hasData, err + } + if c.Items != nil { + if mm["items"], err = gobEncodeItems(c.Items); err != nil { + return hasData, err + } + hasData = true + } + return +} + +func mapOrderedCollectionProperties(mm map[string][]byte, c OrderedCollection) (hasData bool, err error) { + err = OnCollection(c, func(c *Collection) error { + hasData, err = mapIncompleteCollectionProperties(mm, *c) + return err + }) + if c.OrderedItems != nil { + if mm["orderedItems"], err = gobEncodeItems(c.OrderedItems); err != nil { + return hasData, err + } + hasData = true + } + return +} + +func mapCollectionPageProperties(mm map[string][]byte, c CollectionPage) (hasData bool, err error) { + err = OnCollection(c, func(c *Collection) error { + hasData, err = mapCollectionProperties(mm, *c) + return err + }) + if c.PartOf != nil { + if mm["partOf"], err = gobEncodeItem(c.PartOf); err != nil { + return hasData, err + } + hasData = true + } + if c.Next != nil { + if mm["next"], err = gobEncodeItem(c.Next); err != nil { + return hasData, err + } + hasData = true + } + if c.Prev != nil { + if mm["prev"], err = gobEncodeItem(c.Prev); err != nil { + return hasData, err + } + hasData = true + } + return +} + +func mapOrderedCollectionPageProperties(mm map[string][]byte, c OrderedCollectionPage) (hasData bool, err error) { + err = OnOrderedCollection(c, func(c *OrderedCollection) error { + hasData, err = mapOrderedCollectionProperties(mm, *c) + return err + }) + if c.PartOf != nil { + if mm["partOf"], err = gobEncodeItem(c.PartOf); err != nil { + return hasData, err + } + hasData = true + } + if c.Next != nil { + if mm["next"], err = gobEncodeItem(c.Next); err != nil { + return hasData, err + } + hasData = true + } + if c.Prev != nil { + if mm["prev"], err = gobEncodeItem(c.Prev); err != nil { + return hasData, err + } + hasData = true + } + return +} + +func mapLinkProperties(mm map[string][]byte, l Link) (hasData bool, err error) { + if len(l.ID) > 0 { + if mm["id"], err = l.ID.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.Type) > 0 { + if mm["type"], err = l.Type.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.MediaType) > 0 { + if mm["mediaType"], err = l.MediaType.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.Href) > 0 { + if mm["href"], err = l.Href.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.HrefLang) > 0 { + if mm["hrefLang"], err = l.HrefLang.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.Name) > 0 { + if mm["name"], err = l.Name.GobEncode(); err != nil { + return + } + hasData = true + } + if len(l.Rel) > 0 { + if mm["rel"], err = l.Rel.GobEncode(); err != nil { + return + } + hasData = true + } + if l.Width > 0 { + if mm["width"], err = gobEncodeUint(l.Width); err != nil { + return + } + hasData = true + } + if l.Height > 0 { + if mm["height"], err = gobEncodeUint(l.Height); err != nil { + return + } + hasData = true + } + return +} + +func mapPlaceProperties(mm map[string][]byte, p Place) (hasData bool, err error) { + err = OnObject(p, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if p.Accuracy > 0 { + if mm["accuracy"], err = gobEncodeFloat64(p.Accuracy); err != nil { + return + } + hasData = true + } + if p.Altitude > 0 { + if mm["altitude"], err = gobEncodeFloat64(p.Altitude); err != nil { + return + } + hasData = true + } + if p.Latitude > 0 { + if mm["latitude"], err = gobEncodeFloat64(p.Latitude); err != nil { + return + } + hasData = true + } + if p.Longitude > 0 { + if mm["longitude"], err = gobEncodeFloat64(p.Longitude); err != nil { + return + } + hasData = true + } + if p.Radius > 0 { + if mm["radius"], err = gobEncodeInt64(p.Radius); err != nil { + return + } + hasData = true + } + if len(p.Units) > 0 { + mm["units"] = []byte(p.Units) + hasData = true + } + return +} + +func mapProfileProperties(mm map[string][]byte, p Profile) (hasData bool, err error) { + err = OnObject(p, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if p.Describes != nil { + if mm["describes"], err = gobEncodeItem(p.Describes); err != nil { + return + } + hasData = true + } + return +} + +func mapRelationshipProperties(mm map[string][]byte, r Relationship) (hasData bool, err error) { + err = OnObject(r, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if r.Subject != nil { + if mm["subject"], err = gobEncodeItem(r.Subject); err != nil { + return + } + hasData = true + } + if r.Object != nil { + if mm["object"], err = gobEncodeItem(r.Object); err != nil { + return + } + hasData = true + } + if r.Relationship != nil { + if mm["relationship"], err = gobEncodeItem(r.Relationship); err != nil { + return + } + hasData = true + } + return +} + +func mapTombstoneProperties(mm map[string][]byte, t Tombstone) (hasData bool, err error) { + err = OnObject(t, func(o *Object) error { + hasData, err = mapObjectProperties(mm, o) + return err + }) + if len(t.FormerType) > 0 { + if mm["formerType"], err = t.FormerType.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + if !t.Deleted.IsZero() { + if mm["deleted"], err = t.Deleted.GobEncode(); err != nil { + return hasData, err + } + hasData = true + } + return +} + +func mapQuestionProperties(mm map[string][]byte, q Question) (hasData bool, err error) { + err = OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + hasData, err = mapIntransitiveActivityProperties(mm, i) + return err + }) + if q.OneOf != nil { + if mm["oneOf"], err = gobEncodeItem(q.OneOf); err != nil { + return + } + hasData = true + } + if q.AnyOf != nil { + if mm["anyOf"], err = gobEncodeItem(q.AnyOf); err != nil { + return + } + hasData = true + } + if q.Closed { + hasData = true + } + if hasData { + if mm["closed"], err = gobEncodeBool(q.Closed); err != nil { + return + } + } + return +} diff --git a/vendor/github.com/go-ap/activitypub/encoding_json.go b/vendor/github.com/go-ap/activitypub/encoding_json.go new file mode 100644 index 0000000..73baf18 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/encoding_json.go @@ -0,0 +1,387 @@ +package activitypub + +import ( + "encoding/json" + "fmt" + "time" + + xsd "git.sr.ht/~mariusor/go-xsd-duration" + "github.com/go-ap/jsonld" +) + +func JSONWriteComma(b *[]byte) { + if len(*b) > 1 && (*b)[len(*b)-1] != ',' { + *b = append(*b, ',') + } +} + +func JSONWriteProp(b *[]byte, name string, val []byte) (notEmpty bool) { + if len(val) == 0 { + return false + } + JSONWriteComma(b) + success := JSONWritePropName(b, name) && JSONWriteValue(b, val) + if !success { + *b = (*b)[:len(*b)-1] + } + return success +} + +func JSONWrite(b *[]byte, c ...byte) { + *b = append(*b, c...) +} + +func JSONWriteS(b *[]byte, s string) { + *b = append(*b, s...) +} + +func JSONWritePropName(b *[]byte, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + JSONWrite(b, '"') + JSONWriteS(b, s) + JSONWrite(b, '"', ':') + return true +} + +func JSONWriteValue(b *[]byte, s []byte) (notEmpty bool) { + if len(s) == 0 { + return false + } + JSONWrite(b, s...) + return true +} + +func JSONWriteNaturalLanguageProp(b *[]byte, n string, nl NaturalLanguageValues) (notEmpty bool) { + l := nl.Count() + if l > 1 { + n += "Map" + } + if v, err := nl.MarshalJSON(); err == nil && len(v) > 0 { + return JSONWriteProp(b, n, v) + } + return false +} + +func JSONWriteStringProp(b *[]byte, n string, s string) (notEmpty bool) { + return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%s"`, s))) +} + +func JSONWriteBoolProp(b *[]byte, n string, t bool) (notEmpty bool) { + return JSONWriteProp(b, n, []byte(fmt.Sprintf(`"%t"`, t))) +} + +func JSONWriteIntProp(b *[]byte, n string, d int64) (notEmpty bool) { + return JSONWriteProp(b, n, []byte(fmt.Sprintf("%d", d))) +} + +func JSONWriteFloatProp(b *[]byte, n string, f float64) (notEmpty bool) { + return JSONWriteProp(b, n, []byte(fmt.Sprintf("%f", f))) +} + +func JSONWriteTimeProp(b *[]byte, n string, t time.Time) (notEmpty bool) { + var tb []byte + JSONWrite(&tb, '"') + JSONWriteS(&tb, t.UTC().Format(time.RFC3339)) + JSONWrite(&tb, '"') + return JSONWriteProp(b, n, tb) +} + +func JSONWriteDurationProp(b *[]byte, n string, d time.Duration) (notEmpty bool) { + var tb []byte + if v, err := xsd.Marshal(d); err == nil { + JSONWrite(&tb, '"') + JSONWrite(&tb, v...) + JSONWrite(&tb, '"') + } + return JSONWriteProp(b, n, tb) +} + +func JSONWriteIRIProp(b *[]byte, n string, i LinkOrIRI) (notEmpty bool) { + url := i.GetLink().String() + if len(url) == 0 { + return false + } + JSONWriteStringProp(b, n, url) + return true +} + +func JSONWriteItemProp(b *[]byte, n string, i Item) (notEmpty bool) { + if i == nil { + return notEmpty + } + if im, ok := i.(json.Marshaler); ok { + v, err := im.MarshalJSON() + if err != nil { + return false + } + return JSONWriteProp(b, n, v) + } + return notEmpty +} + +func JSONWriteStringValue(b *[]byte, s string) (notEmpty bool) { + if len(s) == 0 { + return false + } + JSONWrite(b, '"') + JSONWriteS(b, s) + JSONWrite(b, '"') + return true +} + +func JSONWriteItemCollectionValue(b *[]byte, col ItemCollection, compact bool) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + if len(col) == 1 && compact { + it := col[0] + im, ok := it.(json.Marshaler) + if !ok { + return false + } + v, err := im.MarshalJSON() + if err != nil { + return false + } + if len(v) == 0 { + return false + } + JSONWrite(b, v...) + return true + } + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + JSONWrite(b, ',') + } + } + JSONWrite(b, '[') + skipComma := true + for _, it := range col { + im, ok := it.(json.Marshaler) + if !ok { + continue + } + v, err := im.MarshalJSON() + if err != nil { + return false + } + if len(v) == 0 { + continue + } + writeCommaIfNotEmpty(!skipComma) + JSONWrite(b, v...) + skipComma = false + } + JSONWrite(b, ']') + return true +} + +func JSONWriteItemCollectionProp(b *[]byte, n string, col ItemCollection, compact bool) (notEmpty bool) { + if len(col) == 0 { + return notEmpty + } + JSONWriteComma(b) + success := JSONWritePropName(b, n) && JSONWriteItemCollectionValue(b, col, compact) + if !success { + *b = (*b)[:len(*b)-1] + } + return success +} + +func JSONWriteObjectValue(b *[]byte, o Object) (notEmpty bool) { + if v, err := o.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "id", v) || notEmpty + } + if v, err := o.Type.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "type", v) || notEmpty + } + if v, err := o.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty + } + if len(o.Name) > 0 { + notEmpty = JSONWriteNaturalLanguageProp(b, "name", o.Name) || notEmpty + } + if len(o.Summary) > 0 { + notEmpty = JSONWriteNaturalLanguageProp(b, "summary", o.Summary) || notEmpty + } + if len(o.Content) > 0 { + notEmpty = JSONWriteNaturalLanguageProp(b, "content", o.Content) || notEmpty + } + if o.Attachment != nil { + notEmpty = JSONWriteItemProp(b, "attachment", o.Attachment) || notEmpty + } + if o.AttributedTo != nil { + notEmpty = JSONWriteItemProp(b, "attributedTo", o.AttributedTo) || notEmpty + } + if o.Audience != nil { + notEmpty = JSONWriteItemProp(b, "audience", o.Audience) || notEmpty + } + if o.Context != nil { + notEmpty = JSONWriteItemProp(b, "context", o.Context) || notEmpty + } + if o.Generator != nil { + notEmpty = JSONWriteItemProp(b, "generator", o.Generator) || notEmpty + } + if o.Icon != nil { + notEmpty = JSONWriteItemProp(b, "icon", o.Icon) || notEmpty + } + if o.Image != nil { + notEmpty = JSONWriteItemProp(b, "image", o.Image) || notEmpty + } + if o.InReplyTo != nil { + notEmpty = JSONWriteItemProp(b, "inReplyTo", o.InReplyTo) || notEmpty + } + if o.Location != nil { + notEmpty = JSONWriteItemProp(b, "location", o.Location) || notEmpty + } + if o.Preview != nil { + notEmpty = JSONWriteItemProp(b, "preview", o.Preview) || notEmpty + } + if o.Replies != nil { + notEmpty = JSONWriteItemProp(b, "replies", o.Replies) || notEmpty + } + if o.Tag != nil { + notEmpty = JSONWriteItemCollectionProp(b, "tag", o.Tag, false) || notEmpty + } + if o.URL != nil { + notEmpty = JSONWriteItemProp(b, "url", o.URL) || notEmpty + } + if o.To != nil { + notEmpty = JSONWriteItemCollectionProp(b, "to", o.To, false) || notEmpty + } + if o.Bto != nil { + notEmpty = JSONWriteItemCollectionProp(b, "bto", o.Bto, false) || notEmpty + } + if o.CC != nil { + notEmpty = JSONWriteItemCollectionProp(b, "cc", o.CC, false) || notEmpty + } + if o.BCC != nil { + notEmpty = JSONWriteItemCollectionProp(b, "bcc", o.BCC, false) || notEmpty + } + if !o.Published.IsZero() { + notEmpty = JSONWriteTimeProp(b, "published", o.Published) || notEmpty + } + if !o.Updated.IsZero() { + notEmpty = JSONWriteTimeProp(b, "updated", o.Updated) || notEmpty + } + if !o.StartTime.IsZero() { + notEmpty = JSONWriteTimeProp(b, "startTime", o.StartTime) || notEmpty + } + if !o.EndTime.IsZero() { + notEmpty = JSONWriteTimeProp(b, "endTime", o.EndTime) || notEmpty + } + if o.Duration != 0 { + // TODO(marius): maybe don't use 0 as a nil value for Object types + // which can have a valid duration of 0 - (Video, Audio, etc) + notEmpty = JSONWriteDurationProp(b, "duration", o.Duration) || notEmpty + } + if o.Likes != nil { + notEmpty = JSONWriteItemProp(b, "likes", o.Likes) || notEmpty + } + if o.Shares != nil { + notEmpty = JSONWriteItemProp(b, "shares", o.Shares) || notEmpty + } + if v, err := o.Source.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "source", v) || notEmpty + } + return notEmpty +} + +func JSONWriteActivityValue(b *[]byte, a Activity) (notEmpty bool) { + OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty + return nil + }) + if a.Object != nil { + notEmpty = JSONWriteItemProp(b, "object", a.Object) || notEmpty + } + return notEmpty +} + +func JSONWriteIntransitiveActivityValue(b *[]byte, i IntransitiveActivity) (notEmpty bool) { + OnObject(i, func(o *Object) error { + if o == nil { + return nil + } + notEmpty = JSONWriteObjectValue(b, *o) || notEmpty + return nil + }) + if i.Actor != nil { + notEmpty = JSONWriteItemProp(b, "actor", i.Actor) || notEmpty + } + if i.Target != nil { + notEmpty = JSONWriteItemProp(b, "target", i.Target) || notEmpty + } + if i.Result != nil { + notEmpty = JSONWriteItemProp(b, "result", i.Result) || notEmpty + } + if i.Origin != nil { + notEmpty = JSONWriteItemProp(b, "origin", i.Origin) || notEmpty + } + if i.Instrument != nil { + notEmpty = JSONWriteItemProp(b, "instrument", i.Instrument) || notEmpty + } + return notEmpty +} + +func JSONWriteQuestionValue(b *[]byte, q Question) (notEmpty bool) { + OnIntransitiveActivity(q, func(i *IntransitiveActivity) error { + if i == nil { + return nil + } + notEmpty = JSONWriteIntransitiveActivityValue(b, *i) || notEmpty + return nil + }) + if q.OneOf != nil { + notEmpty = JSONWriteItemProp(b, "oneOf", q.OneOf) || notEmpty + } + if q.AnyOf != nil { + notEmpty = JSONWriteItemProp(b, "anyOf", q.AnyOf) || notEmpty + } + notEmpty = JSONWriteBoolProp(b, "closed", q.Closed) || notEmpty + return notEmpty +} + +func JSONWriteLinkValue(b *[]byte, l Link) (notEmpty bool) { + if v, err := l.ID.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "id", v) || notEmpty + } + if v, err := l.Type.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "type", v) || notEmpty + } + if v, err := l.MediaType.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "mediaType", v) || notEmpty + } + if len(l.Name) > 0 { + notEmpty = JSONWriteNaturalLanguageProp(b, "name", l.Name) || notEmpty + } + if v, err := l.Rel.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "rel", v) || notEmpty + } + if l.Height > 0 { + notEmpty = JSONWriteIntProp(b, "height", int64(l.Height)) + } + if l.Width > 0 { + notEmpty = JSONWriteIntProp(b, "width", int64(l.Width)) + } + if l.Preview != nil { + notEmpty = JSONWriteItemProp(b, "rel", l.Preview) || notEmpty + } + if v, err := l.Href.MarshalJSON(); err == nil && len(v) > 0 { + notEmpty = JSONWriteProp(b, "href", v) || notEmpty + } + if len(l.HrefLang) > 0 { + notEmpty = JSONWriteStringProp(b, "hrefLang", string(l.HrefLang)) || notEmpty + } + return notEmpty +} + +// MarshalJSON represents just a wrapper for the jsonld.Marshal function +func MarshalJSON(it Item) ([]byte, error) { + return jsonld.Marshal(it) +} diff --git a/vendor/github.com/go-ap/activitypub/flatten.go b/vendor/github.com/go-ap/activitypub/flatten.go new file mode 100644 index 0000000..8c00773 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/flatten.go @@ -0,0 +1,127 @@ +package activitypub + +// FlattenActivityProperties flattens the Activity's properties from Object type to IRI +func FlattenActivityProperties(act *Activity) *Activity { + OnIntransitiveActivity(act, func(in *IntransitiveActivity) error { + FlattenIntransitiveActivityProperties(in) + return nil + }) + act.Object = FlattenToIRI(act.Object) + return act +} + +// FlattenIntransitiveActivityProperties flattens the Activity's properties from Object type to IRI +func FlattenIntransitiveActivityProperties(act *IntransitiveActivity) *IntransitiveActivity { + act.Actor = FlattenToIRI(act.Actor) + act.Target = FlattenToIRI(act.Target) + act.Result = FlattenToIRI(act.Result) + act.Origin = FlattenToIRI(act.Origin) + act.Result = FlattenToIRI(act.Result) + act.Instrument = FlattenToIRI(act.Instrument) + OnObject(act, func(o *Object) error { + FlattenObjectProperties(o) + return nil + }) + return act +} + +// FlattenItemCollection flattens an Item Collection to their respective IRIs +func FlattenItemCollection(col ItemCollection) ItemCollection { + if col == nil { + return col + } + for k, it := range ItemCollectionDeduplication(&col) { + if iri := it.GetLink(); iri != "" { + col[k] = iri + } + } + return col +} + +// FlattenCollection flattens a Collection's objects to their respective IRIs +func FlattenCollection(col *Collection) *Collection { + if col == nil { + return col + } + col.Items = FlattenItemCollection(col.Items) + + return col +} + +// FlattenOrderedCollection flattens an OrderedCollection's objects to their respective IRIs +func FlattenOrderedCollection(col *OrderedCollection) *OrderedCollection { + if col == nil { + return col + } + col.OrderedItems = FlattenItemCollection(col.OrderedItems) + + return col +} + +// FlattenActorProperties flattens the Actor's properties from Object types to IRI +func FlattenActorProperties(a *Actor) *Actor { + OnObject(a, func(o *Object) error { + FlattenObjectProperties(o) + return nil + }) + return a +} + +// FlattenObjectProperties flattens the Object's properties from Object types to IRI +func FlattenObjectProperties(o *Object) *Object { + o.Replies = Flatten(o.Replies) + o.Shares = Flatten(o.Shares) + o.Likes = Flatten(o.Likes) + o.AttributedTo = Flatten(o.AttributedTo) + o.To = FlattenItemCollection(o.To) + o.Bto = FlattenItemCollection(o.Bto) + o.CC = FlattenItemCollection(o.CC) + o.BCC = FlattenItemCollection(o.BCC) + o.Audience = FlattenItemCollection(o.Audience) + // o.Tag = FlattenItemCollection(o.Tag) + return o +} + +// FlattenProperties flattens the Item's properties from Object types to IRI +func FlattenProperties(it Item) Item { + typ := it.GetType() + if IntransitiveActivityTypes.Contains(typ) { + OnIntransitiveActivity(it, func(a *IntransitiveActivity) error { + FlattenIntransitiveActivityProperties(a) + return nil + }) + } else if ActivityTypes.Contains(typ) { + OnActivity(it, func(a *Activity) error { + FlattenActivityProperties(a) + return nil + }) + } + if ActorTypes.Contains(typ) { + OnActor(it, func(a *Actor) error { + FlattenActorProperties(a) + return nil + }) + } + if ObjectTypes.Contains(typ) { + OnObject(it, func(o *Object) error { + FlattenObjectProperties(o) + return nil + }) + } + return it +} + +// Flatten checks if Item can be flattened to an IRI or array of IRIs and returns it if so +func Flatten(it Item) Item { + if IsNil(it) { + return nil + } + if it.IsCollection() { + OnCollectionIntf(it, func(c CollectionInterface) error { + it = FlattenItemCollection(c.Collection()).Normalize() + return nil + }) + return it + } + return it.GetLink() +} diff --git a/vendor/github.com/go-ap/activitypub/helpers.go b/vendor/github.com/go-ap/activitypub/helpers.go new file mode 100644 index 0000000..b4618b1 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/helpers.go @@ -0,0 +1,532 @@ +package activitypub + +import ( + "fmt" +) + +// WithLinkFn represents a function type that can be used as a parameter for OnLink helper function +type WithLinkFn func(*Link) error + +// WithObjectFn represents a function type that can be used as a parameter for OnObject helper function +type WithObjectFn func(*Object) error + +// WithActivityFn represents a function type that can be used as a parameter for OnActivity helper function +type WithActivityFn func(*Activity) error + +// WithIntransitiveActivityFn represents a function type that can be used as a parameter for OnIntransitiveActivity helper function +type WithIntransitiveActivityFn func(*IntransitiveActivity) error + +// WithQuestionFn represents a function type that can be used as a parameter for OnQuestion helper function +type WithQuestionFn func(*Question) error + +// WithActorFn represents a function type that can be used as a parameter for OnActor helper function +type WithActorFn func(*Actor) error + +// WithCollectionInterfaceFn represents a function type that can be used as a parameter for OnCollectionIntf helper function +type WithCollectionInterfaceFn func(CollectionInterface) error + +// WithCollectionFn represents a function type that can be used as a parameter for OnCollection helper function +type WithCollectionFn func(*Collection) error + +// WithCollectionPageFn represents a function type that can be used as a parameter for OnCollectionPage helper function +type WithCollectionPageFn func(*CollectionPage) error + +// WithOrderedCollectionFn represents a function type that can be used as a parameter for OnOrderedCollection helper function +type WithOrderedCollectionFn func(*OrderedCollection) error + +// WithOrderedCollectionPageFn represents a function type that can be used as a parameter for OnOrderedCollectionPage helper function +type WithOrderedCollectionPageFn func(*OrderedCollectionPage) error + +// WithItemCollectionFn represents a function type that can be used as a parameter for OnItemCollection helper function +type WithItemCollectionFn func(*ItemCollection) error + +// WithIRIsFn represents a function type that can be used as a parameter for OnIRIs helper function +type WithIRIsFn func(*IRIs) error + +// OnLink calls function fn on it Item if it can be asserted to type *Link +// +// This function should be safe to use for all types with a structure compatible +// with the Link type +func OnLink(it LinkOrIRI, fn WithLinkFn) error { + if it == nil { + return nil + } + ob, err := ToLink(it) + if err != nil { + return err + } + return fn(ob) +} + +func To[T Item](it Item) (*T, error) { + if ob, ok := it.(T); ok { + return &ob, nil + } + return nil, fmt.Errorf("invalid cast for object %T", it) +} + +// On handles in a generic way the call to fn(*T) if the "it" Item can be asserted to one of the Objects type. +// It also covers the case where "it" is a collection of items that match the assertion. +func On[T Item](it Item, fn func(*T) error) error { + if !IsItemCollection(it) { + ob, err := To[T](it) + if err != nil { + return err + } + return fn(ob) + } + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if err := On(it, fn); err != nil { + return err + } + } + return nil + }) +} + +// OnObject calls function fn on it Item if it can be asserted to type *Object +// +// This function should be safe to be called for all types with a structure compatible +// to the Object type. +func OnObject(it Item, fn WithObjectFn) error { + if it == nil { + return nil + } + if IsItemCollection(it) { + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if IsLink(it) { + continue + } + if err := OnObject(it, fn); err != nil { + return err + } + } + return nil + }) + } + ob, err := ToObject(it) + if err != nil { + return err + } + return fn(ob) +} + +// OnActivity calls function fn on it Item if it can be asserted to type *Activity +// +// This function should be called if trying to access the Activity specific properties +// like "object", for the other properties OnObject, or OnIntransitiveActivity +// should be used instead. +func OnActivity(it Item, fn WithActivityFn) error { + if it == nil { + return nil + } + if IsItemCollection(it) { + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if IsLink(it) { + continue + } + if err := OnActivity(it, fn); err != nil { + return err + } + } + return nil + }) + } + act, err := ToActivity(it) + if err != nil { + return err + } + return fn(act) +} + +// OnIntransitiveActivity calls function fn on it Item if it can be asserted +// to type *IntransitiveActivity +// +// This function should be called if trying to access the IntransitiveActivity +// specific properties like "actor", for the other properties OnObject +// should be used instead. +func OnIntransitiveActivity(it Item, fn WithIntransitiveActivityFn) error { + if it == nil { + return nil + } + if IsItemCollection(it) { + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if err := OnIntransitiveActivity(it, fn); err != nil { + return err + } + } + return nil + }) + } + act, err := ToIntransitiveActivity(it) + if err != nil { + return err + } + return fn(act) +} + +// OnQuestion calls function fn on it Item if it can be asserted to type Question +// +// This function should be called if trying to access the Questions specific +// properties like "anyOf", "oneOf", "closed", etc. For the other properties +// OnObject or OnIntransitiveActivity should be used instead. +func OnQuestion(it Item, fn WithQuestionFn) error { + if it == nil { + return nil + } + if IsItemCollection(it) { + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if err := OnQuestion(it, fn); err != nil { + return err + } + } + return nil + }) + } + act, err := ToQuestion(it) + if err != nil { + return err + } + return fn(act) +} + +// OnActor calls function fn on it Item if it can be asserted to type *Actor +// +// This function should be called if trying to access the Actor specific +// properties like "preferredName", "publicKey", etc. For the other properties +// OnObject should be used instead. +func OnActor(it Item, fn WithActorFn) error { + if it == nil { + return nil + } + if IsItemCollection(it) { + return OnItemCollection(it, func(col *ItemCollection) error { + for _, it := range *col { + if IsLink(it) { + continue + } + if err := OnActor(it, fn); err != nil { + return err + } + } + return nil + }) + } + act, err := ToActor(it) + if err != nil { + return err + } + return fn(act) +} + +// OnItemCollection calls function fn on it Item if it can be asserted to type ItemCollection +// +// It should be used when Item represents an Item collection and it's usually used as a way +// to wrap functionality for other functions that will be called on each item in the collection. +func OnItemCollection(it Item, fn WithItemCollectionFn) error { + if it == nil { + return nil + } + col, err := ToItemCollection(it) + if err != nil { + return err + } + return fn(col) +} + +// OnIRIs calls function fn on it Item if it can be asserted to type IRIs +// +// It should be used when Item represents an IRI slice. +func OnIRIs(it Item, fn WithIRIsFn) error { + if it == nil { + return nil + } + col, err := ToIRIs(it) + if err != nil { + return err + } + return fn(col) +} + +// OnCollectionIntf calls function fn on it Item if it can be asserted to a type +// that implements the CollectionInterface +// +// This function should be called if Item represents a collection of ActivityPub +// objects. It basically wraps functionality for the different collection types +// supported by the package. +func OnCollectionIntf(it Item, fn WithCollectionInterfaceFn) error { + if it == nil { + return nil + } + switch it.GetType() { + case CollectionOfItems: + col, err := ToItemCollection(it) + if err != nil { + return err + } + return fn(col) + case CollectionOfIRIs: + col, err := ToIRIs(it) + if err != nil { + return err + } + itCol := col.Collection() + return fn(&itCol) + case CollectionType: + col, err := ToCollection(it) + if err != nil { + return err + } + return fn(col) + case CollectionPageType: + return OnCollectionPage(it, func(p *CollectionPage) error { + col, err := ToCollectionPage(p) + if err != nil { + return err + } + return fn(col) + }) + case OrderedCollectionType: + col, err := ToOrderedCollection(it) + if err != nil { + return err + } + return fn(col) + case OrderedCollectionPageType: + return OnOrderedCollectionPage(it, func(p *OrderedCollectionPage) error { + col, err := ToOrderedCollectionPage(p) + if err != nil { + return err + } + return fn(col) + }) + default: + return fmt.Errorf("%T[%s] can't be converted to a Collection type", it, it.GetType()) + } +} + +// OnCollection calls function fn on it Item if it can be asserted to type *Collection +// +// This function should be called if trying to access the Collection specific +// properties like "totalItems", "items", etc. For the other properties +// OnObject should be used instead. +func OnCollection(it Item, fn WithCollectionFn) error { + if it == nil { + return nil + } + col, err := ToCollection(it) + if err != nil { + return err + } + return fn(col) +} + +// OnCollectionPage calls function fn on it Item if it can be asserted to +// type *CollectionPage +// +// This function should be called if trying to access the CollectionPage specific +// properties like "partOf", "next", "perv". For the other properties +// OnObject or OnCollection should be used instead. +func OnCollectionPage(it Item, fn WithCollectionPageFn) error { + if it == nil { + return nil + } + col, err := ToCollectionPage(it) + if err != nil { + return err + } + return fn(col) +} + +// OnOrderedCollection calls function fn on it Item if it can be asserted +// to type *OrderedCollection +// +// This function should be called if trying to access the Collection specific +// properties like "totalItems", "orderedItems", etc. For the other properties +// OnObject should be used instead. +func OnOrderedCollection(it Item, fn WithOrderedCollectionFn) error { + if it == nil { + return nil + } + col, err := ToOrderedCollection(it) + if err != nil { + return err + } + return fn(col) +} + +// OnOrderedCollectionPage calls function fn on it Item if it can be asserted +// to type *OrderedCollectionPage +// +// This function should be called if trying to access the OrderedCollectionPage specific +// properties like "partOf", "next", "perv". For the other properties +// OnObject or OnOrderedCollection should be used instead. +func OnOrderedCollectionPage(it Item, fn WithOrderedCollectionPageFn) error { + if it == nil { + return nil + } + col, err := ToOrderedCollectionPage(it) + if err != nil { + return err + } + return fn(col) +} + +// ItemOrderTimestamp is used for ordering a ItemCollection slice using the slice.Sort function +// It orders i1 and i2 based on their Published and Updated timestamps. +func ItemOrderTimestamp(i1, i2 Item) bool { + o1, e1 := ToObject(i1) + o2, e2 := ToObject(i2) + if e1 != nil || e2 != nil { + return false + } + t1 := o1.Published + if !o1.Updated.IsZero() { + t1 = o1.Updated + } + t2 := o2.Published + if !o2.Updated.IsZero() { + t2 = o2.Updated + } + return t1.Sub(t2) > 0 +} + +func notEmptyLink(l *Link) bool { + return len(l.ID) > 0 || + LinkTypes.Contains(l.Type) || + len(l.MediaType) > 0 || + l.Preview != nil || + l.Name != nil || + len(l.Href) > 0 || + len(l.Rel) > 0 || + len(l.HrefLang) > 0 || + l.Height > 0 || + l.Width > 0 +} + +func notEmptyObject(o *Object) bool { + if o == nil { + return false + } + return len(o.ID) > 0 || + len(o.Type) > 0 || + ActivityTypes.Contains(o.Type) || + o.Content != nil || + o.Attachment != nil || + o.AttributedTo != nil || + o.Audience != nil || + o.BCC != nil || + o.Bto != nil || + o.CC != nil || + o.Context != nil || + o.Duration > 0 || + !o.EndTime.IsZero() || + o.Generator != nil || + o.Icon != nil || + o.Image != nil || + o.InReplyTo != nil || + o.Likes != nil || + o.Location != nil || + len(o.MediaType) > 0 || + o.Name != nil || + o.Preview != nil || + !o.Published.IsZero() || + o.Replies != nil || + o.Shares != nil || + o.Source.MediaType != "" || + o.Source.Content != nil || + !o.StartTime.IsZero() || + o.Summary != nil || + o.Tag != nil || + o.To != nil || + !o.Updated.IsZero() || + o.URL != nil +} + +func notEmptyInstransitiveActivity(i *IntransitiveActivity) bool { + notEmpty := i.Actor != nil || + i.Target != nil || + i.Result != nil || + i.Origin != nil || + i.Instrument != nil + if notEmpty { + return true + } + OnObject(i, func(ob *Object) error { + notEmpty = notEmptyObject(ob) + return nil + }) + return notEmpty +} + +func notEmptyActivity(a *Activity) bool { + var notEmpty bool + OnIntransitiveActivity(a, func(i *IntransitiveActivity) error { + notEmpty = notEmptyInstransitiveActivity(i) + return nil + }) + return notEmpty || a.Object != nil +} + +func notEmptyActor(a *Actor) bool { + var notEmpty bool + OnObject(a, func(o *Object) error { + notEmpty = notEmptyObject(o) + return nil + }) + return notEmpty || + a.Inbox != nil || + a.Outbox != nil || + a.Following != nil || + a.Followers != nil || + a.Liked != nil || + a.PreferredUsername != nil || + a.Endpoints != nil || + a.Streams != nil || + len(a.PublicKey.ID)+len(a.PublicKey.Owner)+len(a.PublicKey.PublicKeyPem) > 0 +} + +// NotEmpty tells us if a Item interface value has a non nil value for various types +// that implement +func NotEmpty(i Item) bool { + if IsNil(i) { + return false + } + var notEmpty bool + if IsIRI(i) { + notEmpty = len(i.GetLink()) > 0 + } + if i.IsCollection() { + OnCollectionIntf(i, func(c CollectionInterface) error { + notEmpty = c != nil || len(c.Collection()) > 0 + return nil + }) + } + if ActivityTypes.Contains(i.GetType()) { + OnActivity(i, func(a *Activity) error { + notEmpty = notEmptyActivity(a) + return nil + }) + } else if ActorTypes.Contains(i.GetType()) { + OnActor(i, func(a *Actor) error { + notEmpty = notEmptyActor(a) + return nil + }) + } else if i.IsLink() { + OnLink(i, func(l *Link) error { + notEmpty = notEmptyLink(l) + return nil + }) + } else { + OnObject(i, func(o *Object) error { + notEmpty = notEmptyObject(o) + return nil + }) + } + return notEmpty +} diff --git a/vendor/github.com/go-ap/activitypub/intransitive_activity.go b/vendor/github.com/go-ap/activitypub/intransitive_activity.go new file mode 100644 index 0000000..2d00f94 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/intransitive_activity.go @@ -0,0 +1,384 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "reflect" + "time" + "unsafe" + + "github.com/valyala/fastjson" +) + +type IntransitiveActivities interface { + IntransitiveActivity | Question +} + +// IntransitiveActivity Instances of IntransitiveActivity are a subtype of Activity representing intransitive actions. +// The object property is therefore inappropriate for these activities. +type IntransitiveActivity struct { + // ID provides the globally unique identifier for anActivity Pub Object or Link. + ID ID `jsonld:"id,omitempty"` + // Type identifies the Activity Pub Object or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // Name a simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // Attachment identifies a resource attached or related to an object that potentially requires special handling. + // The intent is to provide a model that is at least semantically similar to attachments in email. + Attachment Item `jsonld:"attachment,omitempty"` + // AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors. + // For instance, an object might be attributed to the completion of another activity. + AttributedTo Item `jsonld:"attributedTo,omitempty"` + // Audience identifies one or more entities that represent the total population of entities + // for which the object can considered to be relevant. + Audience ItemCollection `jsonld:"audience,omitempty"` + // Content or textual representation of the Activity Pub Object encoded as a JSON string. + // By default, the value of content is HTML. + // The mediaType property can be used in the object to indicate a different content type. + // (The content MAY be expressed using multiple language-tagged values.) + Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"` + // Context identifies the context within which the object exists or an activity was performed. + // The notion of "context" used is intentionally vague. + // The intended function is to serve as a means of grouping objects and activities that share a + // common originating context or purpose. An example could be all activities relating to a common project or event. + Context Item `jsonld:"context,omitempty"` + // MediaType when used on an Object, identifies the MIME media type of the value of the content property. + // If not specified, the content property is assumed to contain text/html content. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // EndTime the date and time describing the actual or expected ending time of the object. + // When used with an Activity object, for instance, the endTime property specifies the moment + // the activity concluded or is expected to conclude. + EndTime time.Time `jsonld:"endTime,omitempty"` + // Generator identifies the entity (e.g. an application) that generated the object. + Generator Item `jsonld:"generator,omitempty"` + // Icon indicates an entity that describes an icon for this object. + // The image should have an aspect ratio of one (horizontal) to one (vertical) + // and should be suitable for presentation at a small size. + Icon Item `jsonld:"icon,omitempty"` + // Image indicates an entity that describes an image for this object. + // Unlike the icon property, there are no aspect ratio or display size limitations assumed. + Image Item `jsonld:"image,omitempty"` + // InReplyTo indicates one or more entities for which this object is considered a response. + InReplyTo Item `jsonld:"inReplyTo,omitempty"` + // Location indicates one or more physical or logical locations associated with the object. + Location Item `jsonld:"location,omitempty"` + // Preview identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // Published the date and time at which the object was published + Published time.Time `jsonld:"published,omitempty"` + // Replies identifies a Collection containing objects considered to be responses to this object. + Replies Item `jsonld:"replies,omitempty"` + // StartTime the date and time describing the actual or expected starting time of the object. + // When used with an Activity object, for instance, the startTime property specifies + // the moment the activity began or is scheduled to begin. + StartTime time.Time `jsonld:"startTime,omitempty"` + // Summary a natural language summarization of the object encoded as HTML. + // *Multiple language tagged summaries may be provided.) + Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"` + // Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object. + // The key difference between attachment and tag is that the former implies association by inclusion, + // while the latter implies associated by reference. + Tag ItemCollection `jsonld:"tag,omitempty"` + // Updated the date and time at which the object was updated + Updated time.Time `jsonld:"updated,omitempty"` + // URL identifies one or more links to representations of the object + URL Item `jsonld:"url,omitempty"` + // To identifies an entity considered to be part of the public primary audience of an Activity Pub Object + To ItemCollection `jsonld:"to,omitempty"` + // Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object. + Bto ItemCollection `jsonld:"bto,omitempty"` + // CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object. + CC ItemCollection `jsonld:"cc,omitempty"` + // BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object. + BCC ItemCollection `jsonld:"bcc,omitempty"` + // Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc, + // the duration property indicates the object's approximate duration. + // The value must be expressed as an xsd:duration as defined by [ xmlschema11-2], + // section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S"). + Duration time.Duration `jsonld:"duration,omitempty"` + // This is a list of all Like activities with this object as the object property, added as a side effect. + // The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Likes Item `jsonld:"likes,omitempty"` + // This is a list of all Announce activities with this object as the object property, added as a side effect. + // The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges + // of an authenticated user or as appropriate when no authentication is given. + Shares Item `jsonld:"shares,omitempty"` + // Source property is intended to convey some sort of source from which the content markup was derived, + // as a form of provenance, or to support future editing by clients. + // In general, clients do the conversion from source to content, not the other way around. + Source Source `jsonld:"source,omitempty"` + // CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity. + // Any single activity can have multiple actors. The actor may be specified using an indirect Link. + Actor CanReceiveActivities `jsonld:"actor,omitempty"` + // Target describes the indirect object, or target, of the activity. + // The precise meaning of the target is largely dependent on the type of action being described + // but will often be the object of the English preposition "to". + // For instance, in the activity "John added a movie to his wishlist", + // the target of the activity is John's wishlist. An activity can have more than one target. + Target Item `jsonld:"target,omitempty"` + // Result describes the result of the activity. For instance, if a particular action results in the creation + // of a new resource, the result property can be used to describe that new resource. + Result Item `jsonld:"result,omitempty"` + // Origin describes an indirect object of the activity from which the activity is directed. + // The precise meaning of the origin is the object of the English preposition "from". + // For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A". + Origin Item `jsonld:"origin,omitempty"` + // Instrument identifies one or more objects used (or to be used) in the completion of an Activity. + Instrument Item `jsonld:"instrument,omitempty"` +} + +type ( + // Arrive is an IntransitiveActivity that indicates that the actor has arrived at the location. + // The origin can be used to identify the context from which the actor originated. + // The target typically has no defined meaning. + Arrive = IntransitiveActivity + + // Travel indicates that the actor is traveling to target from origin. + // Travel is an IntransitiveObject whose actor specifies the direct object. + // If the target or origin are not specified, either can be determined by context. + Travel = IntransitiveActivity +) + +// Recipients performs recipient de-duplication on the IntransitiveActivity's To, Bto, CC and BCC properties +func (i *IntransitiveActivity) Recipients() ItemCollection { + return ItemCollectionDeduplication(&ItemCollection{i.Actor}, &i.To, &i.Bto, &i.CC, &i.BCC, &i.Audience) +} + +// Clean removes Bto and BCC properties +func (i *IntransitiveActivity) Clean() { + _ = OnObject(i, func(o *Object) error { + o.Clean() + return nil + }) +} + +// GetType returns the ActivityVocabulary type of the current Intransitive Activity +func (i IntransitiveActivity) GetType() ActivityVocabularyType { + return i.Type +} + +// IsLink returns false for Activity objects +func (i IntransitiveActivity) IsLink() bool { + return false +} + +// GetID returns the ID corresponding to the IntransitiveActivity object +func (i IntransitiveActivity) GetID() ID { + return i.ID +} + +// GetLink returns the IRI corresponding to the IntransitiveActivity object +func (i IntransitiveActivity) GetLink() IRI { + return IRI(i.ID) +} + +// IsObject returns true for IntransitiveActivity objects +func (i IntransitiveActivity) IsObject() bool { + return true +} + +// IsCollection returns false for IntransitiveActivity objects +func (i IntransitiveActivity) IsCollection() bool { + return false +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (i *IntransitiveActivity) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadIntransitiveActivity(val, i) +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (i IntransitiveActivity) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + JSONWrite(&b, '{') + + if !JSONWriteIntransitiveActivityValue(&b, i) { + return nil, nil + } + JSONWrite(&b, '}') + return b, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (i *IntransitiveActivity) UnmarshalBinary(data []byte) error { + return i.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (i IntransitiveActivity) MarshalBinary() ([]byte, error) { + return i.GobEncode() +} + +func (i IntransitiveActivity) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapIntransitiveActivityProperties(mm, &i) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (i *IntransitiveActivity) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapIntransitiveActivityProperties(mm, i) +} + +// IntransitiveActivityNew initializes a intransitive activity +func IntransitiveActivityNew(id ID, typ ActivityVocabularyType) *IntransitiveActivity { + if !IntransitiveActivityTypes.Contains(typ) { + typ = IntransitiveActivityType + } + i := IntransitiveActivity{ID: id, Type: typ} + i.Name = NaturalLanguageValuesNew() + i.Content = NaturalLanguageValuesNew() + + return &i +} + +// ToIntransitiveActivity tries to convert it Item to an IntransitiveActivity object +func ToIntransitiveActivity(it Item) (*IntransitiveActivity, error) { + switch i := it.(type) { + case *IntransitiveActivity: + return i, nil + case IntransitiveActivity: + return &i, nil + case *Question: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Question: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil + case *Activity: + return (*IntransitiveActivity)(unsafe.Pointer(i)), nil + case Activity: + return (*IntransitiveActivity)(unsafe.Pointer(&i)), nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(IntransitiveActivity)) + if reflect.TypeOf(it).ConvertibleTo(typ) { + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*IntransitiveActivity); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[IntransitiveActivity](it) +} + +// ArriveNew initializes an Arrive activity +func ArriveNew(id ID) *Arrive { + a := IntransitiveActivityNew(id, ArriveType) + o := Arrive(*a) + return &o +} + +// TravelNew initializes a Travel activity +func TravelNew(id ID) *Travel { + a := IntransitiveActivityNew(id, TravelType) + o := Travel(*a) + return &o +} + +// Equals verifies if our receiver Object is equals with the "with" Object +func (i IntransitiveActivity) Equals(with Item) bool { + result := true + err := OnIntransitiveActivity(with, func(w *IntransitiveActivity) error { + _ = OnObject(i, func(oa *Object) error { + result = oa.Equals(w) + return nil + }) + if w.Actor != nil { + if !ItemsEqual(i.Actor, w.Actor) { + result = false + return nil + } + } + if w.Target != nil { + if !ItemsEqual(i.Target, w.Target) { + result = false + return nil + } + } + if w.Result != nil { + if !ItemsEqual(i.Result, w.Result) { + result = false + return nil + } + } + if w.Origin != nil { + if !ItemsEqual(i.Origin, w.Origin) { + result = false + return nil + } + } + if w.Instrument != nil { + if !ItemsEqual(i.Instrument, w.Instrument) { + result = false + return nil + } + } + return nil + }) + if err != nil { + result = false + } + return result +} + +func (i IntransitiveActivity) Format(s fmt.State, verb rune) { + switch verb { + case 's': + if i.Type != "" && i.ID != "" { + _, _ = fmt.Fprintf(s, "%T[%s]( %s )", i, i.Type, i.ID) + } else if i.ID != "" { + _, _ = fmt.Fprintf(s, "%T( %s )", i, i.ID) + } else { + _, _ = fmt.Fprintf(s, "%T[%p]", i, &i) + } + case 'v': + _, _ = fmt.Fprintf(s, "%T[%s] {", i, i.Type) + _ = fmtIntransitiveActivityProps(s)(&i) + _, _ = io.WriteString(s, " }") + } +} + +func fmtIntransitiveActivityProps(w io.Writer) func(*IntransitiveActivity) error { + return func(ia *IntransitiveActivity) error { + if !IsNil(ia.Actor) { + _, _ = fmt.Fprintf(w, " actor: %s", ia.Actor) + } + if !IsNil(ia.Target) { + _, _ = fmt.Fprintf(w, " target: %s", ia.Target) + } + if !IsNil(ia.Result) { + _, _ = fmt.Fprintf(w, " result: %s", ia.Result) + } + if !IsNil(ia.Origin) { + _, _ = fmt.Fprintf(w, " origin: %s", ia.Origin) + } + if !IsNil(ia.Instrument) { + _, _ = fmt.Fprintf(w, " instrument: %s", ia.Instrument) + } + return OnObject(ia, fmtObjectProps(w)) + } +} diff --git a/vendor/github.com/go-ap/activitypub/iri.go b/vendor/github.com/go-ap/activitypub/iri.go new file mode 100644 index 0000000..97b6333 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/iri.go @@ -0,0 +1,429 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "net/url" + "path/filepath" + "strings" + + "github.com/valyala/fastjson" +) + +const ( + // ActivityBaseURI the URI for the ActivityStreams namespace + ActivityBaseURI = IRI("https://www.w3.org/ns/activitystreams") + // SecurityContextURI the URI for the security namespace (for an Actor's PublicKey) + SecurityContextURI = IRI("https://w3id.org/security/v1") + // PublicNS is the reference to the Public entity in the ActivityStreams namespace. + // + // Public Addressing + // + // https://www.w3.org/TR/activitypub/#public-addressing + // + // In addition to [ActivityStreams] collections and objects, Activities may additionally be addressed to the + // special "public" collection, with the identifier https://www.w3.org/ns/activitystreams#Public. For example: + // + // { + // "@context": "https://www.w3.org/ns/activitystreams", + // "id": "https://www.w3.org/ns/activitystreams#Public", + // "type": "Collection" + // } + // Activities addressed to this special URI shall be accessible to all users, without authentication. + // Implementations MUST NOT deliver to the "public" special collection; it is not capable of receiving + // actual activities. However, actors MAY have a sharedInbox endpoint which is available for efficient + // shared delivery of public posts (as well as posts to followers-only); see 7.1.3 Shared Inbox Delivery. + // + // NOTE + // Compacting an ActivityStreams object using the ActivityStreams JSON-LD context might result in + // https://www.w3.org/ns/activitystreams#Public being represented as simply Public or as:Public which are valid + // representations of the Public collection. Implementations which treat ActivityStreams objects as simply JSON + // rather than converting an incoming activity over to a local context using JSON-LD tooling should be aware + // of this and should be prepared to accept all three representations. + PublicNS = ActivityBaseURI + "#Public" +) + +// JsonLDContext is a slice of IRIs that form the default context for the objects in the +// GoActivitypub vocabulary. +// It does not represent just the default ActivityStreams public namespace, but it also +// has the W3 Permanent Identifier Community Group's Security namespace, which appears +// in the Actor type objects, which contain public key related data. +var JsonLDContext = []IRI{ + ActivityBaseURI, + SecurityContextURI, +} + +type ( + // IRI is a Internationalized Resource Identifiers (IRIs) RFC3987 + IRI string + IRIs []IRI +) + +func (i IRI) Format(s fmt.State, verb rune) { + switch verb { + case 's', 'v': + _, _ = io.WriteString(s, i.String()) + } +} + +// String returns the String value of the IRI object +func (i IRI) String() string { + return string(i) +} + +// GetLink +func (i IRI) GetLink() IRI { + return i +} + +// URL +func (i IRI) URL() (*url.URL, error) { + return url.Parse(i.String()) +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (i *IRI) UnmarshalJSON(s []byte) error { + *i = IRI(strings.Trim(string(s), "\"")) + return nil +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (i IRI) MarshalJSON() ([]byte, error) { + if i == "" { + return nil, nil + } + b := make([]byte, 0) + JSONWrite(&b, '"') + JSONWriteS(&b, i.String()) + JSONWrite(&b, '"') + return b, nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (i *IRI) UnmarshalBinary(data []byte) error { + return i.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (i IRI) MarshalBinary() ([]byte, error) { + return i.GobEncode() +} + +// GobEncode +func (i IRI) GobEncode() ([]byte, error) { + return []byte(i), nil +} + +// GobEncode +func (i IRIs) GobEncode() ([]byte, error) { + if len(i) == 0 { + return []byte{}, nil + } + b := bytes.Buffer{} + gg := gob.NewEncoder(&b) + bb := make([][]byte, 0) + for _, iri := range i { + bb = append(bb, []byte(iri)) + } + if err := gg.Encode(bb); err != nil { + return nil, err + } + return b.Bytes(), nil +} + +// GobDecode +func (i *IRI) GobDecode(data []byte) error { + *i = IRI(data) + return nil +} + +func (i *IRIs) GobDecode(data []byte) error { + if len(data) == 0 { + // NOTE(marius): this behaviour diverges from vanilla gob package + return nil + } + err := gob.NewDecoder(bytes.NewReader(data)).Decode(i) + if err == nil { + return nil + } + bb := make([][]byte, 0) + err = gob.NewDecoder(bytes.NewReader(data)).Decode(&bb) + if err != nil { + return err + } + for _, b := range bb { + *i = append(*i, IRI(b)) + } + return nil +} + +// AddPath concatenates el elements as a path to i +func (i IRI) AddPath(el ...string) IRI { + iri := strings.TrimRight(i.String(), "/") + return IRI(iri + filepath.Clean(filepath.Join("/", filepath.Join(el...)))) +} + +// GetID +func (i IRI) GetID() ID { + return i +} + +// GetType +func (i IRI) GetType() ActivityVocabularyType { + return IRIType +} + +// IsLink +func (i IRI) IsLink() bool { + return true +} + +// IsObject +func (i IRI) IsObject() bool { + return false +} + +// IsCollection returns false for IRI objects +func (i IRI) IsCollection() bool { + return false +} + +// FlattenToIRI checks if Item can be flatten to an IRI and returns it if so +func FlattenToIRI(it Item) Item { + if !IsNil(it) && it.IsObject() && len(it.GetLink()) > 0 { + return it.GetLink() + } + return it +} + +func (i IRIs) MarshalJSON() ([]byte, error) { + if len(i) == 0 { + return []byte{'[', ']'}, nil + } + b := make([]byte, 0) + writeCommaIfNotEmpty := func(notEmpty bool) { + if notEmpty { + JSONWriteS(&b, ",") + } + } + JSONWrite(&b, '[') + for k, iri := range i { + writeCommaIfNotEmpty(k > 0) + JSONWrite(&b, '"') + JSONWriteS(&b, iri.String()) + JSONWrite(&b, '"') + } + JSONWrite(&b, ']') + return b, nil +} + +func (i *IRIs) UnmarshalJSON(data []byte) error { + if i == nil { + return nil + } + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + switch val.Type() { + case fastjson.TypeString: + if iri, ok := asIRI(val); ok && len(iri) > 0 { + *i = append(*i, iri) + } + case fastjson.TypeArray: + for _, v := range val.GetArray() { + if iri, ok := asIRI(v); ok && len(iri) > 0 { + *i = append(*i, iri) + } + } + } + return nil +} + +// GetID returns the ID corresponding to ItemCollection +func (i IRIs) GetID() ID { + return EmptyID +} + +// GetLink returns the empty IRI +func (i IRIs) GetLink() IRI { + return EmptyIRI +} + +// GetType returns the ItemCollection's type +func (i IRIs) GetType() ActivityVocabularyType { + return CollectionOfIRIs +} + +// IsLink returns false for an ItemCollection object +func (i IRIs) IsLink() bool { + return false +} + +// IsObject returns true for a ItemCollection object +func (i IRIs) IsObject() bool { + return false +} + +// IsCollection returns true for IRI slices +func (i IRIs) IsCollection() bool { + return true +} + +// Append facilitates adding elements to IRI slices +// and ensures IRIs implements the Collection interface +func (i *IRIs) Append(it ...Item) error { + for _, ob := range it { + if (*i).Contains(ob.GetLink()) { + continue + } + *i = append(*i, ob.GetLink()) + } + return nil +} + +func (i *IRIs) Collection() ItemCollection { + res := make(ItemCollection, len(*i)) + for k, iri := range *i { + res[k] = iri + } + return res +} + +func (i *IRIs) Count() uint { + return uint(len(*i)) +} + +// Contains verifies if IRIs array contains the received one +func (i IRIs) Contains(r Item) bool { + if len(i) == 0 { + return false + } + for _, iri := range i { + if r.GetLink().Equals(iri, false) { + return true + } + } + return false +} + +func validURL(u *url.URL) bool { + return len(u.Scheme) > 0 && len(u.Host) > 0 +} + +// Equals verifies if our receiver IRI is equals with the "with" IRI +func (i IRI) Equals(with IRI, checkScheme bool) bool { + if checkScheme { + if strings.EqualFold(string(i), string(with)) { + return true + } + } else { + is := string(i) + ws := string(with) + ip := strings.Index(is, "://") + if ip < 0 { + ip = 0 + } + wp := strings.Index(ws, "://") + if wp < 0 { + wp = 0 + } + if strings.EqualFold(is[ip:], ws[wp:]) { + return true + } + } + u, e := i.URL() + uw, ew := with.URL() + if e != nil || ew != nil || !validURL(u) || !validURL(uw) { + return strings.EqualFold(i.String(), with.String()) + } + if checkScheme { + if !strings.EqualFold(u.Scheme, uw.Scheme) { + return false + } + } + if !strings.EqualFold(u.Host, uw.Host) { + return false + } + if !(u.Path == "/" && uw.Path == "" || u.Path == "" && uw.Path == "/") && + !strings.EqualFold(filepath.Clean(u.Path), filepath.Clean(uw.Path)) { + return false + } + uq := u.Query() + uwq := uw.Query() + if len(uq) != len(uwq) { + return false + } + for k, uqv := range uq { + uwqv, ok := uwq[k] + if !ok { + return false + } + if len(uqv) != len(uwqv) { + return false + } + for _, uqvv := range uqv { + eq := false + for _, uwqvv := range uwqv { + if uwqvv == uqvv { + eq = true + continue + } + } + if !eq { + return false + } + } + } + return true +} + +func hostSplit(h string) (string, string) { + pieces := strings.Split(h, ":") + if len(pieces) == 0 { + return "", "" + } + if len(pieces) == 1 { + return pieces[0], "" + } + return pieces[0], pieces[1] +} + +func (i IRI) Contains(what IRI, checkScheme bool) bool { + u, e := i.URL() + uw, ew := what.URL() + if e != nil || ew != nil { + return strings.Contains(i.String(), what.String()) + } + if checkScheme { + if u.Scheme != uw.Scheme { + return false + } + } + uHost, _ := hostSplit(u.Host) + uwHost, _ := hostSplit(uw.Host) + if uHost != uwHost { + return false + } + p := u.Path + if p != "" { + p = filepath.Clean(p) + } + pw := uw.Path + if pw != "" { + pw = filepath.Clean(pw) + } + return strings.Contains(p, pw) +} + +func (i IRI) ItemsMatch(col ...Item) bool { + for _, it := range col { + if match := it.GetLink().Contains(i, false); !match { + return false + } + } + return true +} diff --git a/vendor/github.com/go-ap/activitypub/item.go b/vendor/github.com/go-ap/activitypub/item.go new file mode 100644 index 0000000..8fbd31e --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/item.go @@ -0,0 +1,162 @@ +package activitypub + +import ( + "fmt" + "reflect" +) + +// Item struct +type Item = ObjectOrLink + +const ( + // EmptyIRI represents a zero length IRI + EmptyIRI IRI = "" + // NilIRI represents by convention an IRI which is nil + // Its use is mostly to check if a property of an ActivityPub Item is nil + NilIRI IRI = "-" + + // EmptyID represents a zero length ID + EmptyID = EmptyIRI + // NilID represents by convention an ID which is nil, see details of NilIRI + NilID = NilIRI +) + +// ItemsEqual checks if it and with Items are equal +func ItemsEqual(it, with Item) bool { + if IsNil(it) || IsNil(with) { + return with == it + } + result := false + if it.IsCollection() { + if it.GetType() == CollectionOfItems { + _ = OnItemCollection(it, func(c *ItemCollection) error { + result = c.Equals(with) + return nil + }) + } + if it.GetType() == CollectionType { + _ = OnCollection(it, func(c *Collection) error { + result = c.Equals(with) + return nil + }) + } + if it.GetType() == OrderedCollectionType { + _ = OnOrderedCollection(it, func(c *OrderedCollection) error { + result = c.Equals(with) + return nil + }) + } + if it.GetType() == CollectionPageType { + _ = OnCollectionPage(it, func(c *CollectionPage) error { + result = c.Equals(with) + return nil + }) + } + if it.GetType() == OrderedCollectionPageType { + _ = OnOrderedCollectionPage(it, func(c *OrderedCollectionPage) error { + result = c.Equals(with) + return nil + }) + } + } else if it.IsObject() { + if ActivityTypes.Contains(with.GetType()) { + _ = OnActivity(it, func(i *Activity) error { + result = i.Equals(with) + return nil + }) + } else if ActorTypes.Contains(with.GetType()) { + _ = OnActor(it, func(i *Actor) error { + result = i.Equals(with) + return nil + }) + } else { + _ = OnObject(it, func(i *Object) error { + result = i.Equals(with) + return nil + }) + } + } + if with.IsLink() { + result = with.GetLink().Equals(it.GetLink(), false) + } + return result +} + +// IsItemCollection returns if the current Item interface holds a Collection +func IsItemCollection(it Item) bool { + _, ok := it.(ItemCollection) + _, okP := it.(*ItemCollection) + return ok || okP +} + +// IsIRI returns if the current Item interface holds an IRI +func IsIRI(it Item) bool { + _, okV := it.(IRI) + _, okP := it.(*IRI) + return okV || okP +} + +// IsIRIs returns if the current Item interface holds an IRI slice +func IsIRIs(it Item) bool { + _, okV := it.(IRIs) + _, okP := it.(*IRIs) + return okV || okP +} + +// IsLink returns if the current Item interface holds a Link +func IsLink(it Item) bool { + _, okV := it.(Link) + _, okP := it.(*Link) + return okV || okP +} + +// IsObject returns if the current Item interface holds an Object +func IsObject(it Item) bool { + switch ob := it.(type) { + case Actor, *Actor, + Object, *Object, Profile, *Profile, Place, *Place, Relationship, *Relationship, Tombstone, *Tombstone, + Activity, *Activity, IntransitiveActivity, *IntransitiveActivity, Question, *Question, + Collection, *Collection, CollectionPage, *CollectionPage, + OrderedCollection, *OrderedCollection, OrderedCollectionPage, *OrderedCollectionPage: + return ob != nil + default: + return false + } +} + +// IsNil checks if the object matching an ObjectOrLink interface is nil +func IsNil(it Item) bool { + if it == nil { + return true + } + // This is the default if the argument can't be cast to Object, as is the case for an ItemCollection + isNil := false + if IsItemCollection(it) { + OnItemCollection(it, func(c *ItemCollection) error { + isNil = c == nil + return nil + }) + } else if IsObject(it) { + OnObject(it, func(o *Object) error { + isNil = o == nil + return nil + }) + } else if IsLink(it) { + OnLink(it, func(l *Link) error { + isNil = l == nil + return nil + }) + } else if IsIRI(it) { + isNil = len(it.GetLink()) == 0 + } else { + // NOTE(marius): we're not dealing with a type that we know about, so we use slow reflection + // as we still care about the result + v := reflect.ValueOf(it) + isNil = v.Kind() == reflect.Pointer && v.IsNil() + } + return isNil +} + +func ErrorInvalidType[T Objects | Links | IRIs](received Item) error { + return fmt.Errorf("unable to convert %T to %T", received, new(T)) +} diff --git a/vendor/github.com/go-ap/activitypub/item_collection.go b/vendor/github.com/go-ap/activitypub/item_collection.go new file mode 100644 index 0000000..dd6d37c --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/item_collection.go @@ -0,0 +1,290 @@ +package activitypub + +import ( + "reflect" + "sort" +) + +// ItemCollection represents an array of items +type ItemCollection []Item + +// GetID returns the ID corresponding to ItemCollection +func (i ItemCollection) GetID() ID { + return EmptyID +} + +// GetLink returns the empty IRI +func (i ItemCollection) GetLink() IRI { + return EmptyIRI +} + +// GetType returns the ItemCollection's type +func (i ItemCollection) GetType() ActivityVocabularyType { + return CollectionOfItems +} + +// IsLink returns false for an ItemCollection object +func (i ItemCollection) IsLink() bool { + return false +} + +// IsObject returns true for a ItemCollection object +func (i ItemCollection) IsObject() bool { + return false +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (i ItemCollection) MarshalJSON() ([]byte, error) { + if i == nil { + return nil, nil + } + b := make([]byte, 0) + JSONWriteItemCollectionValue(&b, i, true) + return b, nil +} + +// Append facilitates adding elements to Item arrays +// and ensures ItemCollection implements the Collection interface +func (i *ItemCollection) Append(it ...Item) error { + for _, ob := range it { + if i.Contains(ob) { + continue + } + *i = append(*i, ob) + } + return nil +} + +// Count returns the length of Items in the item collection +func (i *ItemCollection) Count() uint { + if i == nil { + return 0 + } + return uint(len(*i)) +} + +// First returns the ID corresponding to ItemCollection +func (i ItemCollection) First() Item { + if len(i) == 0 { + return nil + } + return i[0] +} + +// Normalize returns the first item if the collection contains only one, +// the full collection if the collection contains more than one item, +// or nil +func (i ItemCollection) Normalize() Item { + if len(i) == 0 { + return nil + } + if len(i) == 1 { + return i[0] + } + return i +} + +// Collection returns the current object as collection interface +func (i *ItemCollection) Collection() ItemCollection { + return *i +} + +// IsCollection returns true for ItemCollection arrays +func (i ItemCollection) IsCollection() bool { + return true +} + +// Contains verifies if IRIs array contains the received one +func (i ItemCollection) Contains(r Item) bool { + if len(i) == 0 { + return false + } + for _, it := range i { + if ItemsEqual(it, r) { + return true + } + } + return false +} + +// Remove removes the r Item from the i ItemCollection if it contains it +func (i *ItemCollection) Remove(r Item) { + li := len(*i) + if li == 0 { + return + } + if r == nil { + return + } + remIdx := -1 + for idx, it := range *i { + if ItemsEqual(it, r) { + remIdx = idx + } + } + if remIdx == -1 { + return + } + if remIdx < li-1 { + *i = append((*i)[:remIdx], (*i)[remIdx+1:]...) + } else { + *i = (*i)[:remIdx] + } +} + +// ItemCollectionDeduplication normalizes the received arguments lists into a single unified one +func ItemCollectionDeduplication(recCols ...*ItemCollection) ItemCollection { + rec := make(ItemCollection, 0) + + for _, recCol := range recCols { + if recCol == nil { + continue + } + + toRemove := make([]int, 0) + for i, cur := range *recCol { + save := true + if cur == nil { + continue + } + var testIt IRI + if cur.IsObject() { + testIt = cur.GetID() + } else if cur.IsLink() { + testIt = cur.GetLink() + } else { + continue + } + for _, it := range rec { + if testIt.Equals(it.GetID(), false) { + // mark the element for removal + toRemove = append(toRemove, i) + save = false + } + } + if save { + rec = append(rec, testIt) + } + } + + sort.Sort(sort.Reverse(sort.IntSlice(toRemove))) + for _, idx := range toRemove { + *recCol = append((*recCol)[:idx], (*recCol)[idx+1:]...) + } + } + return rec +} + +// ToItemCollection +func ToItemCollection(it Item) (*ItemCollection, error) { + switch i := it.(type) { + case *ItemCollection: + return i, nil + case ItemCollection: + return &i, nil + case *OrderedCollection: + return &i.OrderedItems, nil + case *OrderedCollectionPage: + return &i.OrderedItems, nil + case *Collection: + return &i.Items, nil + case *CollectionPage: + return &i.Items, nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(ItemCollection)) + if reflect.TypeOf(it).ConvertibleTo(typ) { + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*ItemCollection); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[ItemCollection](it) +} + +// ToIRIs +func ToIRIs(it Item) (*IRIs, error) { + switch i := it.(type) { + case *IRIs: + return i, nil + case IRIs: + return &i, nil + case ItemCollection: + iris := make(IRIs, len(i)) + for j, ob := range i { + iris[j] = ob.GetLink() + } + return &iris, nil + case *ItemCollection: + iris := make(IRIs, len(*i)) + for j, ob := range *i { + iris[j] = ob.GetLink() + } + return &iris, nil + default: + // NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes + typ := reflect.TypeOf(new(IRIs)) + if reflect.TypeOf(it).ConvertibleTo(typ) { + if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*IRIs); ok { + return i, nil + } + } + } + return nil, ErrorInvalidType[IRIs](it) +} + +// ItemsMatch +func (i ItemCollection) ItemsMatch(col ...Item) bool { + for _, it := range col { + if match := i.Contains(it); !match { + return false + } + } + return true +} + +// Equals +func (i ItemCollection) Equals(with Item) bool { + if IsNil(with) { + return false + } + if !with.IsCollection() { + return false + } + if with.GetType() != CollectionOfItems { + return false + } + result := true + OnItemCollection(with, func(w *ItemCollection) error { + if w.Count() != i.Count() { + result = false + return nil + } + for _, it := range i { + if !w.Contains(it.GetLink()) { + result = false + return nil + } + } + return nil + }) + return result +} + +// Clean removes Bto and BCC properties on all the members of the collection +func (i ItemCollection) Clean() { + for j, it := range i { + i[j] = CleanRecipients(it) + } +} + +func (i ItemCollection) Recipients() ItemCollection { + all := make(ItemCollection, 0) + for _, it := range i { + _ = OnObject(it, func(ob *Object) error { + _ = all.Append(ItemCollectionDeduplication(&ob.To, &ob.Bto, &ob.CC, &ob.BCC, &ob.Audience)...) + return nil + }) + } + return ItemCollectionDeduplication(&all) +} diff --git a/vendor/github.com/go-ap/activitypub/link.go b/vendor/github.com/go-ap/activitypub/link.go new file mode 100644 index 0000000..004c8f1 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/link.go @@ -0,0 +1,165 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "github.com/valyala/fastjson" +) + +// LinkTypes represent the valid values for a Link object +var LinkTypes = ActivityVocabularyTypes{ + LinkType, + MentionType, +} + +type Links interface { + Link | IRI +} + +// A Link is an indirect, qualified reference to a resource identified by a URL. +// The fundamental model for links is established by [ RFC5988]. +// Many of the properties defined by the Activity Vocabulary allow values that are either instances of APObject or Link. +// When a Link is used, it establishes a qualified relation connecting the subject +// (the containing object) to the resource identified by the href. +// Properties of the Link are properties of the reference as opposed to properties of the resource. +type Link struct { + // Provides the globally unique identifier for an APObject or Link. + ID ID `jsonld:"id,omitempty"` + // Identifies the APObject or Link type. Multiple values may be specified. + Type ActivityVocabularyType `jsonld:"type,omitempty"` + // A simple, human-readable, plain-text name for the object. + // HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values. + Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"` + // A link relation associated with a Link. The value must conform to both the [HTML5] and + // [RFC5988](https://tools.ietf.org/html/rfc5988) "link relation" definitions. + // In the [HTML5], any string not containing the "space" U+0020, "tab" (U+0009), "LF" (U+000A), + // "FF" (U+000C), "CR" (U+000D) or "," (U+002C) characters can be used as a valid link relation. + Rel IRI `jsonld:"rel,omitempty"` + // When used on a Link, identifies the MIME media type of the referenced resource. + MediaType MimeType `jsonld:"mediaType,omitempty"` + // On a Link, specifies a hint as to the rendering height in device-independent pixels of the linked resource. + Height uint `jsonld:"height,omitempty"` + // On a Link, specifies a hint as to the rendering width in device-independent pixels of the linked resource. + Width uint `jsonld:"width,omitempty"` + // Identifies an entity that provides a preview of this object. + Preview Item `jsonld:"preview,omitempty"` + // The target resource pointed to by a Link. + Href IRI `jsonld:"href,omitempty"` + // Hints as to the language used by the target resource. + // Value must be a [BCP47](https://tools.ietf.org/html/bcp47) Language-Tag. + HrefLang LangRef `jsonld:"hrefLang,omitempty"` +} + +// Mention is a specialized Link that represents an @mention. +type Mention = Link + +// LinkNew initializes a new Link +func LinkNew(id ID, typ ActivityVocabularyType) *Link { + if !LinkTypes.Contains(typ) { + typ = LinkType + } + return &Link{ID: id, Type: typ} +} + +// MentionNew initializes a new Mention +func MentionNew(id ID) *Mention { + return &Mention{ID: id, Type: MentionType} +} + +// IsLink validates if current Link is a Link +func (l Link) IsLink() bool { + return l.Type == LinkType || LinkTypes.Contains(l.Type) +} + +// IsObject validates if current Link is an GetID +func (l Link) IsObject() bool { + return l.Type == ObjectType || ObjectTypes.Contains(l.Type) +} + +// IsCollection returns false for Link objects +func (l Link) IsCollection() bool { + return false +} + +// GetID returns the ID corresponding to the Link object +func (l Link) GetID() ID { + return l.ID +} + +// GetLink returns the IRI corresponding to the current Link +func (l Link) GetLink() IRI { + return IRI(l.ID) +} + +// GetType returns the Type corresponding to the Mention object +func (l Link) GetType() ActivityVocabularyType { + return l.Type +} + +// MarshalJSON encodes the receiver object to a JSON document. +func (l Link) MarshalJSON() ([]byte, error) { + b := make([]byte, 0) + JSONWrite(&b, '{') + + if JSONWriteLinkValue(&b, l) { + JSONWrite(&b, '}') + return b, nil + } + return nil, nil +} + +// UnmarshalJSON decodes an incoming JSON document into the receiver object. +func (l *Link) UnmarshalJSON(data []byte) error { + p := fastjson.Parser{} + val, err := p.ParseBytes(data) + if err != nil { + return err + } + return JSONLoadLink(val, l) +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (l *Link) UnmarshalBinary(data []byte) error { + return l.GobDecode(data) +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (l Link) MarshalBinary() ([]byte, error) { + return l.GobEncode() +} + +func (l Link) GobEncode() ([]byte, error) { + mm := make(map[string][]byte) + hasData, err := mapLinkProperties(mm, l) + if err != nil { + return nil, err + } + if !hasData { + return []byte{}, nil + } + bb := bytes.Buffer{} + g := gob.NewEncoder(&bb) + if err := g.Encode(mm); err != nil { + return nil, err + } + return bb.Bytes(), nil +} + +func (l *Link) GobDecode(data []byte) error { + if len(data) == 0 { + return nil + } + mm, err := gobDecodeObjectAsMap(data) + if err != nil { + return err + } + return unmapLinkProperties(mm, l) +} + +func (l Link) Format(s fmt.State, verb rune) { + switch verb { + case 's', 'v': + _, _ = fmt.Fprintf(s, "%T[%s] { }", l, l.Type) + } +} diff --git a/vendor/github.com/go-ap/activitypub/natural_language_values.go b/vendor/github.com/go-ap/activitypub/natural_language_values.go new file mode 100644 index 0000000..62d58c7 --- /dev/null +++ b/vendor/github.com/go-ap/activitypub/natural_language_values.go @@ -0,0 +1,812 @@ +package activitypub + +import ( + "bytes" + "encoding/gob" + "fmt" + "io" + "strings" + "unicode/utf8" + + "github.com/valyala/fastjson" +) + +// NilLangRef represents a convention for a nil language reference. +// It is used for LangRefValue objects without an explicit language key. +const NilLangRef LangRef = "-" + +// DefaultLang represents the default language reference used when using the convenience content generation. +var DefaultLang = NilLangRef + +type ( + // LangRef is the type for a language reference code, should be an ISO639-1 language specifier. + LangRef string + Content []byte + + // LangRefValue is a type for storing per language values + LangRefValue struct { + Ref LangRef + Value Content + } + // NaturalLanguageValues is a mapping for multiple language values + NaturalLanguageValues []LangRefValue +) + +func NaturalLanguageValuesNew(values ...LangRefValue) NaturalLanguageValues { + n := make(NaturalLanguageValues, len(values)) + for i, val := range values { + n[i] = val + } + return n +} + +func DefaultNaturalLanguageValue(content string) NaturalLanguageValues { + return NaturalLanguageValuesNew(DefaultLangRef(content)) +} + +func (n NaturalLanguageValues) String() string { + cnt := len(n) + if cnt == 1 { + return n[0].String() + } + s := strings.Builder{} + s.Write([]byte{'['}) + for k, v := range n { + s.WriteString(v.String()) + if k != cnt-1 { + s.Write([]byte{','}) + } + } + s.Write([]byte{']'}) + return s.String() +} + +func (n NaturalLanguageValues) Get(ref LangRef) Content { + for _, val := range n { + if val.Ref == ref { + return val.Value + } + } + return nil +} + +// Set sets a language, value pair in a NaturalLanguageValues array +func (n *NaturalLanguageValues) Set(ref LangRef, v Content) error { + found := false + for k, vv := range *n { + if vv.Ref == ref { + (*n)[k] = LangRefValue{ref, v} + found = true + } + } + if !found { + n.Append(ref, v) + } + return nil +} + +func (n *NaturalLanguageValues) Add(ref LangRefValue) { + *n = append(*n, ref) +} + +var hex = "0123456789abcdef" + +// safeSet holds the value true if the ASCII character with the given array +// position can be represented inside a JSON string without any further +// escaping. +// +// All values are true except for the ASCII control characters (0-31), the +// double quote ("), and the backslash character ("\"). +var safeSet = [utf8.RuneSelf]bool{ + ' ': true, + '!': true, + '"': false, + '#': true, + '$': true, + '%': true, + '&': true, + '\'': true, + '(': true, + ')': true, + '*': true, + '+': true, + ',': true, + '-': true, + '.': true, + '/': true, + '0': true, + '1': true, + '2': true, + '3': true, + '4': true, + '5': true, + '6': true, + '7': true, + '8': true, + '9': true, + ':': true, + ';': true, + '<': true, + '=': true, + '>': true, + '?': true, + '@': true, + 'A': true, + 'B': true, + 'C': true, + 'D': true, + 'E': true, + 'F': true, + 'G': true, + 'H': true, + 'I': true, + 'J': true, + 'K': true, + 'L': true, + 'M': true, + 'N': true, + 'O': true, + 'P': true, + 'Q': true, + 'R': true, + 'S': true, + 'T': true, + 'U': true, + 'V': true, + 'W': true, + 'X': true, + 'Y': true, + 'Z': true, + '[': true, + '\\': false, + ']': true, + '^': true, + '_': true, + '`': true, + 'a': true, + 'b': true, + 'c': true, + 'd': true, + 'e': true, + 'f': true, + 'g': true, + 'h': true, + 'i': true, + 'j': true, + 'k': true, + 'l': true, + 'm': true, + 'n': true, + 'o': true, + 'p': true, + 'q': true, + 'r': true, + 's': true, + 't': true, + 'u': true, + 'v': true, + 'w': true, + 'x': true, + 'y': true, + 'z': true, + '{': true, + '|': true, + '}': true, + '~': true, + '\u007f': true, +} + +// htmlSafeSet holds the value true if the ASCII character with the given +// array position can be safely represented inside a JSON string, embedded +// inside of HTML