Merge branch 'feature/handlers' into develop
This commit is contained in:
commit
326b7a478a
8
go.mod
8
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
|
||||
)
|
||||
|
|
10
go.sum
10
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=
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-ap/activitypub"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
)
|
||||
|
||||
type (
|
||||
Handler struct{}
|
||||
|
||||
/* TODO(toby3d)
|
||||
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() *Handler {
|
||||
return &Handler{}
|
||||
}
|
||||
|
||||
func (Handler) HandleProfile(site *domain.Site) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
langRef := activitypub.LangRef(site.DefaultLanguage.Lang())
|
||||
person := activitypub.PersonNew(activitypub.IRI(site.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)
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
|
||||
_ = json.NewEncoder(w).Encode(person)
|
||||
})
|
||||
}
|
||||
|
||||
func (Handler) HandleEntry(site *domain.Site, entry *domain.Entry) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
resp := activitypub.ObjectNew(activitypub.NoteType)
|
||||
resp.ID = activitypub.ID(site.BaseURL.JoinPath(entry.File.Path()).String())
|
||||
resp.Content.Add(activitypub.LangRefValueNew(activitypub.LangRef(entry.Language.Lang()),
|
||||
string(entry.Content)))
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMEApplicationActivityJSONCharsetUTF8)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
}
|
|
@ -1,34 +1,34 @@
|
|||
package home
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
activitypubhttpdelivery "source.toby3d.me/toby3d/home/internal/activitypub/delivery/http"
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/entry"
|
||||
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"
|
||||
resourcehttpdelivery "source.toby3d.me/toby3d/home/internal/resource/delivery/http"
|
||||
resourcefsrepo "source.toby3d.me/toby3d/home/internal/resource/repository/fs"
|
||||
resourceucase "source.toby3d.me/toby3d/home/internal/resource/usecase"
|
||||
servercase "source.toby3d.me/toby3d/home/internal/server/usecase"
|
||||
sitehttpdelivery "source.toby3d.me/toby3d/home/internal/site/delivery/http"
|
||||
sitefsrepo "source.toby3d.me/toby3d/home/internal/site/repository/fs"
|
||||
siteucase "source.toby3d.me/toby3d/home/internal/site/usecase"
|
||||
statichttpdelivery "source.toby3d.me/toby3d/home/internal/static/delivery/http"
|
||||
staticfsrepo "source.toby3d.me/toby3d/home/internal/static/repository/fs"
|
||||
staticucase "source.toby3d.me/toby3d/home/internal/static/usecase"
|
||||
themehttpdelivery "source.toby3d.me/toby3d/home/internal/theme/delivery/http"
|
||||
themefsrepo "source.toby3d.me/toby3d/home/internal/theme/repository/fs"
|
||||
themeucase "source.toby3d.me/toby3d/home/internal/theme/usecase"
|
||||
"source.toby3d.me/toby3d/home/internal/urlutil"
|
||||
|
@ -62,80 +62,25 @@ func NewApp(logger *log.Logger, config *domain.Config) (*App, error) {
|
|||
serverer := servercase.NewServerUseCase(sites)
|
||||
webfingerer := webfingerucase.NewWebFingerUseCase(sites)
|
||||
webfingerHandler := webfingerhttpdelivery.NewHandler(webfingerer)
|
||||
resourceHandler := resourcehttpdelivery.NewHandler(resourcer, contentDir)
|
||||
staticHandler := statichttpdelivery.NewHandler(staticer)
|
||||
siteHandler := sitehttpdelivery.NewHandler(siter)
|
||||
themeHandler := themehttpdelivery.NewHandler(themer)
|
||||
activityPubHadnler := activitypubhttpdelivery.NewHandler()
|
||||
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 {
|
||||
http.ServeContent(w, r, static.Name(), static.ModTime(), static)
|
||||
// NOTE(toby3d): any file in $HOME_STATIC_DIR 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 handler, err := staticHandler.Handle(r.Context(), r.URL.Path); err == nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
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 "":
|
||||
if s.IsMultiLingual() {
|
||||
supported := make([]language.Tag, len(s.Languages))
|
||||
for i := range s.Languages {
|
||||
supported[i] = language.Make(s.Languages[i].Lang())
|
||||
}
|
||||
|
||||
if s.DefaultLanguage != domain.LanguageUnd {
|
||||
supported = append([]language.Tag{language.Make(s.DefaultLanguage.Code())},
|
||||
supported...)
|
||||
}
|
||||
|
||||
requested, _, _ := language.ParseAcceptLanguage(r.Header.Get(
|
||||
common.HeaderAcceptLanguage))
|
||||
if len(requested) == 0 {
|
||||
requested = append(requested, language.English)
|
||||
}
|
||||
|
||||
matched, _, _ := language.NewMatcher(supported).Match(requested...)
|
||||
lang = domain.NewLanguage(matched.String())
|
||||
|
||||
http.Redirect(w, r, "/"+lang.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,83 +90,53 @@ 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.
|
||||
lang = domain.NewLanguage(head)
|
||||
|
||||
// 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 {
|
||||
// TODO(toby3d) : ugly workaround, must be refactored
|
||||
resFile, err := contentDir.Open(res.File.Path())
|
||||
if err != nil {
|
||||
http.Error(w, "cannot open: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
defer resFile.Close()
|
||||
|
||||
resBytes, err := io.ReadAll(resFile)
|
||||
if err != nil {
|
||||
http.Error(w, "cannot read all: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
defer resFile.Close()
|
||||
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), bytes.NewReader(resBytes))
|
||||
// NOTE(toby3d): read $HOME_CONTENT_DIR/index.md as a source of
|
||||
// truth and global settings for any child entry.
|
||||
site, handler, err := siteHandler.Handle(r.Context(), head)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
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)
|
||||
mediaType, _, _ := mime.ParseMediaType(r.Header.Get(common.HeaderAccept))
|
||||
|
||||
if handler != nil {
|
||||
if strings.EqualFold(mediaType, common.MIMEApplicationLdJSON) {
|
||||
activityPubHadnler.HandleProfile(site).ServeHTTP(w, r)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
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()
|
||||
if site.IsMultiLingual() {
|
||||
r.URL.Path = tail
|
||||
}
|
||||
|
||||
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)
|
||||
if handler, err = resourceHandler.Handle(r.Context(), r.URL.Path); err == nil {
|
||||
handler.ServeHTTP(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentType, common.MIMETextHTMLCharsetUTF8)
|
||||
if err = template(w); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
entry, err := entrier.Do(r.Context(), site.Language, r.URL.Path)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(mediaType) {
|
||||
default:
|
||||
themeHandler.Handle(site, entry).ServeHTTP(w, r)
|
||||
case common.MIMEApplicationLdJSON:
|
||||
activityPubHadnler.HandleEntry(site, entry).ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
chain := middleware.Chain{
|
||||
middleware.LogFmt(),
|
||||
// middleware.LogFmt(),
|
||||
middleware.Redirect(middleware.RedirectConfig{Serverer: serverer}),
|
||||
middleware.Header(middleware.HeaderConfig{Serverer: serverer}),
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
|
||||
"github.com/adrg/frontmatter"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/entry"
|
||||
)
|
||||
|
||||
type (
|
||||
Page struct {
|
||||
Title string `yaml:"title"`
|
||||
Description string `yaml:"description"`
|
||||
Params map[string]any `yaml:",inline"`
|
||||
Content []byte `yaml:"-"`
|
||||
}
|
||||
|
||||
memoryEntryRepository struct {
|
||||
mutex *sync.RWMutex
|
||||
files fstest.MapFS
|
||||
}
|
||||
)
|
||||
|
||||
var FrontMatterFormats = []*frontmatter.Format{
|
||||
frontmatter.NewFormat(`---`, `---`, yaml.Unmarshal),
|
||||
}
|
||||
|
||||
func (repo *memoryEntryRepository) Get(_ context.Context, lang domain.Language, p string) (*domain.Entry, error) {
|
||||
f, err := repo.files.Open(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get entry from memory: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data := &Page{
|
||||
Params: make(map[string]any),
|
||||
}
|
||||
if data.Content, err = frontmatter.Parse(f, data, FrontMatterFormats...); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse entry content as FrontMatter: %w", err)
|
||||
}
|
||||
|
||||
return &domain.Entry{
|
||||
File: domain.NewPath(p),
|
||||
Language: lang,
|
||||
Title: data.Title,
|
||||
Content: data.Content,
|
||||
Description: data.Description,
|
||||
Params: data.Params,
|
||||
Resources: make([]*domain.Resource, 0),
|
||||
Translations: make([]*domain.Entry, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *memoryEntryRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
|
||||
_, err := fs.Stat(repo.files, p)
|
||||
|
||||
return errors.Is(err, fs.ErrExist), nil
|
||||
}
|
||||
|
||||
func NewMemoryEntryRepository(files fstest.MapFS) entry.Repository {
|
||||
return &memoryEntryRepository{
|
||||
mutex: new(sync.RWMutex),
|
||||
files: files,
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/entry"
|
||||
)
|
||||
|
||||
type memoryEntryRepository struct {
|
||||
mutex *sync.RWMutex
|
||||
files fstest.MapFS
|
||||
}
|
||||
|
||||
func (repo *memoryEntryRepository) Get(_ context.Context, _ domain.Language, p string) (*domain.Page, error) {
|
||||
f, err := repo.files.Open(p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get entry from memory: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (repo *memoryEntryRepository) Stat(_ context.Context, l domain.Language, p string) (bool, error) {
|
||||
_, err := fs.Stat(repo.files, p)
|
||||
|
||||
return errors.Is(err, fs.ErrExist), nil
|
||||
}
|
||||
|
||||
func NewMemoryEntryRepository(files fstest.MapFS) entry.Repository {
|
||||
return &memoryEntryRepository{
|
||||
mutex: new(sync.RWMutex),
|
||||
files: files,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/resource"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
contentDir fs.FS
|
||||
resources resource.UseCase
|
||||
}
|
||||
|
||||
func NewHandler(resources resource.UseCase, contentDir fs.FS) *Handler {
|
||||
return &Handler{
|
||||
contentDir: contentDir,
|
||||
resources: resources,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(ctx context.Context, path string) (http.Handler, error) {
|
||||
res, err := h.resources.Do(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := h.contentDir.Open(res.File.Filename())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open resource: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
resBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read resource: %w", err)
|
||||
}
|
||||
|
||||
// TODO(toby3d): ugly workaround, refactor that.
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, res.Name(), domain.ResourceModTime(res), bytes.NewReader(resBytes))
|
||||
}), nil
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestDo(t *testing.T) {
|
|||
|
||||
site := domain.TestSite(t)
|
||||
|
||||
actual, err := usecase.NewServerUseCase().Do(context.Background(), *site)
|
||||
actual, err := usecase.NewServerUseCase().Do(context.Background(), site)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/site"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
sites site.UseCase
|
||||
}
|
||||
|
||||
func NewHandler(sites site.UseCase) *Handler {
|
||||
return &Handler{sites: sites}
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(ctx context.Context, head string) (*domain.Site, http.Handler, error) {
|
||||
lang := domain.NewLanguage(head)
|
||||
|
||||
site, err := h.sites.Do(ctx, lang)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("cannot handle site: %w", err)
|
||||
}
|
||||
|
||||
if !site.IsMultiLingual() || head != "" {
|
||||
return site, nil, nil
|
||||
}
|
||||
|
||||
supported := make([]language.Tag, len(site.Languages))
|
||||
for i := range site.Languages {
|
||||
supported[i] = language.Make(site.Languages[i].Lang())
|
||||
}
|
||||
|
||||
if site.DefaultLanguage != domain.LanguageUnd {
|
||||
supported = append([]language.Tag{language.Make(site.DefaultLanguage.Code())}, supported...)
|
||||
}
|
||||
|
||||
return site, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requested, _, _ := language.ParseAcceptLanguage(r.Header.Get(common.HeaderAcceptLanguage))
|
||||
if len(requested) == 0 {
|
||||
requested = append(requested, language.English)
|
||||
}
|
||||
|
||||
matched, _, _ := language.NewMatcher(supported).Match(requested...)
|
||||
|
||||
http.Redirect(w, r, "/"+domain.NewLanguage(matched.String()).Lang()+"/", http.StatusSeeOther)
|
||||
}), nil
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/static"
|
||||
)
|
||||
|
@ -18,17 +16,13 @@ func NewHandler(static static.UseCase) *Handler {
|
|||
return &Handler{static: static}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s, err := h.static.Do(r.Context(), strings.TrimPrefix(path.Clean(r.URL.Path), "/"))
|
||||
func (h *Handler) Handle(ctx context.Context, path string) (http.Handler, error) {
|
||||
static, err := h.static.Do(ctx, path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
http.NotFound(w, r)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return
|
||||
return nil, fmt.Errorf("cannot handle static: %w", err)
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, s.Name(), s.ModTime(), s)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, static.Name(), static.ModTime(), static)
|
||||
}), nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package http_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
@ -18,11 +19,16 @@ func TestHandler_ServeHTTP(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/robots.txt", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
testStatic := domain.NewStatic(strings.NewReader("User-agent: *\nAllow: /"), time.Now().UTC(), "robots.txt")
|
||||
delivery.NewHandler(usecase.NewStaticUseCase(repository.NewStubStaticRepository(nil, testStatic, false))).
|
||||
ServeHTTP(w, req)
|
||||
staticService := usecase.NewStaticUseCase(repository.NewStubStaticRepository(nil, testStatic, false))
|
||||
|
||||
handler, err := delivery.NewHandler(staticService).Handle(context.Background(), req.URL.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
resp := w.Result()
|
||||
if expect := http.StatusOK; resp.StatusCode != expect {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"source.toby3d.me/toby3d/home/internal/common"
|
||||
"source.toby3d.me/toby3d/home/internal/domain"
|
||||
"source.toby3d.me/toby3d/home/internal/theme"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
themes theme.UseCase
|
||||
}
|
||||
|
||||
func NewHandler(themes theme.UseCase) *Handler {
|
||||
return &Handler{
|
||||
themes: themes,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Handle(site *domain.Site, entry *domain.Entry) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(toby3d): handle home page.
|
||||
// TODO(toby3d): handle sections.
|
||||
// TODO(toby3d): handle errors.
|
||||
|
||||
// NOTE(toby3d): wrap founded entry into theme template and
|
||||
// answer to client.
|
||||
contentLanguage := make([]string, len(entry.Translations))
|
||||
for i := range entry.Translations {
|
||||
contentLanguage[i] = entry.Translations[i].Language.Code()
|
||||
}
|
||||
|
||||
w.Header().Set(common.HeaderContentLanguage, strings.Join(contentLanguage, ", "))
|
||||
|
||||
template, err := h.themes.Do(r.Context(), site, entry)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,812 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// NilLangRef represents a convention for a nil language reference.
|
||||
// It is used for LangRefValue objects without an explicit language key.
|
||||
const NilLangRef LangRef = "-"
|
||||
|
||||
// DefaultLang represents the default language reference used when using the convenience content generation.
|
||||
var DefaultLang = NilLangRef
|
||||
|
||||
type (
|
||||
// LangRef is the type for a language reference code, should be an ISO639-1 language specifier.
|
||||
LangRef string
|
||||
Content []byte
|
||||
|
||||
// LangRefValue is a type for storing per language values
|
||||
LangRefValue struct {
|
||||
Ref LangRef
|
||||
Value Content
|
||||
}
|
||||
// NaturalLanguageValues is a mapping for multiple language values
|
||||
NaturalLanguageValues []LangRefValue
|
||||
)
|
||||
|
||||
func NaturalLanguageValuesNew(values ...LangRefValue) NaturalLanguageValues {
|
||||
n := make(NaturalLanguageValues, len(values))
|
||||
for i, val := range values {
|
||||
n[i] = val
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func DefaultNaturalLanguageValue(content string) NaturalLanguageValues {
|
||||
return NaturalLanguageValuesNew(DefaultLangRef(content))
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) String() string {
|
||||
cnt := len(n)
|
||||
if cnt == 1 {
|
||||
return n[0].String()
|
||||
}
|
||||
s := strings.Builder{}
|
||||
s.Write([]byte{'['})
|
||||
for k, v := range n {
|
||||
s.WriteString(v.String())
|
||||
if k != cnt-1 {
|
||||
s.Write([]byte{','})
|
||||
}
|
||||
}
|
||||
s.Write([]byte{']'})
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) Get(ref LangRef) Content {
|
||||
for _, val := range n {
|
||||
if val.Ref == ref {
|
||||
return val.Value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set sets a language, value pair in a NaturalLanguageValues array
|
||||
func (n *NaturalLanguageValues) Set(ref LangRef, v Content) error {
|
||||
found := false
|
||||
for k, vv := range *n {
|
||||
if vv.Ref == ref {
|
||||
(*n)[k] = LangRefValue{ref, v}
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
n.Append(ref, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NaturalLanguageValues) Add(ref LangRefValue) {
|
||||
*n = append(*n, ref)
|
||||
}
|
||||
|
||||
var hex = "0123456789abcdef"
|
||||
|
||||
// safeSet holds the value true if the ASCII character with the given array
|
||||
// position can be represented inside a JSON string without any further
|
||||
// escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), and the backslash character ("\").
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
// htmlSafeSet holds the value true if the ASCII character with the given
|
||||
// array position can be safely represented inside a JSON string, embedded
|
||||
// inside of HTML <script> tags, without any additional escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), the backslash character ("\"), HTML opening and closing
|
||||
// tags ("<" and ">"), and the ampersand ("&").
|
||||
var htmlSafeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': false,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': false,
|
||||
'=': true,
|
||||
'>': false,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
// NOTE: keep in sync with string above.
|
||||
func stringBytes(e *bytes.Buffer, s []byte, escapeHTML bool) {
|
||||
e.WriteRune('"')
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if htmlSafeSet[b] || (!escapeHTML && safeSet[b]) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteRune('\\')
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
e.WriteRune(rune(b))
|
||||
case '\n':
|
||||
e.WriteRune('n')
|
||||
case '\r':
|
||||
e.WriteRune('r')
|
||||
case '\t':
|
||||
e.WriteRune('t')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \t, \n and \r.
|
||||
// If escapeHTML is set, it also escapes <, >, and &
|
||||
// because they can lead to security holes when
|
||||
// user-controlled strings are rendered into JSON
|
||||
// and served to some browsers.
|
||||
e.WriteString(`u00`)
|
||||
e.WriteByte(hex[b>>4])
|
||||
e.WriteByte(hex[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRune(s[i:])
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteString(`\ufffd`)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// U+2028 is LINE SEPARATOR.
|
||||
// U+2029 is PARAGRAPH SEPARATOR.
|
||||
// They are both technically valid characters in JSON strings,
|
||||
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||
// and can lead to security holes there. It is valid JSON to
|
||||
// escape them, so we do so unconditionally.
|
||||
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
if start < i {
|
||||
e.Write(s[start:i])
|
||||
}
|
||||
e.WriteString(`\u202`)
|
||||
e.WriteByte(hex[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
e.Write(s[start:])
|
||||
}
|
||||
e.WriteByte('"')
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (n NaturalLanguageValues) MarshalJSON() ([]byte, error) {
|
||||
l := len(n)
|
||||
if l <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b := bytes.Buffer{}
|
||||
if l == 1 {
|
||||
v := n[0]
|
||||
if len(v.Value) > 0 {
|
||||
v.Value = unescape(v.Value)
|
||||
stringBytes(&b, v.Value, false)
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
}
|
||||
b.Write([]byte{'{'})
|
||||
empty := true
|
||||
for _, val := range n {
|
||||
if len(val.Ref) == 0 || len(val.Value) == 0 {
|
||||
continue
|
||||
}
|
||||
if !empty {
|
||||
b.Write([]byte{','})
|
||||
}
|
||||
if v, err := val.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
l, err := b.Write(v)
|
||||
if err == nil && l > 0 {
|
||||
empty = false
|
||||
}
|
||||
}
|
||||
}
|
||||
b.Write([]byte{'}'})
|
||||
if !empty {
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// First returns the first element in the array
|
||||
func (n NaturalLanguageValues) First() LangRefValue {
|
||||
for _, v := range n {
|
||||
return v
|
||||
}
|
||||
return LangRefValue{}
|
||||
}
|
||||
|
||||
// MarshalText serializes the NaturalLanguageValues into Text
|
||||
func (n NaturalLanguageValues) MarshalText() ([]byte, error) {
|
||||
for _, v := range n {
|
||||
return []byte(fmt.Sprintf("%q", v)), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
_, _ = io.WriteString(s, "[")
|
||||
for _, nn := range n {
|
||||
nn.Format(s, verb)
|
||||
}
|
||||
_, _ = io.WriteString(s, "]")
|
||||
case 'v':
|
||||
_, _ = io.WriteString(s, "[")
|
||||
for _, nn := range n {
|
||||
nn.Format(s, verb)
|
||||
}
|
||||
_, _ = io.WriteString(s, "]")
|
||||
}
|
||||
}
|
||||
|
||||
// Append is syntactic sugar for resizing the NaturalLanguageValues map
|
||||
// and appending an element
|
||||
func (n *NaturalLanguageValues) Append(lang LangRef, value Content) error {
|
||||
*n = append(*n, LangRefValue{lang, value})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Count returns the length of Items in the item collection
|
||||
func (n *NaturalLanguageValues) Count() uint {
|
||||
if n == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(*n))
|
||||
}
|
||||
|
||||
// String adds support for Stringer interface. It returns the Value[LangRef] text or just Value if LangRef is NIL
|
||||
func (l LangRefValue) String() string {
|
||||
if l.Ref == NilLangRef {
|
||||
return l.Value.String()
|
||||
}
|
||||
return fmt.Sprintf("%s[%s]", l.Value, l.Ref)
|
||||
}
|
||||
|
||||
func DefaultLangRef(value string) LangRefValue {
|
||||
return LangRefValue{Ref: DefaultLang, Value: Content(value)}
|
||||
}
|
||||
|
||||
func LangRefValueNew(lang LangRef, value string) LangRefValue {
|
||||
return LangRefValue{Ref: lang, Value: Content(value)}
|
||||
}
|
||||
|
||||
func (l LangRefValue) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
if l.Ref == NilLangRef {
|
||||
_, _ = io.WriteString(s, string(l.Value))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%q[%s]", l.Value, l.Ref)
|
||||
}
|
||||
case 'v':
|
||||
if l.Ref == NilLangRef {
|
||||
_, _ = fmt.Fprintf(s, "%q", string(l.Value))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%q[%s]", string(l.Value), l.Ref)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (l *LangRefValue) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(data)
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeObject:
|
||||
o, _ := val.Object()
|
||||
o.Visit(func(key []byte, v *fastjson.Value) {
|
||||
l.Ref = LangRef(key)
|
||||
l.Value = unescape(v.GetStringBytes())
|
||||
})
|
||||
case fastjson.TypeString:
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(val.GetStringBytes())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements the TextEncoder interface
|
||||
func (l *LangRefValue) UnmarshalText(data []byte) error {
|
||||
l.Ref = NilLangRef
|
||||
l.Value = unescape(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LangRef) GobEncode() ([]byte, error) {
|
||||
if len(l) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
if err := gobEncodeStringLikeType(gg, []byte(l)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (l *LangRef) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
var bb []byte
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&bb); err != nil {
|
||||
return err
|
||||
}
|
||||
*l = LangRef(bb)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (l LangRefValue) MarshalJSON() ([]byte, error) {
|
||||
buf := bytes.Buffer{}
|
||||
if l.Ref != NilLangRef && len(l.Ref) > 0 {
|
||||
if l.Value.Equals(Content("")) {
|
||||
return nil, nil
|
||||
}
|
||||
stringBytes(&buf, []byte(l.Ref), false)
|
||||
buf.Write([]byte{':'})
|
||||
}
|
||||
stringBytes(&buf, l.Value, false)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarshalText serializes the LangRefValue into JSON
|
||||
func (l LangRefValue) MarshalText() ([]byte, error) {
|
||||
if l.Ref != NilLangRef && l.Value.Equals(Content("")) {
|
||||
return nil, nil
|
||||
}
|
||||
buf := bytes.Buffer{}
|
||||
buf.WriteString(l.Value.String())
|
||||
if l.Ref != NilLangRef {
|
||||
buf.WriteByte('[')
|
||||
buf.WriteString(l.Ref.String())
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
K []byte
|
||||
V []byte
|
||||
}
|
||||
|
||||
func (l LangRefValue) GobEncode() ([]byte, error) {
|
||||
if len(l.Value) == 0 && len(l.Ref) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
mm := kv{
|
||||
K: []byte(l.Ref),
|
||||
V: []byte(l.Value),
|
||||
}
|
||||
if err := gg.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (l *LangRefValue) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
mm := kv{}
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&mm); err != nil {
|
||||
return err
|
||||
}
|
||||
l.Ref = LangRef(mm.K)
|
||||
l.Value = mm.V
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (l *LangRef) UnmarshalJSON(data []byte) error {
|
||||
return l.UnmarshalText(data)
|
||||
}
|
||||
|
||||
// UnmarshalText implements the TextEncoder interface
|
||||
func (l *LangRef) UnmarshalText(data []byte) error {
|
||||
*l = ""
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) > 2 {
|
||||
if data[0] == '"' && data[len(data)-1] == '"' {
|
||||
*l = LangRef(data[1 : len(data)-1])
|
||||
}
|
||||
} else {
|
||||
*l = LangRef(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l LangRef) String() string {
|
||||
return string(l)
|
||||
}
|
||||
|
||||
func (l LangRefValue) Equals(other LangRefValue) bool {
|
||||
return l.Ref == other.Ref && l.Value.Equals(other.Value)
|
||||
}
|
||||
|
||||
func (c *Content) UnmarshalJSON(data []byte) error {
|
||||
return c.UnmarshalText(data)
|
||||
}
|
||||
|
||||
func (c *Content) UnmarshalText(data []byte) error {
|
||||
*c = Content{}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) > 2 {
|
||||
if data[0] == '"' && data[len(data)-1] == '"' {
|
||||
*c = Content(data[1 : len(data)-1])
|
||||
}
|
||||
} else {
|
||||
*c = Content(data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Content) GobEncode() ([]byte, error) {
|
||||
if len(c) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
if err := gobEncodeStringLikeType(gg, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Content) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
bb := make([]byte, 0)
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&bb); err != nil {
|
||||
return err
|
||||
}
|
||||
*c = bb
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Content) String() string {
|
||||
return string(c)
|
||||
}
|
||||
|
||||
func (c Content) Equals(other Content) bool {
|
||||
return bytes.Equal(c, other)
|
||||
}
|
||||
|
||||
func (c Content) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'q':
|
||||
_, _ = io.WriteString(s, string(c))
|
||||
case 'v':
|
||||
_, _ = fmt.Fprintf(s, "%q", string(c))
|
||||
}
|
||||
}
|
||||
|
||||
func unescape(b []byte) []byte {
|
||||
// FIXME(marius): I feel like I'm missing something really obvious about encoding/decoding from Json regarding
|
||||
// escape characters, and that this function is just a hack. Be better future Marius, find the real problem!
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'a'}, []byte{'\a'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'f'}, []byte{'\f'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'n'}, []byte{'\n'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'r'}, []byte{'\r'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 't'}, []byte{'\t'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', 'v'}, []byte{'\v'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', '"'}, []byte{'"'})
|
||||
b = bytes.ReplaceAll(b, []byte{'\\', '\\'}, []byte{'\\'}) // this should cover the case of \\u -> \u
|
||||
return b
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (n *NaturalLanguageValues) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
// try our luck if data contains an unquoted string
|
||||
n.Append(NilLangRef, unescape(data))
|
||||
return nil
|
||||
}
|
||||
switch val.Type() {
|
||||
case fastjson.TypeObject:
|
||||
ob, _ := val.Object()
|
||||
ob.Visit(func(key []byte, v *fastjson.Value) {
|
||||
if dat := v.GetStringBytes(); len(dat) > 0 {
|
||||
n.Append(LangRef(key), unescape(dat))
|
||||
}
|
||||
})
|
||||
case fastjson.TypeString:
|
||||
if dat := val.GetStringBytes(); len(dat) > 0 {
|
||||
n.Append(NilLangRef, unescape(dat))
|
||||
}
|
||||
case fastjson.TypeArray:
|
||||
for _, v := range val.GetArray() {
|
||||
l := LangRefValue{}
|
||||
l.UnmarshalJSON([]byte(v.String()))
|
||||
if len(l.Value) > 0 {
|
||||
n.Append(l.Ref, l.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalText tries to load the NaturalLanguage array from the incoming Text value
|
||||
func (n *NaturalLanguageValues) UnmarshalText(data []byte) error {
|
||||
if data[0] == '"' {
|
||||
// a quoted string - loading it to c.URL
|
||||
if data[len(data)-1] != '"' {
|
||||
return fmt.Errorf("invalid string value when unmarshaling %T value", n)
|
||||
}
|
||||
n.Append(LangRef(NilLangRef), Content(data[1:len(data)-1]))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n NaturalLanguageValues) GobEncode() ([]byte, error) {
|
||||
if len(n) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
gg := gob.NewEncoder(b)
|
||||
mm := make([]kv, len(n))
|
||||
for i, l := range n {
|
||||
mm[i] = kv{K: []byte(l.Ref), V: l.Value}
|
||||
}
|
||||
if err := gg.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func (n *NaturalLanguageValues) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
mm := make([]kv, 0)
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&mm); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range mm {
|
||||
*n = append(*n, LangRefValue{Ref: LangRef(m.K), Value: m.V})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (n NaturalLanguageValues) Equals(with NaturalLanguageValues) bool {
|
||||
if n.Count() != with.Count() {
|
||||
return false
|
||||
}
|
||||
for _, wv := range with {
|
||||
for _, nv := range n {
|
||||
if nv.Equals(wv) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,999 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
const (
|
||||
IRIType ActivityVocabularyType = "IRI"
|
||||
ObjectType ActivityVocabularyType = "Object"
|
||||
LinkType ActivityVocabularyType = "Link"
|
||||
ActivityType ActivityVocabularyType = "Activity"
|
||||
IntransitiveActivityType ActivityVocabularyType = "IntransitiveActivity"
|
||||
ActorType ActivityVocabularyType = "Actor"
|
||||
CollectionType ActivityVocabularyType = "Collection"
|
||||
OrderedCollectionType ActivityVocabularyType = "OrderedCollection"
|
||||
CollectionPageType ActivityVocabularyType = "CollectionPage"
|
||||
OrderedCollectionPageType ActivityVocabularyType = "OrderedCollectionPage"
|
||||
|
||||
// ActivityPub Object Types
|
||||
ArticleType ActivityVocabularyType = "Article"
|
||||
AudioType ActivityVocabularyType = "Audio"
|
||||
DocumentType ActivityVocabularyType = "Document"
|
||||
EventType ActivityVocabularyType = "Event"
|
||||
ImageType ActivityVocabularyType = "Image"
|
||||
NoteType ActivityVocabularyType = "Note"
|
||||
PageType ActivityVocabularyType = "Page"
|
||||
PlaceType ActivityVocabularyType = "Place"
|
||||
ProfileType ActivityVocabularyType = "Profile"
|
||||
RelationshipType ActivityVocabularyType = "Relationship"
|
||||
TombstoneType ActivityVocabularyType = "Tombstone"
|
||||
VideoType ActivityVocabularyType = "Video"
|
||||
|
||||
// MentionType is a link type for @mentions
|
||||
MentionType ActivityVocabularyType = "Mention"
|
||||
)
|
||||
|
||||
var GenericTypes = ActivityVocabularyTypes{
|
||||
ActivityType,
|
||||
IntransitiveActivityType,
|
||||
ObjectType,
|
||||
ActorType,
|
||||
}
|
||||
|
||||
var ObjectTypes = ActivityVocabularyTypes{
|
||||
ArticleType,
|
||||
AudioType,
|
||||
DocumentType,
|
||||
EventType,
|
||||
ImageType,
|
||||
NoteType,
|
||||
PageType,
|
||||
PlaceType,
|
||||
ProfileType,
|
||||
RelationshipType,
|
||||
TombstoneType,
|
||||
VideoType,
|
||||
}
|
||||
|
||||
type (
|
||||
// ActivityVocabularyType is the data type for an Activity type object
|
||||
ActivityVocabularyType string
|
||||
// ActivityObject is a subtype of Object that describes some form of action that may happen,
|
||||
// is currently happening, or has already happened
|
||||
ActivityObject interface {
|
||||
// GetID returns the dereferenceable ActivityStreams object id
|
||||
GetID() ID
|
||||
// GetType returns the ActivityStreams type
|
||||
GetType() ActivityVocabularyType
|
||||
}
|
||||
// LinkOrIRI is an interface that Object and Link structs implement, and at the same time
|
||||
// they are kept disjointed
|
||||
LinkOrIRI interface {
|
||||
// GetLink returns the object id in IRI type
|
||||
GetLink() IRI
|
||||
}
|
||||
// ObjectOrLink describes the requirements of an ActivityStreams object
|
||||
ObjectOrLink interface {
|
||||
ActivityObject
|
||||
LinkOrIRI
|
||||
// IsLink shows if current item represents a Link object or an IRI
|
||||
IsLink() bool
|
||||
// IsObject shows if current item represents an ActivityStreams object
|
||||
IsObject() bool
|
||||
// IsCollection shows if the current item represents an ItemCollection
|
||||
IsCollection() bool
|
||||
}
|
||||
// Mapper interface allows external objects to implement their own mechanism for loading information
|
||||
// from an ActivityStreams vocabulary object
|
||||
Mapper interface {
|
||||
// FromActivityStreams maps an ActivityStreams object to another struct representation
|
||||
FromActivityStreams(Item) error
|
||||
}
|
||||
|
||||
// MimeType is the type for representing MIME types in certain ActivityStreams properties
|
||||
MimeType string
|
||||
)
|
||||
|
||||
func (a ActivityVocabularyType) MarshalJSON() ([]byte, error) {
|
||||
if len(a) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
b := make([]byte, 0)
|
||||
JSONWriteStringValue(&b, string(a))
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (a ActivityVocabularyType) GobEncode() ([]byte, error) {
|
||||
return []byte(a), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (a *ActivityVocabularyType) GobDecode(data []byte) error {
|
||||
*a = ActivityVocabularyType(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (a *ActivityVocabularyType) UnmarshalBinary(data []byte) error {
|
||||
return a.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (a ActivityVocabularyType) MarshalBinary() ([]byte, error) {
|
||||
return a.GobEncode()
|
||||
}
|
||||
|
||||
type Objects interface {
|
||||
Object | Tombstone | Place | Profile | Relationship |
|
||||
Actors |
|
||||
Activities |
|
||||
IntransitiveActivities |
|
||||
Collections |
|
||||
IRI
|
||||
}
|
||||
|
||||
// Object describes an ActivityPub object of any kind.
|
||||
// It serves as the base type for most of the other kinds of objects defined in the Activity
|
||||
// Vocabulary, including other Core types such as Activity, IntransitiveActivity, Collection and OrderedCollection.
|
||||
type Object struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID 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"`
|
||||
}
|
||||
|
||||
// ObjectNew initializes a new Object
|
||||
func ObjectNew(typ ActivityVocabularyType) *Object {
|
||||
if !(ObjectTypes.Contains(typ)) {
|
||||
typ = ObjectType
|
||||
}
|
||||
o := Object{Type: typ}
|
||||
o.Name = NaturalLanguageValuesNew()
|
||||
o.Content = NaturalLanguageValuesNew()
|
||||
return &o
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Object
|
||||
func (o Object) GetID() ID {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Object
|
||||
func (o Object) GetLink() IRI {
|
||||
return IRI(o.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Object
|
||||
func (o Object) GetType() ActivityVocabularyType {
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// IsLink validates if currentActivity Pub Object is a Link
|
||||
func (o Object) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject validates if currentActivity Pub Object is an Object
|
||||
func (o Object) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Object objects
|
||||
func (o Object) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (o *Object) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadObject(val, o)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (o Object) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if JSONWriteObjectValue(&b, o) {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (o *Object) UnmarshalBinary(data []byte) error {
|
||||
return o.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (o Object) MarshalBinary() ([]byte, error) {
|
||||
return o.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (o Object) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapObjectProperties(mm, &o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (o *Object) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapObjectProperties(mm, o)
|
||||
}
|
||||
|
||||
func fmtObjectProps(w io.Writer) func(*Object) error {
|
||||
return func(o *Object) error {
|
||||
if len(o.ID) > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "ID:%s", o.ID); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if len(o.Name) > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: [%s]", "name", o.Name); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if len(o.Summary) > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: [%s]", "summary", o.Summary); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if len(o.Content) > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: [%s]", "content", o.Content); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Attachment) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "attachment", o.Attachment); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.AttributedTo) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "attributedTo", o.AttributedTo); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Audience) && o.Audience.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "audience", o.Audience); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Context) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "context", o.Context); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Generator) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "generator", o.Generator); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Icon) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "icon", o.Icon); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Image) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "image", o.Image); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.InReplyTo) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "inReplyTo", o.InReplyTo); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Location) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "location", o.Location); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Preview) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "preview", o.Preview); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Replies) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "replies", o.Replies); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Tag) && o.Tag.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "tag", o.Tag); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.URL) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "url", o.URL); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.To) && o.To.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "to", o.To); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Bto) && o.Bto.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "bto", o.Bto); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.CC) && o.CC.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "cc", o.CC); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.BCC) && o.BCC.Count() > 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "bcc", o.BCC); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !o.Published.IsZero() {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "published", o.Published); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !o.Updated.IsZero() {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "updated", o.Updated); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !o.StartTime.IsZero() {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "startTime", o.StartTime); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !o.EndTime.IsZero() {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "endTime", o.EndTime); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if o.Duration != 0 {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "duration", o.Duration); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Likes) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "likes", o.Likes); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
if !IsNil(o.Shares) {
|
||||
if n, _ := fmt.Fprintf(w, "%s: %s", "shares", o.Shares); n > 0 {
|
||||
_, _ = io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o Object) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
if o.Type != "" && o.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T[%s]( %s )", o, o.Type, o.ID)
|
||||
} else if o.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T( %s )", o, o.ID)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(s, "%T[%p]", o, &o)
|
||||
}
|
||||
case 'v':
|
||||
if o.Type != "" && o.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] {", o, o.Type)
|
||||
_ = fmtObjectProps(s)(&o)
|
||||
_, _ = io.WriteString(s, " }")
|
||||
} else if o.ID != "" {
|
||||
_, _ = fmt.Fprintf(s, "%T { ", o)
|
||||
_ = fmtObjectProps(s)(&o)
|
||||
_, _ = io.WriteString(s, " }")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Object's To, Bto, CC and BCC properties
|
||||
func (o *Object) Recipients() ItemCollection {
|
||||
var aud ItemCollection
|
||||
return ItemCollectionDeduplication(&aud, &o.To, &o.Bto, &o.CC, &o.BCC, &o.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (o *Object) Clean() {
|
||||
o.BCC = o.BCC[:0]
|
||||
o.Bto = o.Bto[:0]
|
||||
CleanRecipients(o.Audience)
|
||||
CleanRecipients(o.Attachment)
|
||||
CleanRecipients(o.Icon)
|
||||
CleanRecipients(o.Image)
|
||||
CleanRecipients(o.Context)
|
||||
CleanRecipients(o.Generator)
|
||||
CleanRecipients(o.AttributedTo)
|
||||
CleanRecipients(o.Preview)
|
||||
CleanRecipients(o.Tag)
|
||||
}
|
||||
|
||||
type (
|
||||
// Article represents any kind of multi-paragraph written work.
|
||||
Article = Object
|
||||
// Audio represents an audio document of any kind.
|
||||
Audio = Document
|
||||
// Document represents a document of any kind.
|
||||
Document = Object
|
||||
// Event represents any kind of event.
|
||||
Event = Object
|
||||
// Image An image document of any kind
|
||||
Image = Document
|
||||
// Note represents a short written work typically less than a single paragraph in length.
|
||||
Note = Object
|
||||
// Page represents a Web Page.
|
||||
Page = Document
|
||||
// Video represents a video document of any kind
|
||||
Video = Document
|
||||
)
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (m *MimeType) UnmarshalJSON(data []byte) error {
|
||||
*m = MimeType(strings.Trim(string(data), "\""))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (m MimeType) MarshalJSON() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
b := make([]byte, 0)
|
||||
JSONWriteStringValue(&b, string(m))
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (m MimeType) GobEncode() ([]byte, error) {
|
||||
if len(m) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
b := bytes.Buffer{}
|
||||
gg := gob.NewEncoder(&b)
|
||||
if err := gobEncodeStringLikeType(gg, []byte(m)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (m *MimeType) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
// NOTE(marius): this behaviour diverges from vanilla gob package
|
||||
return nil
|
||||
}
|
||||
var bb []byte
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&bb); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = MimeType(bb)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (m *MimeType) UnmarshalBinary(data []byte) error {
|
||||
return m.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (m MimeType) MarshalBinary() ([]byte, error) {
|
||||
return m.GobEncode()
|
||||
}
|
||||
|
||||
// ToLink returns a Link pointer to the data in the current Item
|
||||
func ToLink(it LinkOrIRI) (*Link, error) {
|
||||
switch i := it.(type) {
|
||||
case *Link:
|
||||
return i, nil
|
||||
case Link:
|
||||
return &i, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unable to convert %T to %T", it, new(Link))
|
||||
}
|
||||
|
||||
// ToObject returns an Object pointer to the data in the current Item
|
||||
// It relies on the fact that all the types in this package have a data layout compatible with Object.
|
||||
func ToObject(it Item) (*Object, error) {
|
||||
switch i := it.(type) {
|
||||
case *Object:
|
||||
return i, nil
|
||||
case Object:
|
||||
return &i, nil
|
||||
case *Place:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Place:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Profile:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Profile:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Relationship:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Relationship:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Tombstone:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Tombstone:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Actor:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Actor:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Activity:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Activity:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *IntransitiveActivity:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case IntransitiveActivity:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Question:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Question:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *Collection:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case Collection:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *CollectionPage:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case CollectionPage:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *OrderedCollection:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollection:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
case *OrderedCollectionPage:
|
||||
return (*Object)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollectionPage:
|
||||
return (*Object)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Object))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if reflect.ValueOf(it).IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Object); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Object](it)
|
||||
}
|
||||
|
||||
// Source is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
type Source struct {
|
||||
// Content
|
||||
Content NaturalLanguageValues `jsonld:"content"`
|
||||
// MediaType
|
||||
MediaType MimeType `jsonld:"mediaType"`
|
||||
}
|
||||
|
||||
// GetAPSource
|
||||
func GetAPSource(val *fastjson.Value) Source {
|
||||
s := Source{}
|
||||
if val == nil {
|
||||
return s
|
||||
}
|
||||
|
||||
if contBytes := val.Get("source", "content").GetStringBytes(); len(contBytes) > 0 {
|
||||
s.Content.UnmarshalJSON(contBytes)
|
||||
}
|
||||
if mimeBytes := val.Get("source", "mediaType").GetStringBytes(); len(mimeBytes) > 0 {
|
||||
s.MediaType.UnmarshalJSON(mimeBytes)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (s *Source) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = GetAPSource(val)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (s Source) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
empty := true
|
||||
JSONWrite(&b, '{')
|
||||
if len(s.MediaType) > 0 {
|
||||
if v, err := s.MediaType.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
empty = !JSONWriteProp(&b, "mediaType", v)
|
||||
}
|
||||
}
|
||||
if len(s.Content) > 0 {
|
||||
empty = !JSONWriteNaturalLanguageProp(&b, "content", s.Content)
|
||||
}
|
||||
if !empty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (s *Source) UnmarshalBinary(data []byte) error {
|
||||
return s.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (s Source) MarshalBinary() ([]byte, error) {
|
||||
return s.GobEncode()
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (s *Source) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm := make(map[string][]byte)
|
||||
g := gob.NewDecoder(bytes.NewReader(data))
|
||||
if err := g.Decode(&mm); err != nil {
|
||||
return err
|
||||
}
|
||||
if raw, ok := mm["mediaType"]; ok {
|
||||
if err := s.MediaType.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if raw, ok := mm["content"]; ok {
|
||||
if err := s.Content.GobDecode(raw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (s Source) GobEncode() ([]byte, error) {
|
||||
var (
|
||||
mm = make(map[string][]byte)
|
||||
err error
|
||||
hasData bool
|
||||
)
|
||||
if len(s.MediaType) > 0 {
|
||||
if mm["mediaType"], err = s.MediaType.GobEncode(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasData = true
|
||||
}
|
||||
if len(s.Content) > 0 {
|
||||
if mm["content"], err = s.Content.GobEncode(); 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
|
||||
}
|
||||
|
||||
// Equals verifies if our receiver Object is equals with the "with" Object
|
||||
func (o Object) Equals(with Item) bool {
|
||||
if IsItemCollection(with) {
|
||||
return false
|
||||
}
|
||||
if withID := with.GetID(); !o.ID.Equals(withID, true) {
|
||||
return false
|
||||
}
|
||||
if withType := with.GetType(); !strings.EqualFold(string(o.Type), string(withType)) {
|
||||
return false
|
||||
}
|
||||
if with.IsLink() && !with.GetLink().Equals(o.GetLink(), false) {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
err := OnObject(with, func(w *Object) error {
|
||||
if len(w.Name) > 0 {
|
||||
if !w.Name.Equals(o.Name) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(w.Summary) > 0 {
|
||||
if !w.Summary.Equals(o.Summary) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if len(w.Content) > 0 {
|
||||
if !w.Content.Equals(o.Content) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Attachment != nil {
|
||||
if !ItemsEqual(o.Attachment, w.Attachment) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.AttributedTo != nil {
|
||||
if !ItemsEqual(o.AttributedTo, w.AttributedTo) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Audience != nil {
|
||||
if !ItemsEqual(o.Audience, w.Audience) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Context != nil {
|
||||
if !ItemsEqual(o.Context, w.Context) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Generator != nil {
|
||||
if !ItemsEqual(o.Generator, w.Generator) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Icon != nil {
|
||||
if !ItemsEqual(o.Icon, w.Icon) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Image != nil {
|
||||
if !ItemsEqual(o.Image, w.Image) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.InReplyTo != nil {
|
||||
if !ItemsEqual(o.InReplyTo, w.InReplyTo) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Location != nil {
|
||||
if !ItemsEqual(o.Location, w.Location) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Preview != nil {
|
||||
if !ItemsEqual(o.Preview, w.Preview) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Replies != nil {
|
||||
if !ItemsEqual(o.Replies, w.Replies) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Tag != nil {
|
||||
if !ItemsEqual(o.Tag, w.Tag) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.URL != nil {
|
||||
if o.URL == nil {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
if !w.URL.GetLink().Equals(o.URL.GetLink(), false) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.To != nil {
|
||||
if !ItemsEqual(o.To, w.To) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Bto != nil {
|
||||
if !ItemsEqual(o.Bto, w.Bto) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.CC != nil {
|
||||
if !ItemsEqual(o.CC, w.CC) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.BCC != nil {
|
||||
if !ItemsEqual(o.BCC, w.BCC) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !w.Published.IsZero() {
|
||||
if !w.Published.Equal(o.Published) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !w.Updated.IsZero() {
|
||||
if !w.Updated.Equal(o.Updated) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !w.StartTime.IsZero() {
|
||||
if !w.StartTime.Equal(o.StartTime) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !w.EndTime.IsZero() {
|
||||
if !w.EndTime.Equal(o.EndTime) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Duration != 0 {
|
||||
if w.Duration != o.Duration {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Likes != nil {
|
||||
if !ItemsEqual(o.Likes, w.Likes) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Shares != nil {
|
||||
if !ItemsEqual(o.Shares, w.Shares) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
result = false
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package activitypub
|
||||
|
||||
// ID designates a unique global identifier.
|
||||
// All Objects in [ActivityStreams] should have unique global identifiers.
|
||||
// ActivityPub extends this requirement; all objects distributed by the ActivityPub protocol MUST
|
||||
// have unique global identifiers, unless they are intentionally transient
|
||||
// (short-lived activities that are not intended to be able to be looked up,
|
||||
// such as some kinds of chat messages or game notifications).
|
||||
// These identifiers must fall into one of the following groups:
|
||||
//
|
||||
// 1. Publicly de-referenceable URIs, such as HTTPS URIs, with their authority belonging
|
||||
// to that of their originating server. (Publicly facing content SHOULD use HTTPS URIs).
|
||||
// 2. An ID explicitly specified as the JSON null object, which implies an anonymous object
|
||||
// (a part of its parent context)
|
||||
type ID = IRI
|
||||
|
||||
// IsValid returns if the receiver pointer is not nil and if dereferenced it has a positive length.
|
||||
func (i *ID) IsValid() bool {
|
||||
return i != nil && len(*i) > 0
|
||||
}
|
|
@ -0,0 +1,427 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// OrderedCollection is a subtype of Collection in which members of the logical
|
||||
// collection are assumed to always be strictly ordered.
|
||||
type OrderedCollection struct {
|
||||
// ID provides the globally unique identifier for an Activity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be ordered or unordered.
|
||||
OrderedItems ItemCollection `jsonld:"orderedItems,omitempty"`
|
||||
}
|
||||
|
||||
type (
|
||||
// InboxStream contains all activities received by the actor.
|
||||
// The server SHOULD filter content according to the requester's permission.
|
||||
// In general, the owner of an inbox is likely to be able to access all of their inbox contents.
|
||||
// Depending on access control, some other content may be public, whereas other content may
|
||||
// require authentication for non-owner users, if they can access the inbox at all.
|
||||
InboxStream = OrderedCollection
|
||||
|
||||
// LikedCollection is a list of every object from all of the actor's Like activities,
|
||||
// added as a side effect. The liked collection MUST be either an OrderedCollection or
|
||||
// a Collection and MAY be filtered on privileges of an authenticated user or as
|
||||
// appropriate when no authentication is given.
|
||||
LikedCollection = OrderedCollection
|
||||
|
||||
// LikesCollection is a list of all Like activities with this object as the object property,
|
||||
// added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when
|
||||
// no authentication is given.
|
||||
LikesCollection = OrderedCollection
|
||||
|
||||
// OutboxStream contains activities the user has published,
|
||||
// subject to the ability of the requestor to retrieve the activity (that is,
|
||||
// the contents of the outbox are filtered by the permissions of the person reading it).
|
||||
OutboxStream = OrderedCollection
|
||||
|
||||
// SharesCollection is a list of all Announce activities with this object as the object property,
|
||||
// added as a side effect. The shares collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication
|
||||
// is given.
|
||||
SharesCollection = OrderedCollection
|
||||
)
|
||||
|
||||
// GetType returns the OrderedCollection's type
|
||||
func (o OrderedCollection) GetType() ActivityVocabularyType {
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for an OrderedCollection object
|
||||
func (o OrderedCollection) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the OrderedCollection
|
||||
func (o OrderedCollection) GetID() ID {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the OrderedCollection object
|
||||
func (o OrderedCollection) GetLink() IRI {
|
||||
return IRI(o.ID)
|
||||
}
|
||||
|
||||
// IsObject returns true for am OrderedCollection object
|
||||
func (o OrderedCollection) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Collection returns the underlying Collection type
|
||||
func (o OrderedCollection) Collection() ItemCollection {
|
||||
return o.OrderedItems
|
||||
}
|
||||
|
||||
// IsCollection returns true for OrderedCollection objects.
|
||||
func (o OrderedCollection) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Contains verifies if OrderedCollection array contains the received item r.
|
||||
func (o OrderedCollection) Contains(r Item) bool {
|
||||
if len(o.OrderedItems) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range o.OrderedItems {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in collection and its TotalItems property
|
||||
func (o *OrderedCollection) Count() uint {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(o.OrderedItems))
|
||||
}
|
||||
|
||||
// Append adds an element to an the receiver collection object.
|
||||
func (o *OrderedCollection) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if o.OrderedItems.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
o.OrderedItems = append(o.OrderedItems, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (o *OrderedCollection) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadOrderedCollection(val, o)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (o OrderedCollection) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(o, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if o.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", o.Current) || notEmpty
|
||||
}
|
||||
if o.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", o.First) || notEmpty
|
||||
}
|
||||
if o.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", o.Last) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty
|
||||
if o.OrderedItems != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "orderedItems", o.OrderedItems, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (o *OrderedCollection) UnmarshalBinary(data []byte) error {
|
||||
return o.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (o OrderedCollection) MarshalBinary() ([]byte, error) {
|
||||
return o.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (o OrderedCollection) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapOrderedCollectionProperties(mm, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (o *OrderedCollection) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapOrderedCollectionProperties(mm, o)
|
||||
}
|
||||
|
||||
// OrderedCollectionPageNew initializes a new OrderedCollectionPage
|
||||
func OrderedCollectionPageNew(parent CollectionInterface) *OrderedCollectionPage {
|
||||
p := OrderedCollectionPage{
|
||||
PartOf: parent.GetLink(),
|
||||
}
|
||||
if pc, ok := parent.(*OrderedCollection); ok {
|
||||
copyOrderedCollectionToPage(pc, &p)
|
||||
}
|
||||
p.Type = OrderedCollectionPageType
|
||||
return &p
|
||||
}
|
||||
|
||||
// ToOrderedCollection
|
||||
func ToOrderedCollection(it Item) (*OrderedCollection, error) {
|
||||
switch i := it.(type) {
|
||||
case *OrderedCollection:
|
||||
return i, nil
|
||||
case OrderedCollection:
|
||||
return &i, nil
|
||||
case *OrderedCollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(i)), nil
|
||||
case OrderedCollectionPage:
|
||||
return (*OrderedCollection)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(OrderedCollection))
|
||||
val := reflect.ValueOf(it)
|
||||
if val.IsValid() && typ.Elem().Name() == val.Type().Elem().Name() {
|
||||
conv := val.Convert(typ)
|
||||
if i, ok := conv.Interface().(*OrderedCollection); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[OrderedCollection](it)
|
||||
}
|
||||
|
||||
func copyOrderedCollectionToPage(c *OrderedCollection, p *OrderedCollectionPage) error {
|
||||
p.Type = OrderedCollectionPageType
|
||||
p.Name = c.Name
|
||||
p.Content = c.Content
|
||||
p.Summary = c.Summary
|
||||
p.Context = c.Context
|
||||
p.URL = c.URL
|
||||
p.MediaType = c.MediaType
|
||||
p.Generator = c.Generator
|
||||
p.AttributedTo = c.AttributedTo
|
||||
p.Attachment = c.Attachment
|
||||
p.Location = c.Location
|
||||
p.Published = c.Published
|
||||
p.StartTime = c.StartTime
|
||||
p.EndTime = c.EndTime
|
||||
p.Duration = c.Duration
|
||||
p.Icon = c.Icon
|
||||
p.Preview = c.Preview
|
||||
p.Image = c.Image
|
||||
p.Updated = c.Updated
|
||||
p.InReplyTo = c.InReplyTo
|
||||
p.To = c.To
|
||||
p.Audience = c.Audience
|
||||
p.Bto = c.Bto
|
||||
p.CC = c.CC
|
||||
p.BCC = c.BCC
|
||||
p.Replies = c.Replies
|
||||
p.Tag = c.Tag
|
||||
p.TotalItems = c.TotalItems
|
||||
p.OrderedItems = c.OrderedItems
|
||||
p.Current = c.Current
|
||||
p.First = c.First
|
||||
p.PartOf = c.GetLink()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (o OrderedCollection) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := o.OrderedItems.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (o OrderedCollection) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
OnOrderedCollection(with, func(w *OrderedCollection) error {
|
||||
OnCollection(w, func(wo *Collection) error {
|
||||
if !wo.Equals(o) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.OrderedItems != nil {
|
||||
if !o.OrderedItems.Equals(w.OrderedItems) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (o OrderedCollection) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", o, o.Type, o.TotalItems)
|
||||
}
|
||||
}
|
||||
func (o *OrderedCollection) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&o.To, &o.Bto, &o.CC, &o.BCC, &o.Audience)
|
||||
}
|
||||
|
||||
func (o *OrderedCollection) Clean() {
|
||||
_ = OnObject(o, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,393 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// OrderedCollectionPage type extends from both CollectionPage and OrderedCollection.
|
||||
// In addition to the properties inherited from each of those, the OrderedCollectionPage
|
||||
// may contain an additional startIndex property whose value indicates the relative index position
|
||||
// of the first item contained by the page within the OrderedCollection to which the page belongs.
|
||||
type OrderedCollectionPage struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// In a paged Collection, indicates the page that contains the most recently updated member items.
|
||||
Current ObjectOrLink `jsonld:"current,omitempty"`
|
||||
// In a paged Collection, indicates the furthest preceding page of items in the collection.
|
||||
First ObjectOrLink `jsonld:"first,omitempty"`
|
||||
// In a paged Collection, indicates the furthest proceeding page of the collection.
|
||||
Last ObjectOrLink `jsonld:"last,omitempty"`
|
||||
// A non-negative integer specifying the total number of objects contained by the logical view of the collection.
|
||||
// This number might not reflect the actual number of items serialized within the Collection object instance.
|
||||
TotalItems uint `jsonld:"totalItems"`
|
||||
// Identifies the items contained in a collection. The items might be ordered or unordered.
|
||||
OrderedItems ItemCollection `jsonld:"orderedItems,omitempty"`
|
||||
// Identifies the Collection to which a CollectionPage objects items belong.
|
||||
PartOf Item `jsonld:"partOf,omitempty"`
|
||||
// In a paged Collection, indicates the next page of items.
|
||||
Next Item `jsonld:"next,omitempty"`
|
||||
// In a paged Collection, identifies the previous page of items.
|
||||
Prev Item `jsonld:"prev,omitempty"`
|
||||
// A non-negative integer value identifying the relative position within the logical view of a strictly ordered collection.
|
||||
StartIndex uint `jsonld:"startIndex,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) GetID() ID {
|
||||
return o.ID
|
||||
}
|
||||
|
||||
// GetType returns the OrderedCollectionPage's type
|
||||
func (o OrderedCollectionPage) GetType() ActivityVocabularyType {
|
||||
return o.Type
|
||||
}
|
||||
|
||||
// IsLink returns false for a OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for a OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns true for OrderedCollectionPage objects
|
||||
func (o OrderedCollectionPage) IsCollection() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the OrderedCollectionPage object
|
||||
func (o OrderedCollectionPage) GetLink() IRI {
|
||||
return IRI(o.ID)
|
||||
}
|
||||
|
||||
// Collection returns the underlying Collection type
|
||||
func (o OrderedCollectionPage) Collection() ItemCollection {
|
||||
return o.OrderedItems
|
||||
}
|
||||
|
||||
// Count returns the maximum between the length of Items in the collection page and its TotalItems property
|
||||
func (o *OrderedCollectionPage) Count() uint {
|
||||
if o == nil {
|
||||
return 0
|
||||
}
|
||||
return uint(len(o.OrderedItems))
|
||||
}
|
||||
|
||||
// Append adds an element to an OrderedCollectionPage
|
||||
func (o *OrderedCollectionPage) Append(it ...Item) error {
|
||||
for _, ob := range it {
|
||||
if o.OrderedItems.Contains(ob) {
|
||||
continue
|
||||
}
|
||||
o.OrderedItems = append(o.OrderedItems, ob)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Contains verifies if OrderedCollectionPage array contains the received one
|
||||
func (o OrderedCollectionPage) Contains(r Item) bool {
|
||||
if len(o.OrderedItems) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, it := range o.OrderedItems {
|
||||
if ItemsEqual(it, r) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (o *OrderedCollectionPage) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadOrderedCollectionPage(val, o)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (o OrderedCollectionPage) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(o, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if o.PartOf != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "partOf", o.PartOf) || notEmpty
|
||||
}
|
||||
if o.Current != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "current", o.Current) || notEmpty
|
||||
}
|
||||
if o.First != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "first", o.First) || notEmpty
|
||||
}
|
||||
if o.Last != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "last", o.Last) || notEmpty
|
||||
}
|
||||
if o.Next != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "next", o.Next) || notEmpty
|
||||
}
|
||||
if o.Prev != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "prev", o.Prev) || notEmpty
|
||||
}
|
||||
notEmpty = JSONWriteIntProp(&b, "totalItems", int64(o.TotalItems)) || notEmpty
|
||||
if o.OrderedItems != nil {
|
||||
notEmpty = JSONWriteItemCollectionProp(&b, "orderedItems", o.OrderedItems, false) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (o *OrderedCollectionPage) UnmarshalBinary(data []byte) error {
|
||||
return o.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (o OrderedCollectionPage) MarshalBinary() ([]byte, error) {
|
||||
return o.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (o OrderedCollectionPage) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapOrderedCollectionPageProperties(mm, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (o *OrderedCollectionPage) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapOrderedCollectionPageProperties(mm, o)
|
||||
}
|
||||
|
||||
// ToOrderedCollectionPage
|
||||
func ToOrderedCollectionPage(it Item) (*OrderedCollectionPage, error) {
|
||||
switch i := it.(type) {
|
||||
case *OrderedCollectionPage:
|
||||
return i, nil
|
||||
case OrderedCollectionPage:
|
||||
return &i, nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(OrderedCollectionPage))
|
||||
val := reflect.ValueOf(it)
|
||||
if val.IsValid() && typ.Elem().Name() == val.Type().Elem().Name() {
|
||||
conv := val.Convert(typ)
|
||||
if i, ok := conv.Interface().(*OrderedCollectionPage); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[OrderedCollectionPage](it)
|
||||
}
|
||||
|
||||
// ItemsMatch
|
||||
func (o OrderedCollectionPage) ItemsMatch(col ...Item) bool {
|
||||
for _, it := range col {
|
||||
if match := o.OrderedItems.Contains(it); !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals
|
||||
func (o OrderedCollectionPage) Equals(with Item) bool {
|
||||
if IsNil(with) {
|
||||
return false
|
||||
}
|
||||
if !with.IsCollection() {
|
||||
return false
|
||||
}
|
||||
result := true
|
||||
|
||||
OnOrderedCollectionPage(with, func(w *OrderedCollectionPage) error {
|
||||
OnOrderedCollection(w, func(wo *OrderedCollection) error {
|
||||
if !wo.Equals(o) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if w.PartOf != nil {
|
||||
if !ItemsEqual(o.PartOf, w.PartOf) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Current != nil {
|
||||
if !ItemsEqual(o.Current, w.Current) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.First != nil {
|
||||
if !ItemsEqual(o.First, w.First) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Last != nil {
|
||||
if !ItemsEqual(o.Last, w.Last) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Next != nil {
|
||||
if !ItemsEqual(o.Next, w.Next) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if w.Prev != nil {
|
||||
if !ItemsEqual(o.Prev, w.Prev) {
|
||||
result = false
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
func (o OrderedCollectionPage) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { totalItems: %d }", o, o.Type, o.TotalItems)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderedCollectionPage) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&o.To, &o.Bto, &o.CC, &o.BCC, &o.Audience)
|
||||
}
|
||||
|
||||
func (o *OrderedCollectionPage) Clean() {
|
||||
_ = OnObject(o, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Place represents a logical or physical location. See 5.3 Representing Places for additional information.
|
||||
type Place struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Accuracy indicates the accuracy of position coordinates on a Place objects.
|
||||
// Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
|
||||
Accuracy float64 `jsonld:"accuracy,omitempty"`
|
||||
// Altitude indicates the altitude of a place. The measurement units is indicated using the units property.
|
||||
// If units is not specified, the default is assumed to be "m" indicating meters.
|
||||
Altitude float64 `jsonld:"altitude,omitempty"`
|
||||
// Latitude the latitude of a place
|
||||
Latitude float64 `jsonld:"latitude,omitempty"`
|
||||
// Longitude the longitude of a place
|
||||
Longitude float64 `jsonld:"longitude,omitempty"`
|
||||
// Radius the radius from the given latitude and longitude for a Place.
|
||||
// The units is expressed by the units property. If units is not specified,
|
||||
// the default is assumed to be "m" indicating "meters".
|
||||
Radius int64 `jsonld:"radius,omitempty"`
|
||||
// Specifies the measurement units for the radius and altitude properties on a Place object.
|
||||
// If not specified, the default is assumed to be "m" for "meters".
|
||||
// Values "cm" | " feet" | " inches" | " km" | " m" | " miles" | xsd:anyURI
|
||||
Units string `jsonld:"units,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Place objects
|
||||
func (p Place) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Place objects
|
||||
func (p Place) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Place objects
|
||||
func (p Place) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Place object
|
||||
func (p Place) GetLink() IRI {
|
||||
return IRI(p.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Place
|
||||
func (p Place) GetType() ActivityVocabularyType {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Place
|
||||
func (p Place) GetID() ID {
|
||||
return p.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (p *Place) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadPlace(val, p)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (p Place) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(p, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if p.Accuracy > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "accuracy", p.Accuracy) || notEmpty
|
||||
}
|
||||
if p.Altitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "altitude", p.Altitude) || notEmpty
|
||||
}
|
||||
if p.Latitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "latitude", p.Latitude) || notEmpty
|
||||
}
|
||||
if p.Longitude > 0 {
|
||||
notEmpty = JSONWriteFloatProp(&b, "longitude", p.Longitude) || notEmpty
|
||||
}
|
||||
if p.Radius > 0 {
|
||||
notEmpty = JSONWriteIntProp(&b, "radius", p.Radius) || notEmpty
|
||||
}
|
||||
if len(p.Units) > 0 {
|
||||
notEmpty = JSONWriteStringProp(&b, "radius", p.Units) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (p *Place) UnmarshalBinary(data []byte) error {
|
||||
return p.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (p Place) MarshalBinary() ([]byte, error) {
|
||||
return p.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (p Place) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapPlaceProperties(mm, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (p *Place) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapPlaceProperties(mm, p)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Place object's To, Bto, CC and BCC properties
|
||||
func (p *Place) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&p.To, &p.Bto, &p.CC, &p.BCC, &p.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (p *Place) Clean() {
|
||||
_ = OnObject(p, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p Place) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", p, p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToPlace
|
||||
func ToPlace(it Item) (*Place, error) {
|
||||
switch i := it.(type) {
|
||||
case *Place:
|
||||
return i, nil
|
||||
case Place:
|
||||
return &i, nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Place))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Place); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Place](it)
|
||||
}
|
||||
|
||||
type withPlaceFn func(*Place) error
|
||||
|
||||
// OnPlace calls function fn on it Item if it can be asserted to type *Place
|
||||
//
|
||||
// This function should be called if trying to access the Place specific properties
|
||||
// like "accuracy", "altitude", "latitude", "longitude", "radius", or "units".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnPlace(it Item, fn withPlaceFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnPlace(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToPlace(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,282 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Profile a Profile is a content object that describes another Object,
|
||||
// typically used to describe CanReceiveActivities Type objects.
|
||||
// The describes property is used to reference the object being described by the profile.
|
||||
type Profile struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Describes On a Profile object, the describes property identifies the object described by the Profile.
|
||||
Describes Item `jsonld:"describes,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Profile objects
|
||||
func (p Profile) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Profile objects
|
||||
func (p Profile) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Profile objects
|
||||
func (p Profile) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Profile object
|
||||
func (p Profile) GetLink() IRI {
|
||||
return IRI(p.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Profile
|
||||
func (p Profile) GetType() ActivityVocabularyType {
|
||||
return p.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Profile
|
||||
func (p Profile) GetID() ID {
|
||||
return p.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (p *Profile) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadProfile(val, p)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (p Profile) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(p, func(o *Object) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if p.Describes != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "describes", p.Describes) || notEmpty
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (p *Profile) UnmarshalBinary(data []byte) error {
|
||||
return p.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (p Profile) MarshalBinary() ([]byte, error) {
|
||||
return p.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (p Profile) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapProfileProperties(mm, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (p *Profile) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapProfileProperties(mm, p)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Profile object's To, Bto, CC and BCC properties
|
||||
func (p *Profile) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&p.To, &p.Bto, &p.CC, &p.BCC, &p.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (p *Profile) Clean() {
|
||||
_ = OnObject(p, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (p Profile) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", p, p.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToProfile tries to convert the "it" Item to a Profile object
|
||||
func ToProfile(it Item) (*Profile, error) {
|
||||
switch i := it.(type) {
|
||||
case *Profile:
|
||||
return i, nil
|
||||
case Profile:
|
||||
return &i, nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Profile))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Profile); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Profile](it)
|
||||
}
|
||||
|
||||
type withProfileFn func(*Profile) error
|
||||
|
||||
// OnProfile calls function fn on it Item if it can be asserted to type *Profile
|
||||
//
|
||||
// This function should be called if trying to access the Profile specific properties
|
||||
// like "describes".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnProfile(it Item, fn withProfileFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if IsLink(it) {
|
||||
continue
|
||||
}
|
||||
if err := OnProfile(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToProfile(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,276 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Question represents a question being asked. Question objects are an extension of IntransitiveActivity.
|
||||
// That is, the Question object is an Activity, but the direct object is the question
|
||||
// itself and therefore it would not contain an object property.
|
||||
// Either of the anyOf and oneOf properties may be used to express possible answers,
|
||||
// but a Question object must not have both properties.
|
||||
type Question struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// CanReceiveActivities describes one or more entities that either performed or are expected to perform the activity.
|
||||
// Any single activity can have multiple actors. The actor may be specified using an indirect Link.
|
||||
Actor CanReceiveActivities `jsonld:"actor,omitempty"`
|
||||
// Target describes the indirect object, or target, of the activity.
|
||||
// The precise meaning of the target is largely dependent on the type of action being described
|
||||
// but will often be the object of the English preposition "to".
|
||||
// For instance, in the activity "John added a movie to his wishlist",
|
||||
// the target of the activity is John's wishlist. An activity can have more than one target.
|
||||
Target Item `jsonld:"target,omitempty"`
|
||||
// Result describes the result of the activity. For instance, if a particular action results in the creation
|
||||
// of a new resource, the result property can be used to describe that new resource.
|
||||
Result Item `jsonld:"result,omitempty"`
|
||||
// Origin describes an indirect object of the activity from which the activity is directed.
|
||||
// The precise meaning of the origin is the object of the English preposition "from".
|
||||
// For instance, in the activity "John moved an item to List B from List A", the origin of the activity is "List A".
|
||||
Origin Item `jsonld:"origin,omitempty"`
|
||||
// Instrument identifies one or more objects used (or to be used) in the completion of an Activity.
|
||||
Instrument Item `jsonld:"instrument,omitempty"`
|
||||
// OneOf identifies an exclusive option for a Question. Use of oneOf implies that the Question
|
||||
// can have only a single answer. To indicate that a Question can have multiple answers, use anyOf.
|
||||
OneOf Item `jsonld:"oneOf,omitempty"`
|
||||
// AnyOf identifies an inclusive option for a Question. Use of anyOf implies that the Question can have multiple answers.
|
||||
// To indicate that a Question can have only one answer, use oneOf.
|
||||
AnyOf Item `jsonld:"anyOf,omitempty"`
|
||||
// Closed indicates that a question has been closed, and answers are no longer accepted.
|
||||
Closed bool `jsonld:"closed,omitempty"`
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the Question object
|
||||
func (q Question) GetID() ID {
|
||||
return q.ID
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the Question object
|
||||
func (q Question) GetLink() IRI {
|
||||
return IRI(q.ID)
|
||||
}
|
||||
|
||||
// GetType returns the ActivityVocabulary type of the current Activity
|
||||
func (q Question) GetType() ActivityVocabularyType {
|
||||
return q.Type
|
||||
}
|
||||
|
||||
// IsObject returns true for Question objects
|
||||
func (q Question) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsLink returns false for Question objects
|
||||
func (q Question) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCollection returns false for Question objects
|
||||
func (q Question) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (q *Question) UnmarshalJSON(data []byte) error {
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadQuestion(val, q)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (q Question) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
if !JSONWriteQuestionValue(&b, q) {
|
||||
return nil, nil
|
||||
}
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (q *Question) UnmarshalBinary(data []byte) error {
|
||||
return q.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (q Question) MarshalBinary() ([]byte, error) {
|
||||
return q.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (q Question) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapQuestionProperties(mm, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (q *Question) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapQuestionProperties(mm, q)
|
||||
}
|
||||
|
||||
func (q Question) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", q, q.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// QuestionNew initializes a Question activity
|
||||
func QuestionNew(id ID) *Question {
|
||||
q := Question{ID: id, Type: QuestionType}
|
||||
q.Name = NaturalLanguageValuesNew()
|
||||
q.Content = NaturalLanguageValuesNew()
|
||||
return &q
|
||||
}
|
||||
|
||||
// ToQuestion tries to convert the it Item to a Question object.
|
||||
func ToQuestion(it Item) (*Question, error) {
|
||||
switch i := it.(type) {
|
||||
case *Question:
|
||||
return i, nil
|
||||
case Question:
|
||||
return &i, nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Question))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Question); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Question](it)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Question's To, Bto, CC and BCC properties
|
||||
func (q *Question) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&ItemCollection{q.Actor}, &q.To, &q.Bto, &q.CC, &q.BCC, &q.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (q *Question) Clean() {
|
||||
_ = OnObject(q, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Relationship describes a relationship between two individuals.
|
||||
// The subject and object properties are used to identify the connected individuals.
|
||||
// See 5.2 Representing Relationships Between Entities for additional information.
|
||||
//
|
||||
// 5.2: The relationship property specifies the kind of relationship that exists between the two individuals identified
|
||||
// by the subject and object properties. Used together, these three properties form what is commonly known
|
||||
// as a "reified statement" where subject identifies the subject, relationship identifies the predicate,
|
||||
// and object identifies the object.
|
||||
type Relationship struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// Subject Subject On a Relationship object, the subject property identifies one of the connected individuals.
|
||||
// For instance, for a Relationship object describing "John is related to Sally", subject would refer to John.
|
||||
Subject Item `jsonld:"subject,omitempty"`
|
||||
// Object
|
||||
Object Item `jsonld:"object,omitempty"`
|
||||
// Relationship On a Relationship object, the relationship property identifies the kind
|
||||
// of relationship that exists between subject and object.
|
||||
Relationship Item `jsonld:"relationship,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Relationship objects
|
||||
func (r Relationship) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Relationship objects
|
||||
func (r Relationship) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Relationship objects
|
||||
func (r Relationship) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Relationship object
|
||||
func (r Relationship) GetLink() IRI {
|
||||
return IRI(r.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Relationship
|
||||
func (r Relationship) GetType() ActivityVocabularyType {
|
||||
return r.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Relationship
|
||||
func (r Relationship) GetID() ID {
|
||||
return r.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (r *Relationship) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadRelationship(val, r)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (r Relationship) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(r, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
|
||||
if r.Subject != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "subject", r.Subject) || notEmpty
|
||||
}
|
||||
if r.Object != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "object", r.Object) || notEmpty
|
||||
}
|
||||
if r.Relationship != nil {
|
||||
notEmpty = JSONWriteItemProp(&b, "relationship", r.Relationship) || notEmpty
|
||||
}
|
||||
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (r *Relationship) UnmarshalBinary(data []byte) error {
|
||||
return r.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (r Relationship) MarshalBinary() ([]byte, error) {
|
||||
return r.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (r Relationship) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapRelationshipProperties(mm, r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (r *Relationship) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapRelationshipProperties(mm, r)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Relationship object's To, Bto, CC and BCC properties
|
||||
func (r *Relationship) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&r.To, &r.Bto, &r.CC, &r.BCC, &r.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (r *Relationship) Clean() {
|
||||
_ = OnObject(r, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r Relationship) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { }", r, r.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// ToRelationship tries to convert the "it" Item to a Relationship object.
|
||||
func ToRelationship(it Item) (*Relationship, error) {
|
||||
switch i := it.(type) {
|
||||
case *Relationship:
|
||||
return i, nil
|
||||
case Relationship:
|
||||
return &i, nil
|
||||
case *Object:
|
||||
return (*Relationship)(unsafe.Pointer(i)), nil
|
||||
case Object:
|
||||
return (*Relationship)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Relationship))
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Relationship); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Relationship](it)
|
||||
}
|
||||
|
||||
type withRelationshipFn func(*Relationship) error
|
||||
|
||||
// OnRelationship calls function fn on it Item if it can be asserted to type *Relationship
|
||||
//
|
||||
// This function should be called if trying to access the Relationship specific properties
|
||||
// like "subject", "object", or "relationship".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnRelationship(it Item, fn withRelationshipFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
ob, err := ToRelationship(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
// Tombstone a Tombstone represents a content object that has been deleted.
|
||||
// It can be used in Collections to signify that there used to be an object at this position,
|
||||
// but it has been deleted.
|
||||
type Tombstone struct {
|
||||
// ID provides the globally unique identifier for anActivity Pub Object or Link.
|
||||
ID ID `jsonld:"id,omitempty"`
|
||||
// Type identifies the Activity Pub Object or Link type. Multiple values may be specified.
|
||||
Type ActivityVocabularyType `jsonld:"type,omitempty"`
|
||||
// Name a simple, human-readable, plain-text name for the object.
|
||||
// HTML markup MUST NOT be included. The name MAY be expressed using multiple language-tagged values.
|
||||
Name NaturalLanguageValues `jsonld:"name,omitempty,collapsible"`
|
||||
// Attachment identifies a resource attached or related to an object that potentially requires special handling.
|
||||
// The intent is to provide a model that is at least semantically similar to attachments in email.
|
||||
Attachment Item `jsonld:"attachment,omitempty"`
|
||||
// AttributedTo identifies one or more entities to which this object is attributed. The attributed entities might not be Actors.
|
||||
// For instance, an object might be attributed to the completion of another activity.
|
||||
AttributedTo Item `jsonld:"attributedTo,omitempty"`
|
||||
// Audience identifies one or more entities that represent the total population of entities
|
||||
// for which the object can considered to be relevant.
|
||||
Audience ItemCollection `jsonld:"audience,omitempty"`
|
||||
// Content or textual representation of the Activity Pub Object encoded as a JSON string.
|
||||
// By default, the value of content is HTML.
|
||||
// The mediaType property can be used in the object to indicate a different content type.
|
||||
// (The content MAY be expressed using multiple language-tagged values.)
|
||||
Content NaturalLanguageValues `jsonld:"content,omitempty,collapsible"`
|
||||
// Context identifies the context within which the object exists or an activity was performed.
|
||||
// The notion of "context" used is intentionally vague.
|
||||
// The intended function is to serve as a means of grouping objects and activities that share a
|
||||
// common originating context or purpose. An example could be all activities relating to a common project or event.
|
||||
Context Item `jsonld:"context,omitempty"`
|
||||
// MediaType when used on an Object, identifies the MIME media type of the value of the content property.
|
||||
// If not specified, the content property is assumed to contain text/html content.
|
||||
MediaType MimeType `jsonld:"mediaType,omitempty"`
|
||||
// EndTime the date and time describing the actual or expected ending time of the object.
|
||||
// When used with an Activity object, for instance, the endTime property specifies the moment
|
||||
// the activity concluded or is expected to conclude.
|
||||
EndTime time.Time `jsonld:"endTime,omitempty"`
|
||||
// Generator identifies the entity (e.g. an application) that generated the object.
|
||||
Generator Item `jsonld:"generator,omitempty"`
|
||||
// Icon indicates an entity that describes an icon for this object.
|
||||
// The image should have an aspect ratio of one (horizontal) to one (vertical)
|
||||
// and should be suitable for presentation at a small size.
|
||||
Icon Item `jsonld:"icon,omitempty"`
|
||||
// Image indicates an entity that describes an image for this object.
|
||||
// Unlike the icon property, there are no aspect ratio or display size limitations assumed.
|
||||
Image Item `jsonld:"image,omitempty"`
|
||||
// InReplyTo indicates one or more entities for which this object is considered a response.
|
||||
InReplyTo Item `jsonld:"inReplyTo,omitempty"`
|
||||
// Location indicates one or more physical or logical locations associated with the object.
|
||||
Location Item `jsonld:"location,omitempty"`
|
||||
// Preview identifies an entity that provides a preview of this object.
|
||||
Preview Item `jsonld:"preview,omitempty"`
|
||||
// Published the date and time at which the object was published
|
||||
Published time.Time `jsonld:"published,omitempty"`
|
||||
// Replies identifies a Collection containing objects considered to be responses to this object.
|
||||
Replies Item `jsonld:"replies,omitempty"`
|
||||
// StartTime the date and time describing the actual or expected starting time of the object.
|
||||
// When used with an Activity object, for instance, the startTime property specifies
|
||||
// the moment the activity began or is scheduled to begin.
|
||||
StartTime time.Time `jsonld:"startTime,omitempty"`
|
||||
// Summary a natural language summarization of the object encoded as HTML.
|
||||
// *Multiple language tagged summaries may be provided.)
|
||||
Summary NaturalLanguageValues `jsonld:"summary,omitempty,collapsible"`
|
||||
// Tag one or more "tags" that have been associated with an objects. A tag can be any kind of Activity Pub Object.
|
||||
// The key difference between attachment and tag is that the former implies association by inclusion,
|
||||
// while the latter implies associated by reference.
|
||||
Tag ItemCollection `jsonld:"tag,omitempty"`
|
||||
// Updated the date and time at which the object was updated
|
||||
Updated time.Time `jsonld:"updated,omitempty"`
|
||||
// URL identifies one or more links to representations of the object
|
||||
URL Item `jsonld:"url,omitempty"`
|
||||
// To identifies an entity considered to be part of the public primary audience of an Activity Pub Object
|
||||
To ItemCollection `jsonld:"to,omitempty"`
|
||||
// Bto identifies anActivity Pub Object that is part of the private primary audience of this Activity Pub Object.
|
||||
Bto ItemCollection `jsonld:"bto,omitempty"`
|
||||
// CC identifies anActivity Pub Object that is part of the public secondary audience of this Activity Pub Object.
|
||||
CC ItemCollection `jsonld:"cc,omitempty"`
|
||||
// BCC identifies one or more Objects that are part of the private secondary audience of this Activity Pub Object.
|
||||
BCC ItemCollection `jsonld:"bcc,omitempty"`
|
||||
// Duration when the object describes a time-bound resource, such as an audio or video, a meeting, etc,
|
||||
// the duration property indicates the object's approximate duration.
|
||||
// The value must be expressed as an xsd:duration as defined by [ xmlschema11-2],
|
||||
// section 3.3.6 (e.g. a period of 5 seconds is represented as "PT5S").
|
||||
Duration time.Duration `jsonld:"duration,omitempty"`
|
||||
// This is a list of all Like activities with this object as the object property, added as a side effect.
|
||||
// The likes collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Likes Item `jsonld:"likes,omitempty"`
|
||||
// This is a list of all Announce activities with this object as the object property, added as a side effect.
|
||||
// The shares collection MUST be either an OrderedCollection or a Collection and MAY be filtered on privileges
|
||||
// of an authenticated user or as appropriate when no authentication is given.
|
||||
Shares Item `jsonld:"shares,omitempty"`
|
||||
// Source property is intended to convey some sort of source from which the content markup was derived,
|
||||
// as a form of provenance, or to support future editing by clients.
|
||||
// In general, clients do the conversion from source to content, not the other way around.
|
||||
Source Source `jsonld:"source,omitempty"`
|
||||
// FormerType On a Tombstone object, the formerType property identifies the type of the object that was deleted.
|
||||
FormerType ActivityVocabularyType `jsonld:"formerType,omitempty"`
|
||||
// Deleted On a Tombstone object, the deleted property is a timestamp for when the object was deleted.
|
||||
Deleted time.Time `jsonld:"deleted,omitempty"`
|
||||
}
|
||||
|
||||
// IsLink returns false for Tombstone objects
|
||||
func (t Tombstone) IsLink() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsObject returns true for Tombstone objects
|
||||
func (t Tombstone) IsObject() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsCollection returns false for Tombstone objects
|
||||
func (t Tombstone) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetLink returns the IRI corresponding to the current Tombstone object
|
||||
func (t Tombstone) GetLink() IRI {
|
||||
return IRI(t.ID)
|
||||
}
|
||||
|
||||
// GetType returns the type of the current Tombstone
|
||||
func (t Tombstone) GetType() ActivityVocabularyType {
|
||||
return t.Type
|
||||
}
|
||||
|
||||
// GetID returns the ID corresponding to the current Tombstone
|
||||
func (t Tombstone) GetID() ID {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
// UnmarshalJSON decodes an incoming JSON document into the receiver object.
|
||||
func (t *Tombstone) UnmarshalJSON(data []byte) error {
|
||||
par := fastjson.Parser{}
|
||||
val, err := par.ParseBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return JSONLoadTombstone(val, t)
|
||||
}
|
||||
|
||||
// MarshalJSON encodes the receiver object to a JSON document.
|
||||
func (t Tombstone) MarshalJSON() ([]byte, error) {
|
||||
b := make([]byte, 0)
|
||||
notEmpty := false
|
||||
JSONWrite(&b, '{')
|
||||
|
||||
OnObject(t, func(o *Object) error {
|
||||
notEmpty = JSONWriteObjectValue(&b, *o)
|
||||
return nil
|
||||
})
|
||||
if len(t.FormerType) > 0 {
|
||||
if v, err := t.FormerType.MarshalJSON(); err == nil && len(v) > 0 {
|
||||
notEmpty = JSONWriteProp(&b, "formerType", v) || notEmpty
|
||||
}
|
||||
}
|
||||
if !t.Deleted.IsZero() {
|
||||
notEmpty = JSONWriteTimeProp(&b, "deleted", t.Deleted) || notEmpty
|
||||
}
|
||||
if notEmpty {
|
||||
JSONWrite(&b, '}')
|
||||
return b, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||
func (t *Tombstone) UnmarshalBinary(data []byte) error {
|
||||
return t.GobDecode(data)
|
||||
}
|
||||
|
||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||
func (t Tombstone) MarshalBinary() ([]byte, error) {
|
||||
return t.GobEncode()
|
||||
}
|
||||
|
||||
// GobEncode
|
||||
func (t Tombstone) GobEncode() ([]byte, error) {
|
||||
mm := make(map[string][]byte)
|
||||
hasData, err := mapTombstoneProperties(mm, t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !hasData {
|
||||
return []byte{}, nil
|
||||
}
|
||||
bb := bytes.Buffer{}
|
||||
g := gob.NewEncoder(&bb)
|
||||
if err := g.Encode(mm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.Bytes(), nil
|
||||
}
|
||||
|
||||
// GobDecode
|
||||
func (t *Tombstone) GobDecode(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
mm, err := gobDecodeObjectAsMap(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmapTombstoneProperties(mm, t)
|
||||
}
|
||||
|
||||
// Recipients performs recipient de-duplication on the Tombstone object's To, Bto, CC and BCC properties
|
||||
func (t *Tombstone) Recipients() ItemCollection {
|
||||
return ItemCollectionDeduplication(&t.To, &t.Bto, &t.CC, &t.BCC, &t.Audience)
|
||||
}
|
||||
|
||||
// Clean removes Bto and BCC properties
|
||||
func (t *Tombstone) Clean() {
|
||||
_ = OnObject(t, func(o *Object) error {
|
||||
o.Clean()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t Tombstone) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's', 'v':
|
||||
_, _ = fmt.Fprintf(s, "%T[%s] { formerType: %q }", t, t.Type, t.FormerType)
|
||||
}
|
||||
}
|
||||
|
||||
// ToTombstone
|
||||
func ToTombstone(it Item) (*Tombstone, error) {
|
||||
switch i := it.(type) {
|
||||
case *Tombstone:
|
||||
return i, nil
|
||||
case Tombstone:
|
||||
return &i, nil
|
||||
case *Object:
|
||||
return (*Tombstone)(unsafe.Pointer(i)), nil
|
||||
case Object:
|
||||
return (*Tombstone)(unsafe.Pointer(&i)), nil
|
||||
default:
|
||||
// NOTE(marius): this is an ugly way of dealing with the interface conversion error: types from different scopes
|
||||
typ := reflect.TypeOf(new(Tombstone))
|
||||
if reflect.TypeOf(it).ConvertibleTo(typ) {
|
||||
if i, ok := reflect.ValueOf(it).Convert(typ).Interface().(*Tombstone); ok {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ErrorInvalidType[Tombstone](it)
|
||||
}
|
||||
|
||||
type withTombstoneFn func(*Tombstone) error
|
||||
|
||||
// OnTombstone calls function fn on it Item if it can be asserted to type *Tombstone
|
||||
//
|
||||
// This function should be called if trying to access the Tombstone specific properties
|
||||
// like "formerType" or "deleted".
|
||||
// For the other properties OnObject should be used instead.
|
||||
func OnTombstone(it Item, fn withTombstoneFn) error {
|
||||
if it == nil {
|
||||
return nil
|
||||
}
|
||||
if IsItemCollection(it) {
|
||||
return OnItemCollection(it, func(col *ItemCollection) error {
|
||||
for _, it := range *col {
|
||||
if err := OnTombstone(it, fn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ob, err := ToTombstone(it)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fn(ob)
|
||||
}
|
|
@ -0,0 +1,397 @@
|
|||
package activitypub
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/go-ap/errors"
|
||||
)
|
||||
|
||||
// CollectionPath
|
||||
type CollectionPath string
|
||||
|
||||
// CollectionPaths
|
||||
type CollectionPaths []CollectionPath
|
||||
|
||||
const (
|
||||
Unknown = CollectionPath("")
|
||||
// Outbox
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#outbox
|
||||
//
|
||||
// The outbox is discovered through the outbox property of an actor's profile.
|
||||
// The outbox MUST be an OrderedCollection.
|
||||
//
|
||||
// The outbox stream contains activities the user has published, subject to the ability of the requestor
|
||||
// to retrieve the activity (that is, the contents of the outbox are filtered by the permissions
|
||||
// of the person reading it). If a user submits a request without Authorization the server should respond
|
||||
// with all of the Public posts. This could potentially be all relevant objects published by the user,
|
||||
// though the number of available items is left to the discretion of those implementing and deploying the server.
|
||||
//
|
||||
// The outbox accepts HTTP POST requests, with behaviour described in Client to Server Interactions.
|
||||
Outbox = CollectionPath("outbox")
|
||||
// Inbox
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#inbox
|
||||
//
|
||||
// The inbox is discovered through the inbox property of an actor's profile. The inbox MUST be an OrderedCollection.
|
||||
//
|
||||
// The inbox stream contains all activities received by the actor. The server SHOULD filter content according
|
||||
// to the requester's permission. In general, the owner of an inbox is likely to be able to access
|
||||
// all of their inbox contents. Depending on access control, some other content may be public,
|
||||
// whereas other content may require authentication for non-owner users, if they can access the inbox at all.
|
||||
//
|
||||
// The server MUST perform de-duplication of activities returned by the inbox. Duplication can occur
|
||||
// if an activity is addressed both to an actor's followers, and a specific actor who also follows
|
||||
// the recipient actor, and the server has failed to de-duplicate the recipients list.
|
||||
// Such deduplication MUST be performed by comparing the id of the activities and dropping any activities already seen.
|
||||
//
|
||||
// The inboxes of actors on federated servers accepts HTTP POST requests, with behaviour described in Delivery.
|
||||
// Non-federated servers SHOULD return a 405 Method Not Allowed upon receipt of a POST request.
|
||||
Inbox = CollectionPath("inbox")
|
||||
// Followers
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#followers
|
||||
//
|
||||
// Every actor SHOULD have a followers collection. This is a list of everyone who has sent a Follow activity
|
||||
// for the actor, added as a side effect. This is where one would find a list of all the actors that are following
|
||||
// the actor. The followers collection MUST be either an OrderedCollection or a Collection and MAY be filtered
|
||||
// on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
//
|
||||
// NOTE: Default for notification targeting
|
||||
// The follow activity generally is a request to see the objects an actor creates.
|
||||
// This makes the Followers collection an appropriate default target for delivery of notifications.
|
||||
Followers = CollectionPath("followers")
|
||||
// Following
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#following
|
||||
//
|
||||
// Every actor SHOULD have a following collection. This is a list of everybody that the actor has followed,
|
||||
// added as a side effect. The following collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
Following = CollectionPath("following")
|
||||
// Liked
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#liked
|
||||
//
|
||||
// Every actor MAY have a liked collection. This is a list of every object from all of the actor's Like activities,
|
||||
// added as a side effect. The liked collection MUST be either an OrderedCollection or a Collection and
|
||||
// MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
Liked = CollectionPath("liked")
|
||||
// Likes
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#likes
|
||||
//
|
||||
// Every object MAY have a likes collection. This is a list of all Like activities with this object as the object
|
||||
// property, added as a side effect. The likes collection MUST be either an OrderedCollection or a Collection
|
||||
// and MAY be filtered on privileges of an authenticated user or as appropriate when no authentication is given.
|
||||
//
|
||||
// NOTE
|
||||
// Care should be taken to not confuse the the likes collection with the similarly named but different liked
|
||||
// collection. In sum:
|
||||
//
|
||||
// * liked: Specifically a property of actors. This is a collection of Like activities performed by the actor,
|
||||
// added to the collection as a side effect of delivery to the outbox.
|
||||
// * likes: May be a property of any object. This is a collection of Like activities referencing this object,
|
||||
// added to the collection as a side effect of delivery to the inbox.
|
||||
Likes = CollectionPath("likes")
|
||||
// Shares
|
||||
//
|
||||
// https://www.w3.org/TR/activitypub/#shares
|
||||
//
|
||||
// Every object MAY have a shares collection. This is a list of all Announce activities with this object
|
||||
// as the object property, added as a side effect. The shares collection MUST be either an OrderedCollection
|
||||
// or a Collection and MAY be filtered on privileges of an authenticated user or as appropriate when
|
||||
// no authentication is given.
|
||||
Shares = CollectionPath("shares")
|
||||
Replies = CollectionPath("replies") // activitystreams
|
||||
)
|
||||
|
||||
var (
|
||||
validActivityCollection = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Likes,
|
||||
Shares,
|
||||
Replies, // activitystreams
|
||||
}
|
||||
OfObject = CollectionPaths{
|
||||
Likes,
|
||||
Shares,
|
||||
Replies,
|
||||
}
|
||||
OfActor = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Liked,
|
||||
Following,
|
||||
Followers,
|
||||
}
|
||||
|
||||
ActivityPubCollections = CollectionPaths{
|
||||
Outbox,
|
||||
Inbox,
|
||||
Liked,
|
||||
Following,
|
||||
Followers,
|
||||
Likes,
|
||||
Shares,
|
||||
Replies,
|
||||
}
|
||||
)
|
||||
|
||||
func (t CollectionPaths) Contains(typ CollectionPath) bool {
|
||||
for _, tt := range t {
|
||||
if strings.EqualFold(string(typ), string(tt)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Split splits the IRI in an actor IRI and its CollectionPath
|
||||
// if the CollectionPath is found in the elements in the t CollectionPaths slice
|
||||
func (t CollectionPaths) Split(i IRI) (IRI, CollectionPath) {
|
||||
if u, err := i.URL(); err == nil {
|
||||
maybeActor, maybeCol := filepath.Split(u.Path)
|
||||
if len(maybeActor) == 0 {
|
||||
return i, Unknown
|
||||
}
|
||||
tt := CollectionPath(maybeCol)
|
||||
if !t.Contains(tt) {
|
||||
tt = ""
|
||||
}
|
||||
u.Path = strings.TrimRight(maybeActor, "/")
|
||||
iri := IRI(u.String())
|
||||
return iri, tt
|
||||
}
|
||||
maybeActor, maybeCol := filepath.Split(i.String())
|
||||
if len(maybeActor) == 0 {
|
||||
return i, Unknown
|
||||
}
|
||||
tt := CollectionPath(maybeCol)
|
||||
if !t.Contains(tt) {
|
||||
return i, Unknown
|
||||
}
|
||||
maybeActor = strings.TrimRight(maybeActor, "/")
|
||||
return IRI(maybeActor), tt
|
||||
}
|
||||
|
||||
// IRIf formats an IRI from an existing IRI and the CollectionPath type
|
||||
func IRIf(i IRI, t CollectionPath) IRI {
|
||||
si := i.String()
|
||||
s := strings.Builder{}
|
||||
_, _ = s.WriteString(si)
|
||||
if l := len(si); l == 0 || si[l-1] != '/' {
|
||||
_, _ = s.WriteRune('/')
|
||||
}
|
||||
_, _ = s.WriteString(string(t))
|
||||
return IRI(s.String())
|
||||
}
|
||||
|
||||
// IRI gives us the IRI of the t CollectionPath type corresponding to the i Item,
|
||||
// or generates a new one if not found.
|
||||
func (t CollectionPath) IRI(i Item) IRI {
|
||||
if IsNil(i) {
|
||||
return IRIf("", t)
|
||||
}
|
||||
if IsObject(i) {
|
||||
if it := t.Of(i); !IsNil(it) {
|
||||
return it.GetLink()
|
||||
}
|
||||
}
|
||||
return IRIf(i.GetLink(), t)
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofItemCollection(col ItemCollection) Item {
|
||||
iriCol := make(ItemCollection, len(col))
|
||||
for i, it := range col {
|
||||
iriCol[i] = t.Of(it)
|
||||
}
|
||||
return iriCol
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofObject(ob *Object) Item {
|
||||
var it Item
|
||||
switch t {
|
||||
case Likes:
|
||||
it = ob.Likes
|
||||
case Shares:
|
||||
it = ob.Shares
|
||||
case Replies:
|
||||
it = ob.Replies
|
||||
}
|
||||
if it == nil {
|
||||
it = t.ofIRI(ob.ID)
|
||||
}
|
||||
return it
|
||||
}
|
||||
func (t CollectionPath) ofActor(a *Actor) Item {
|
||||
var it Item
|
||||
switch t {
|
||||
case Inbox:
|
||||
it = a.Inbox
|
||||
case Outbox:
|
||||
it = a.Outbox
|
||||
case Liked:
|
||||
it = a.Liked
|
||||
case Following:
|
||||
it = a.Following
|
||||
case Followers:
|
||||
it = a.Followers
|
||||
}
|
||||
if it == nil {
|
||||
it = t.ofIRI(a.ID)
|
||||
}
|
||||
return it
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofIRI(iri IRI) Item {
|
||||
if len(iri) == 0 {
|
||||
return nil
|
||||
}
|
||||
return iri.AddPath(string(t))
|
||||
}
|
||||
|
||||
func (t CollectionPath) ofItem(i Item) Item {
|
||||
var it Item
|
||||
return it
|
||||
}
|
||||
|
||||
// Of gives us the property of the i Item that corresponds to the t CollectionPath type.
|
||||
func (t CollectionPath) Of(i Item) Item {
|
||||
if IsNil(i) {
|
||||
return nil
|
||||
}
|
||||
it := t.ofIRI(i.GetLink())
|
||||
if IsItemCollection(i) {
|
||||
OnItemCollection(i, func(col *ItemCollection) error {
|
||||
it = t.ofItemCollection(*col)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if OfActor.Contains(t) && ActorTypes.Contains(i.GetType()) {
|
||||
OnActor(i, func(a *Actor) error {
|
||||
it = t.ofActor(a)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
OnObject(i, func(o *Object) error {
|
||||
it = t.ofObject(o)
|
||||
return nil
|
||||
})
|
||||
return it
|
||||
}
|
||||
|
||||
// OfActor returns the base IRI of received i, if i represents an IRI matching CollectionPath type t
|
||||
func (t CollectionPath) OfActor(i IRI) (IRI, error) {
|
||||
maybeActor, maybeCol := filepath.Split(i.String())
|
||||
if strings.EqualFold(maybeCol, string(t)) {
|
||||
maybeActor = strings.TrimRight(maybeActor, "/")
|
||||
return IRI(maybeActor), nil
|
||||
}
|
||||
return EmptyIRI, errors.Newf("IRI does not represent a valid %s CollectionPath", t)
|
||||
}
|
||||
|
||||
// Split returns the base IRI of received i, if i represents an IRI matching CollectionPath type t
|
||||
func Split(i IRI) (IRI, CollectionPath) {
|
||||
return ActivityPubCollections.Split(i)
|
||||
}
|
||||
|
||||
func getValidActivityCollection(t CollectionPath) CollectionPath {
|
||||
if validActivityCollection.Contains(t) {
|
||||
return t
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Activities
|
||||
func ValidActivityCollection(typ CollectionPath) bool {
|
||||
return getValidActivityCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
var validObjectCollection = []CollectionPath{
|
||||
Following,
|
||||
Followers,
|
||||
Liked,
|
||||
}
|
||||
|
||||
func getValidObjectCollection(typ CollectionPath) CollectionPath {
|
||||
for _, t := range validObjectCollection {
|
||||
if strings.EqualFold(string(typ), string(t)) {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// ValidActivityCollection shows if the current ActivityPub end-point type is a valid one for handling Objects
|
||||
func ValidObjectCollection(typ CollectionPath) bool {
|
||||
return getValidObjectCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
func getValidCollection(typ CollectionPath) CollectionPath {
|
||||
if typ := getValidActivityCollection(typ); typ != Unknown {
|
||||
return typ
|
||||
}
|
||||
if typ := getValidObjectCollection(typ); typ != Unknown {
|
||||
return typ
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
func ValidCollection(typ CollectionPath) bool {
|
||||
return getValidCollection(typ) != Unknown
|
||||
}
|
||||
|
||||
func ValidCollectionIRI(i IRI) bool {
|
||||
_, t := Split(i)
|
||||
return getValidCollection(t) != Unknown
|
||||
}
|
||||
|
||||
// AddTo adds CollectionPath type IRI on the corresponding property of the i Item
|
||||
func (t CollectionPath) AddTo(i Item) (IRI, bool) {
|
||||
if IsNil(i) || !i.IsObject() {
|
||||
return NilIRI, false
|
||||
}
|
||||
status := false
|
||||
var iri IRI
|
||||
if OfActor.Contains(t) {
|
||||
OnActor(i, func(a *Actor) error {
|
||||
if status = t == Inbox && IsNil(a.Inbox); status {
|
||||
a.Inbox = IRIf(a.GetLink(), t)
|
||||
iri = a.Inbox.GetLink()
|
||||
} else if status = t == Outbox && IsNil(a.Outbox); status {
|
||||
a.Outbox = IRIf(a.GetLink(), t)
|
||||
iri = a.Outbox.GetLink()
|
||||
} else if status = t == Liked && IsNil(a.Liked); status {
|
||||
a.Liked = IRIf(a.GetLink(), t)
|
||||
iri = a.Liked.GetLink()
|
||||
} else if status = t == Following && IsNil(a.Following); status {
|
||||
a.Following = IRIf(a.GetLink(), t)
|
||||
iri = a.Following.GetLink()
|
||||
} else if status = t == Followers && IsNil(a.Followers); status {
|
||||
a.Followers = IRIf(a.GetLink(), t)
|
||||
iri = a.Followers.GetLink()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else if OfObject.Contains(t) {
|
||||
OnObject(i, func(o *Object) error {
|
||||
if status = t == Likes && IsNil(o.Likes); status {
|
||||
o.Likes = IRIf(o.GetLink(), t)
|
||||
iri = o.Likes.GetLink()
|
||||
} else if status = t == Shares && IsNil(o.Shares); status {
|
||||
o.Shares = IRIf(o.GetLink(), t)
|
||||
iri = o.Shares.GetLink()
|
||||
} else if status = t == Replies && IsNil(o.Replies); status {
|
||||
o.Replies = IRIf(o.GetLink(), t)
|
||||
iri = o.Replies.GetLink()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
iri = IRIf(i.GetLink(), t)
|
||||
}
|
||||
return iri, status
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package activitypub
|
||||
|
||||
// ActivityVocabularyTypes is a type alias for a slice of ActivityVocabularyType elements
|
||||
type ActivityVocabularyTypes []ActivityVocabularyType
|
||||
|
||||
// Types contains all valid types in the ActivityPub vocabulary
|
||||
var Types = ActivityVocabularyTypes{
|
||||
LinkType,
|
||||
MentionType,
|
||||
|
||||
ArticleType,
|
||||
AudioType,
|
||||
DocumentType,
|
||||
EventType,
|
||||
ImageType,
|
||||
NoteType,
|
||||
PageType,
|
||||
PlaceType,
|
||||
ProfileType,
|
||||
RelationshipType,
|
||||
TombstoneType,
|
||||
VideoType,
|
||||
|
||||
QuestionType,
|
||||
|
||||
CollectionType,
|
||||
OrderedCollectionType,
|
||||
CollectionPageType,
|
||||
OrderedCollectionPageType,
|
||||
|
||||
ApplicationType,
|
||||
GroupType,
|
||||
OrganizationType,
|
||||
PersonType,
|
||||
ServiceType,
|
||||
|
||||
AcceptType,
|
||||
AddType,
|
||||
AnnounceType,
|
||||
BlockType,
|
||||
CreateType,
|
||||
DeleteType,
|
||||
DislikeType,
|
||||
FlagType,
|
||||
FollowType,
|
||||
IgnoreType,
|
||||
InviteType,
|
||||
JoinType,
|
||||
LeaveType,
|
||||
LikeType,
|
||||
ListenType,
|
||||
MoveType,
|
||||
OfferType,
|
||||
RejectType,
|
||||
ReadType,
|
||||
RemoveType,
|
||||
TentativeRejectType,
|
||||
TentativeAcceptType,
|
||||
UndoType,
|
||||
UpdateType,
|
||||
ViewType,
|
||||
|
||||
ArriveType,
|
||||
TravelType,
|
||||
QuestionType,
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package activitypub
|
||||
|
||||
// ValidationErrors is an aggregated error interface that allows
|
||||
// a Validator implementation to return all possible errors.
|
||||
type ValidationErrors interface {
|
||||
error
|
||||
Errors() []error
|
||||
Add(error)
|
||||
}
|
||||
|
||||
// Validator is the interface that needs to be implemented by objects that
|
||||
// provide a validation mechanism for incoming ActivityPub Objects or IRIs
|
||||
// against an external set of rules.
|
||||
type Validator interface {
|
||||
Validate(receiver IRI, incoming Item) (bool, ValidationErrors)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
image: archlinux
|
||||
packages:
|
||||
- go
|
||||
sources:
|
||||
- https://github.com/go-ap/errors
|
||||
environment:
|
||||
GO111MODULE: 'on'
|
||||
tasks:
|
||||
- tests: |
|
||||
cd errors
|
||||
make test
|
||||
- coverage: |
|
||||
set -a +x
|
||||
cd errors && make coverage
|
||||
GIT_SHA=$(git rev-parse --verify HEAD)
|
||||
GIT_BRANCH=$(git name-rev --name-only HEAD)
|
|
@ -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
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2019 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.
|
|
@ -0,0 +1,22 @@
|
|||
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
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
[![MIT Licensed](https://img.shields.io/github/license/go-ap/errors.svg)](https://raw.githubusercontent.com/go-ap/errors/master/LICENSE)
|
||||
[![Build Status](https://builds.sr.ht/~mariusor/errors.svg)](https://builds.sr.ht/~mariusor/errors)
|
||||
[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/errors.svg)](https://codecov.io/gh/go-ap/errors)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/errors)](https://goreportcard.com/report/github.com/go-ap/errors)
|
||||
<!--[![Codacy Badge](https://api.codacy.com/project/badge/Grade/29664f7ae6c643bca76700143e912cd3)](https://www.codacy.com/app/go-ap/errors/dashboard)-->
|
|
@ -0,0 +1,82 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"github.com/valyala/fastjson"
|
||||
)
|
||||
|
||||
func UnmarshalJSON(data []byte) ([]Error, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
p := fastjson.Parser{}
|
||||
val, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v := val.Get("errors")
|
||||
if v == nil {
|
||||
return nil, wrap(nil, "invalid errors array")
|
||||
}
|
||||
items := make([]Error, 0)
|
||||
switch v.Type() {
|
||||
case fastjson.TypeArray:
|
||||
for _, v := range v.GetArray() {
|
||||
status := v.GetInt("status")
|
||||
localErr := errorFromStatus(status)
|
||||
if err := localErr.UnmarshalJSON([]byte(v.String())); err == nil {
|
||||
items = append(items, localErr)
|
||||
}
|
||||
}
|
||||
return items, err
|
||||
case fastjson.TypeObject:
|
||||
status := v.GetInt("status")
|
||||
localErr := errorFromStatus(status)
|
||||
if err := localErr.UnmarshalJSON(data); err == nil {
|
||||
items = append(items, localErr)
|
||||
}
|
||||
case fastjson.TypeString:
|
||||
it := new(Err)
|
||||
it.m = string(data)
|
||||
items = append(items, it)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (e *Err) UnmarshalJSON(data []byte) error {
|
||||
if m := fastjson.GetString(data, "message"); len(m) > 0 {
|
||||
e.m = m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *notFound) UnmarshalJSON(data []byte) error {
|
||||
return n.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (m *methodNotAllowed) UnmarshalJSON(data []byte) error {
|
||||
return m.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (n *notValid) UnmarshalJSON(data []byte) error {
|
||||
return n.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (f *forbidden) UnmarshalJSON(data []byte) error {
|
||||
return f.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (n *notImplemented) UnmarshalJSON(data []byte) error {
|
||||
return n.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (b *badRequest) UnmarshalJSON(data []byte) error {
|
||||
return b.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (u *unauthorized) UnmarshalJSON(data []byte) error {
|
||||
return u.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (n *notSupported) UnmarshalJSON(data []byte) error {
|
||||
return n.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (t *timeout) UnmarshalJSON(data []byte) error {
|
||||
return t.Err.UnmarshalJSON(data)
|
||||
}
|
||||
func (b *badGateway) UnmarshalJSON(data []byte) error {
|
||||
return b.Err.UnmarshalJSON(data)
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Export a number of functions or variables from package errors.
|
||||
var (
|
||||
As = errors.As
|
||||
Is = errors.Is
|
||||
Unwrap = errors.Unwrap
|
||||
)
|
||||
|
||||
// IncludeBacktrace is a static variable that decides if when creating an error we store the backtrace with it.
|
||||
var IncludeBacktrace = true
|
||||
|
||||
// Err is our custom error type that can store backtrace, file and line number
|
||||
type Err struct {
|
||||
m string
|
||||
c error
|
||||
t stack
|
||||
}
|
||||
|
||||
func (e Err) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
io.WriteString(s, e.m)
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
if e.c != nil {
|
||||
io.WriteString(s, ": ")
|
||||
io.WriteString(s, fmt.Sprintf("%+s", e.c))
|
||||
}
|
||||
}
|
||||
case 'v':
|
||||
e.Format(s, 's')
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
if e.t != nil {
|
||||
io.WriteString(s, "\n\t")
|
||||
e.t.Format(s, 'v')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e Err) Error() string {
|
||||
if IncludeBacktrace {
|
||||
return e.m
|
||||
}
|
||||
s := strings.Builder{}
|
||||
s.WriteString(e.m)
|
||||
if ch := errors.Unwrap(e); ch != nil {
|
||||
s.WriteString(": ")
|
||||
s.WriteString(ch.Error())
|
||||
}
|
||||
return s.String()
|
||||
}
|
||||
|
||||
// Unwrap implements the errors.Wrapper interface
|
||||
func (e Err) Unwrap() error {
|
||||
return e.c
|
||||
}
|
||||
|
||||
// StackTrace returns the stack trace as returned by the debug.Stack function
|
||||
func (e Err) StackTrace() StackTrace {
|
||||
return e.t.StackTrace()
|
||||
}
|
||||
|
||||
// Annotatef wraps an error with new message
|
||||
func Annotatef(e error, s string, args ...interface{}) *Err {
|
||||
err := wrap(e, s, args...)
|
||||
return &err
|
||||
}
|
||||
|
||||
// Newf creaates a new error
|
||||
func Newf(s string, args ...interface{}) *Err {
|
||||
err := wrap(nil, s, args...)
|
||||
return &err
|
||||
}
|
||||
|
||||
// Errorf is an alias for Newf
|
||||
func Errorf(s string, args ...interface{}) error {
|
||||
err := wrap(nil, s, args...)
|
||||
return &err
|
||||
}
|
||||
|
||||
// As implements support for errors.As
|
||||
func (e *Err) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **Err:
|
||||
*x = e
|
||||
case *Err:
|
||||
*x = *e
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type StackTracer interface {
|
||||
StackTrace() StackTrace
|
||||
}
|
||||
|
||||
// ancestorOfCause returns true if the caller looks to be an ancestor of the given stack
|
||||
// trace. We check this by seeing whether our stack prefix-matches the cause stack, which
|
||||
// should imply the error was generated directly from our goroutine.
|
||||
func ancestorOfCause(ourStack stack, causeStack StackTrace) bool {
|
||||
// Stack traces are ordered such that the deepest frame is first. We'll want to check
|
||||
// for prefix matching in reverse.
|
||||
//
|
||||
// As an example, imagine we have a prefix-matching stack for ourselves:
|
||||
// [
|
||||
// "github.com/go-ap/processing/processing.Validate",
|
||||
// "testing.tRunner",
|
||||
// "runtime.goexit"
|
||||
// ]
|
||||
//
|
||||
// We'll want to compare this against an error cause that will have happened further
|
||||
// down the stack. An example stack trace from such an error might be:
|
||||
// [
|
||||
// "github.com/go-ap/errors/errors.New",
|
||||
// "testing.tRunner",
|
||||
// "runtime.goexit"
|
||||
// ]
|
||||
//
|
||||
// Their prefix matches, but we'll have to handle the match carefully as we need to match
|
||||
// from back to forward.
|
||||
|
||||
// We can't possibly prefix match if our stack is larger than the cause stack.
|
||||
if len(ourStack) > len(causeStack) {
|
||||
return false
|
||||
}
|
||||
|
||||
// We know the sizes are compatible, so compare program counters from back to front.
|
||||
for idx := 0; idx < len(ourStack); idx++ {
|
||||
if ourStack[len(ourStack)-1] != (uintptr)(causeStack[len(causeStack)-1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func wrap(e error, s string, args ...interface{}) Err {
|
||||
err := Err{
|
||||
c: e,
|
||||
m: fmt.Sprintf(s, args...),
|
||||
}
|
||||
if IncludeBacktrace {
|
||||
causeStackTracer := new(StackTracer)
|
||||
// If our cause has set a stack trace, and that trace is a child of our own function
|
||||
// as inferred by prefix matching our current program counter stack, then we only want
|
||||
// to decorate the error message rather than add a redundant stack trace.
|
||||
stack := callers(2)
|
||||
if !(As(e, causeStackTracer) && ancestorOfCause(*stack, (*causeStackTracer).StackTrace())) {
|
||||
err.t = *stack
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,944 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-ap/jsonld"
|
||||
)
|
||||
|
||||
const errorsPackageName = "github.com/go-ap/errors"
|
||||
const runtimeDebugPackageName = "runtime/debug"
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
json.Unmarshaler
|
||||
}
|
||||
|
||||
type notFound struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type methodNotAllowed struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type notValid struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type forbidden struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type notImplemented struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type conflict struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type gone struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type badRequest struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type unauthorized struct {
|
||||
Err
|
||||
s int
|
||||
challenge string
|
||||
}
|
||||
|
||||
type notSupported struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type timeout struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
type badGateway struct {
|
||||
Err
|
||||
s int
|
||||
}
|
||||
|
||||
func wrapErr(err error, s string, args ...interface{}) Err {
|
||||
e := Annotatef(err, s, args...)
|
||||
asErr := Err{}
|
||||
As(e, &asErr)
|
||||
return asErr
|
||||
}
|
||||
|
||||
func FromResponse(resp *http.Response) error {
|
||||
if resp.StatusCode < http.StatusBadRequest {
|
||||
return nil
|
||||
}
|
||||
body := make([]byte, 0)
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ = ioutil.ReadAll(resp.Body)
|
||||
|
||||
var withStatus error
|
||||
errors, err := UnmarshalJSON(body)
|
||||
if err != nil {
|
||||
return AnnotateFromStatus(nil, resp.StatusCode, string(body))
|
||||
}
|
||||
for _, err := range errors {
|
||||
if err == nil {
|
||||
withStatus = err
|
||||
}
|
||||
withStatus = Annotatef(err, err.Error())
|
||||
}
|
||||
return AnnotateFromStatus(withStatus, resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
func AnnotateFromStatus(err error, status int, s string, args ...interface{}) error {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return NewBadRequest(err, s, args...)
|
||||
case http.StatusUnauthorized:
|
||||
return NewUnauthorized(err, s, args...)
|
||||
// http.StatusPaymentRequired
|
||||
case http.StatusForbidden:
|
||||
return NewForbidden(err, s, args...)
|
||||
case http.StatusNotFound:
|
||||
return NewNotFound(err, s, args...)
|
||||
case http.StatusMethodNotAllowed:
|
||||
return NewMethodNotAllowed(err, s, args...)
|
||||
case http.StatusNotAcceptable:
|
||||
return NewNotValid(err, s, args...)
|
||||
// case http.StatusProxyAuthRequired
|
||||
// case http.StatusRequestTimeout
|
||||
case http.StatusConflict:
|
||||
return NewConflict(err, s, args...)
|
||||
case http.StatusGone:
|
||||
return NewGone(err, s, args...)
|
||||
// case http.StatusLengthRequres
|
||||
// case http.StatusPreconditionFailed
|
||||
// case http.StatusRequestEntityTooLarge
|
||||
// case http.StatusRequestURITooLong
|
||||
// TODO(marius): http.StatusUnsupportedMediaType
|
||||
// case http.StatusRequestedRangeNotSatisfiable
|
||||
// case http.StatusExpectationFailed
|
||||
// case http.StatusTeapot
|
||||
// case http.StatusMisdirectedRequest
|
||||
// case http.StatusUnprocessableEntity
|
||||
// case http.StatusLocked
|
||||
// case http.StatusFailedDependency
|
||||
// case http.StatusTooEarly
|
||||
// case http.StatusTooManyRequests
|
||||
// case http.StatusRequestHeaderFieldsTooLarge
|
||||
// case http.StatusUnavailableForLegalReason
|
||||
// case http.StatusInternalServerError
|
||||
case http.StatusNotImplemented:
|
||||
return NewNotImplemented(err, s, args...)
|
||||
case http.StatusBadGateway:
|
||||
return NewBadGateway(err, s, args...)
|
||||
// case http.StatusServiceUnavailable
|
||||
// case http.StatusGatewayTimeout
|
||||
case http.StatusHTTPVersionNotSupported:
|
||||
return NewNotSupported(err, s, args...)
|
||||
case http.StatusGatewayTimeout:
|
||||
return NewTimeout(err, s, args...)
|
||||
}
|
||||
return Annotatef(err, s, args...)
|
||||
}
|
||||
|
||||
func NewFromStatus(status int, s string, args ...interface{}) error {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return BadRequestf(s, args...)
|
||||
case http.StatusUnauthorized:
|
||||
return Unauthorizedf(s, args...)
|
||||
// http.StatusPaymentRequired
|
||||
case http.StatusForbidden:
|
||||
return Forbiddenf(s, args...)
|
||||
case http.StatusNotFound:
|
||||
return NotFoundf(s, args...)
|
||||
case http.StatusMethodNotAllowed:
|
||||
return MethodNotAllowedf(s, args...)
|
||||
case http.StatusNotAcceptable:
|
||||
return NotValidf(s, args...)
|
||||
// case http.StatusProxyAuthRequired
|
||||
// case http.StatusRequestTimeout
|
||||
case http.StatusConflict:
|
||||
return Conflictf(s, args...)
|
||||
case http.StatusGone:
|
||||
return Gonef(s, args...)
|
||||
// case http.StatusLengthRequres
|
||||
// case http.StatusPreconditionFailed
|
||||
// case http.StatusRequestEntityTooLarge
|
||||
// case http.StatusRequestURITooLong
|
||||
// TODO(marius): http.StatusUnsupportedMediaType
|
||||
// case http.StatusRequestedRangeNotSatisfiable
|
||||
// case http.StatusExpectationFailed
|
||||
// case http.StatusTeapot
|
||||
// case http.StatusMisdirectedRequest
|
||||
// case http.StatusUnprocessableEntity
|
||||
// case http.StatusLocked
|
||||
// case http.StatusFailedDependency
|
||||
// case http.StatusTooEarly
|
||||
// case http.StatusTooManyRequests
|
||||
// case http.StatusRequestHeaderFieldsTooLarge
|
||||
// case http.StatusUnavailableForLegalReason
|
||||
// case http.StatusInternalServerError
|
||||
case http.StatusNotImplemented:
|
||||
return NotImplementedf(s, args...)
|
||||
case http.StatusBadGateway:
|
||||
return BadGatewayf(s, args...)
|
||||
// case http.StatusServiceUnavailable
|
||||
// case http.StatusGatewayTimeout
|
||||
case http.StatusHTTPVersionNotSupported:
|
||||
return NotSupportedf(s, args...)
|
||||
case http.StatusGatewayTimeout:
|
||||
return Timeoutf(s, args...)
|
||||
}
|
||||
return Newf(s, args...)
|
||||
}
|
||||
|
||||
func WrapWithStatus(status int, err error, s string, args ...interface{}) error {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return NewBadRequest(err, s, args...)
|
||||
case http.StatusUnauthorized:
|
||||
return NewUnauthorized(err, s, args...)
|
||||
// http.StatusPaymentRequired
|
||||
case http.StatusForbidden:
|
||||
return NewForbidden(err, s, args...)
|
||||
case http.StatusNotFound:
|
||||
return NewNotFound(err, s, args...)
|
||||
case http.StatusMethodNotAllowed:
|
||||
return NewMethodNotAllowed(err, s, args...)
|
||||
case http.StatusNotAcceptable:
|
||||
return NewNotValid(err, s, args...)
|
||||
// case http.StatusProxyAuthRequired
|
||||
// case http.StatusRequestTimeout
|
||||
case http.StatusConflict:
|
||||
return NewConflict(err, s, args...)
|
||||
case http.StatusGone:
|
||||
return NewGone(err, s, args...)
|
||||
// case http.StatusLengthRequres
|
||||
// case http.StatusPreconditionFailed
|
||||
// case http.StatusRequestEntityTooLarge
|
||||
// case http.StatusRequestURITooLong
|
||||
// TODO(marius): http.StatusUnsupportedMediaType
|
||||
// case http.StatusRequestedRangeNotSatisfiable
|
||||
// case http.StatusExpectationFailed
|
||||
// case http.StatusTeapot
|
||||
// case http.StatusMisdirectedRequest
|
||||
// case http.StatusUnprocessableEntity
|
||||
// case http.StatusLocked
|
||||
// case http.StatusFailedDependency
|
||||
// case http.StatusTooEarly
|
||||
// case http.StatusTooManyRequests
|
||||
// case http.StatusRequestHeaderFieldsTooLarge
|
||||
// case http.StatusUnavailableForLegalReason
|
||||
// case http.StatusInternalServerError
|
||||
case http.StatusNotImplemented:
|
||||
return NewNotImplemented(err, s, args...)
|
||||
case http.StatusBadGateway:
|
||||
return NewBadGateway(err, s, args...)
|
||||
// case http.StatusServiceUnavailable
|
||||
// case http.StatusGatewayTimeout
|
||||
case http.StatusHTTPVersionNotSupported:
|
||||
return NewNotSupported(err, s, args...)
|
||||
case http.StatusGatewayTimeout:
|
||||
return NewTimeout(err, s, args...)
|
||||
}
|
||||
return wrapErr(err, s, args...)
|
||||
}
|
||||
func NotFoundf(s string, args ...interface{}) *notFound {
|
||||
return ¬Found{Err: wrapErr(nil, s, args...), s: http.StatusNotFound}
|
||||
}
|
||||
func NewNotFound(e error, s string, args ...interface{}) *notFound {
|
||||
return ¬Found{Err: wrapErr(e, s, args...), s: http.StatusNotFound}
|
||||
}
|
||||
func MethodNotAllowedf(s string, args ...interface{}) *methodNotAllowed {
|
||||
return &methodNotAllowed{Err: wrapErr(nil, s, args...), s: http.StatusMethodNotAllowed}
|
||||
}
|
||||
func NewMethodNotAllowed(e error, s string, args ...interface{}) *methodNotAllowed {
|
||||
return &methodNotAllowed{Err: wrapErr(e, s, args...), s: http.StatusMethodNotAllowed}
|
||||
}
|
||||
func NotValidf(s string, args ...interface{}) *notValid {
|
||||
return ¬Valid{Err: wrapErr(nil, s, args...)}
|
||||
}
|
||||
func NewNotValid(e error, s string, args ...interface{}) *notValid {
|
||||
return ¬Valid{Err: wrapErr(e, s, args...)}
|
||||
}
|
||||
func Conflictf(s string, args ...interface{}) *conflict {
|
||||
return &conflict{Err: wrapErr(nil, s, args...), s: http.StatusConflict}
|
||||
}
|
||||
func NewConflict(e error, s string, args ...interface{}) *conflict {
|
||||
return &conflict{Err: wrapErr(e, s, args...), s: http.StatusConflict}
|
||||
}
|
||||
func Gonef(s string, args ...interface{}) *gone {
|
||||
return &gone{Err: wrapErr(nil, s, args...), s: http.StatusGone}
|
||||
}
|
||||
func NewGone(e error, s string, args ...interface{}) *gone {
|
||||
return &gone{Err: wrapErr(e, s, args...), s: http.StatusGone}
|
||||
}
|
||||
func Forbiddenf(s string, args ...interface{}) *forbidden {
|
||||
return &forbidden{Err: wrapErr(nil, s, args...), s: http.StatusForbidden}
|
||||
}
|
||||
func NewForbidden(e error, s string, args ...interface{}) *forbidden {
|
||||
return &forbidden{Err: wrapErr(e, s, args...), s: http.StatusForbidden}
|
||||
}
|
||||
func NotImplementedf(s string, args ...interface{}) *notImplemented {
|
||||
return ¬Implemented{Err: wrapErr(nil, s, args...), s: http.StatusNotImplemented}
|
||||
}
|
||||
func NewNotImplemented(e error, s string, args ...interface{}) *notImplemented {
|
||||
return ¬Implemented{Err: wrapErr(e, s, args...), s: http.StatusNotImplemented}
|
||||
}
|
||||
func BadRequestf(s string, args ...interface{}) *badRequest {
|
||||
return &badRequest{Err: wrapErr(nil, s, args...), s: http.StatusBadRequest}
|
||||
}
|
||||
func NewBadRequest(e error, s string, args ...interface{}) *badRequest {
|
||||
return &badRequest{Err: wrapErr(e, s, args...), s: http.StatusBadRequest}
|
||||
}
|
||||
func Unauthorizedf(s string, args ...interface{}) *unauthorized {
|
||||
return &unauthorized{Err: wrapErr(nil, s, args...), s: http.StatusUnauthorized}
|
||||
}
|
||||
func NewUnauthorized(e error, s string, args ...interface{}) *unauthorized {
|
||||
return &unauthorized{Err: wrapErr(e, s, args...), s: http.StatusUnauthorized}
|
||||
}
|
||||
func NotSupportedf(s string, args ...interface{}) *notSupported {
|
||||
return ¬Supported{Err: wrapErr(nil, s, args...), s: http.StatusHTTPVersionNotSupported}
|
||||
}
|
||||
func NewNotSupported(e error, s string, args ...interface{}) *notSupported {
|
||||
return ¬Supported{Err: wrapErr(e, s, args...), s: http.StatusHTTPVersionNotSupported}
|
||||
}
|
||||
func Timeoutf(s string, args ...interface{}) *timeout {
|
||||
return &timeout{Err: wrapErr(nil, s, args...), s: http.StatusRequestTimeout}
|
||||
}
|
||||
func NewTimeout(e error, s string, args ...interface{}) *timeout {
|
||||
return &timeout{Err: wrapErr(e, s, args...), s: http.StatusRequestTimeout}
|
||||
}
|
||||
func BadGatewayf(s string, args ...interface{}) *badGateway {
|
||||
return &badGateway{Err: wrapErr(nil, s, args...), s: http.StatusBadGateway}
|
||||
}
|
||||
func NewBadGateway(e error, s string, args ...interface{}) *badGateway {
|
||||
return &badGateway{Err: wrapErr(e, s, args...), s: http.StatusBadGateway}
|
||||
}
|
||||
func IsBadRequest(e error) bool {
|
||||
_, okp := e.(*badRequest)
|
||||
_, oks := e.(badRequest)
|
||||
return okp || oks || As(e, &badRequest{})
|
||||
}
|
||||
func IsForbidden(e error) bool {
|
||||
_, okp := e.(*forbidden)
|
||||
_, oks := e.(forbidden)
|
||||
return okp || oks || As(e, &forbidden{})
|
||||
}
|
||||
func IsNotSupported(e error) bool {
|
||||
_, okp := e.(*notSupported)
|
||||
_, oks := e.(notSupported)
|
||||
return okp || oks
|
||||
}
|
||||
func IsConflict(e error) bool {
|
||||
_, okp := e.(*conflict)
|
||||
_, oks := e.(conflict)
|
||||
return okp || oks || As(e, &conflict{})
|
||||
}
|
||||
func IsGone(e error) bool {
|
||||
_, okp := e.(*gone)
|
||||
_, oks := e.(gone)
|
||||
return okp || oks || As(e, &gone{})
|
||||
}
|
||||
func IsMethodNotAllowed(e error) bool {
|
||||
_, okp := e.(*methodNotAllowed)
|
||||
_, oks := e.(methodNotAllowed)
|
||||
return okp || oks || As(e, &methodNotAllowed{})
|
||||
}
|
||||
func IsNotFound(e error) bool {
|
||||
_, okp := e.(*notFound)
|
||||
_, oks := e.(notFound)
|
||||
return okp || oks || As(e, ¬Found{})
|
||||
}
|
||||
func IsNotImplemented(e error) bool {
|
||||
_, okp := e.(*notImplemented)
|
||||
_, oks := e.(notImplemented)
|
||||
return okp || oks || As(e, ¬Implemented{})
|
||||
}
|
||||
func IsUnauthorized(e error) bool {
|
||||
_, okp := e.(*unauthorized)
|
||||
_, oks := e.(unauthorized)
|
||||
return okp || oks || As(e, &unauthorized{})
|
||||
}
|
||||
func IsTimeout(e error) bool {
|
||||
_, okp := e.(*timeout)
|
||||
_, oks := e.(timeout)
|
||||
return okp || oks || As(e, &timeout{})
|
||||
}
|
||||
func IsNotValid(e error) bool {
|
||||
_, okp := e.(*notValid)
|
||||
_, oks := e.(notValid)
|
||||
return okp || oks || As(e, ¬Valid{})
|
||||
}
|
||||
|
||||
func IsBadGateway(e error) bool {
|
||||
_, okp := e.(*badGateway)
|
||||
_, oks := e.(badGateway)
|
||||
return okp || oks || As(e, &badGateway{})
|
||||
}
|
||||
func (n notFound) Is(e error) bool {
|
||||
return IsNotFound(e)
|
||||
}
|
||||
func (n notValid) Is(e error) bool {
|
||||
return IsNotValid(e)
|
||||
}
|
||||
func (n notImplemented) Is(e error) bool {
|
||||
return IsNotImplemented(e)
|
||||
}
|
||||
func (n notSupported) Is(e error) bool {
|
||||
return IsNotSupported(e)
|
||||
}
|
||||
func (b badRequest) Is(e error) bool {
|
||||
return IsBadRequest(e)
|
||||
}
|
||||
func (t timeout) Is(e error) bool {
|
||||
return IsTimeout(e)
|
||||
}
|
||||
func (u unauthorized) Is(e error) bool {
|
||||
return IsUnauthorized(e)
|
||||
}
|
||||
func (m methodNotAllowed) Is(e error) bool {
|
||||
return IsMethodNotAllowed(e)
|
||||
}
|
||||
func (f forbidden) Is(e error) bool {
|
||||
return IsForbidden(e)
|
||||
}
|
||||
func (b badGateway) Is(e error) bool {
|
||||
return IsBadGateway(e)
|
||||
}
|
||||
func (g gone) Is(e error) bool {
|
||||
return IsGone(e)
|
||||
}
|
||||
func (c conflict) Is(e error) bool {
|
||||
return IsConflict(e)
|
||||
}
|
||||
func (n notFound) Unwrap() error {
|
||||
return n.Err.Unwrap()
|
||||
}
|
||||
func (n notValid) Unwrap() error {
|
||||
return n.Err.Unwrap()
|
||||
}
|
||||
func (n notImplemented) Unwrap() error {
|
||||
return n.Err.Unwrap()
|
||||
}
|
||||
func (n notSupported) Unwrap() error {
|
||||
return n.Err.Unwrap()
|
||||
}
|
||||
func (b badRequest) Unwrap() error {
|
||||
return b.Err.Unwrap()
|
||||
}
|
||||
func (t timeout) Unwrap() error {
|
||||
return t.Err.Unwrap()
|
||||
}
|
||||
func (u unauthorized) Unwrap() error {
|
||||
return u.Err.Unwrap()
|
||||
}
|
||||
func (m methodNotAllowed) Unwrap() error {
|
||||
return m.Err.Unwrap()
|
||||
}
|
||||
func (f forbidden) Unwrap() error {
|
||||
return f.Err.Unwrap()
|
||||
}
|
||||
func (b badGateway) Unwrap() error {
|
||||
return b.Err.Unwrap()
|
||||
}
|
||||
func (g gone) Unwrap() error {
|
||||
return g.Err.Unwrap()
|
||||
}
|
||||
func (c conflict) Unwrap() error {
|
||||
return c.Err.Unwrap()
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a notFound to its underlying type Err.
|
||||
func (n *notFound) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **notFound:
|
||||
*x = n
|
||||
case *notFound:
|
||||
*x = *n
|
||||
case *Err:
|
||||
return n.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a notValid to its underlying type Err.
|
||||
func (n *notValid) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **notValid:
|
||||
*x = n
|
||||
case *notValid:
|
||||
*x = *n
|
||||
case *Err:
|
||||
return n.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a notImplemented to its underlying type Err.
|
||||
func (n *notImplemented) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **notImplemented:
|
||||
*x = n
|
||||
case *notImplemented:
|
||||
*x = *n
|
||||
case *Err:
|
||||
return n.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a notSupported to its underlying type Err.
|
||||
func (n *notSupported) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **notSupported:
|
||||
*x = n
|
||||
case *notSupported:
|
||||
*x = *n
|
||||
case *Err:
|
||||
return n.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a badRequest to its underlying type Err.
|
||||
func (b *badRequest) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **badRequest:
|
||||
*x = b
|
||||
case *badRequest:
|
||||
*x = *b
|
||||
case *Err:
|
||||
return b.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a timeout to its underlying type Err.
|
||||
func (t *timeout) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **timeout:
|
||||
*x = t
|
||||
case *timeout:
|
||||
*x = *t
|
||||
case *Err:
|
||||
return t.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a unauthorized to its underlying type Err.
|
||||
func (u *unauthorized) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **unauthorized:
|
||||
*x = u
|
||||
case *unauthorized:
|
||||
*x = *u
|
||||
case *Err:
|
||||
return u.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a methodNotAllowed to its underlying type Err.
|
||||
func (m *methodNotAllowed) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **methodNotAllowed:
|
||||
*x = m
|
||||
case *methodNotAllowed:
|
||||
*x = *m
|
||||
case *Err:
|
||||
return m.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a forbidden to its underlying type Err.
|
||||
func (f *forbidden) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **forbidden:
|
||||
*x = f
|
||||
case *forbidden:
|
||||
*x = *f
|
||||
case *Err:
|
||||
return f.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a badGateway to its underlying type Err.
|
||||
func (b *badGateway) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **badGateway:
|
||||
*x = b
|
||||
case *badGateway:
|
||||
*x = *b
|
||||
case *Err:
|
||||
return b.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a gone error to its underlying type Err.
|
||||
func (g *gone) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **gone:
|
||||
*x = g
|
||||
case *gone:
|
||||
*x = *g
|
||||
case *Err:
|
||||
return g.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// As is used by the errors.As() function to coerce the method's parameter to the one of the receiver
|
||||
//
|
||||
// if the underlying logic of the receiver's type can understand it.
|
||||
//
|
||||
// In this case we're converting a conflict error to its underlying type Err.
|
||||
func (c *conflict) As(err interface{}) bool {
|
||||
switch x := err.(type) {
|
||||
case **conflict:
|
||||
*x = c
|
||||
case *conflict:
|
||||
*x = *c
|
||||
case *Err:
|
||||
return c.Err.As(x)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Challenge adds a challenge token to be added to the HTTP response
|
||||
func (u *unauthorized) Challenge(c string) *unauthorized {
|
||||
u.challenge = c
|
||||
return u
|
||||
}
|
||||
|
||||
// Challenge returns the challenge of the err parameter if it's an unauthorized type error
|
||||
func Challenge(err error) string {
|
||||
un := unauthorized{}
|
||||
if ok := As(err, &un); ok {
|
||||
return un.challenge
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ErrorHandlerFn
|
||||
type ErrorHandlerFn func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
// ServeHTTP implements the http.Handler interface for the ItemHandlerFn type
|
||||
func (h ErrorHandlerFn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var dat []byte
|
||||
var status int
|
||||
|
||||
if err := h(w, r); err != nil {
|
||||
if status, dat = RenderErrors(r, err); status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
w.Write(dat)
|
||||
}
|
||||
|
||||
// HandleError is a generic method to return an HTTP handler that passes an error up the chain
|
||||
func HandleError(e error) ErrorHandlerFn {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound is a generic method to return an 404 error HTTP handler that
|
||||
var NotFound = ErrorHandlerFn(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return NotFoundf("%s not found", r.URL.Path)
|
||||
})
|
||||
|
||||
type Http struct {
|
||||
Code int `jsonld:"status,omitempty"`
|
||||
Message string `jsonld:"message"`
|
||||
Trace StackTrace `jsonld:"trace,omitempty"`
|
||||
}
|
||||
|
||||
func HttpErrors(err error) []Http {
|
||||
https := make([]Http, 0)
|
||||
|
||||
load := func(err error) Http {
|
||||
var trace StackTrace
|
||||
var msg string
|
||||
switch e := err.(type) {
|
||||
case *Err:
|
||||
msg = e.Error()
|
||||
if IncludeBacktrace {
|
||||
trace = e.StackTrace()
|
||||
}
|
||||
default:
|
||||
local := new(Err)
|
||||
if ok := As(err, local); ok {
|
||||
if IncludeBacktrace {
|
||||
trace = local.StackTrace()
|
||||
}
|
||||
}
|
||||
msg = err.Error()
|
||||
}
|
||||
|
||||
return Http{
|
||||
Message: msg,
|
||||
Trace: trace,
|
||||
Code: HttpStatus(err),
|
||||
}
|
||||
}
|
||||
https = append(https, load(err))
|
||||
for {
|
||||
if err = Unwrap(err); err != nil {
|
||||
https = append(https, load(err))
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return https
|
||||
}
|
||||
|
||||
func HttpStatus(e error) int {
|
||||
if IsBadRequest(e) {
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
if IsUnauthorized(e) {
|
||||
return http.StatusUnauthorized
|
||||
}
|
||||
// http.StatusPaymentRequired
|
||||
if IsForbidden(e) {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
if IsNotFound(e) {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
if IsMethodNotAllowed(e) {
|
||||
return http.StatusMethodNotAllowed
|
||||
}
|
||||
if IsNotValid(e) {
|
||||
return http.StatusNotAcceptable
|
||||
}
|
||||
// http.StatusProxyAuthRequired
|
||||
// http.StatusRequestTimeout
|
||||
if IsConflict(e) {
|
||||
return http.StatusConflict
|
||||
}
|
||||
if IsGone(e) {
|
||||
return http.StatusGone
|
||||
}
|
||||
// TODO(marius): http.StatusGone
|
||||
// http.StatusLengthRequires
|
||||
// http.StatusPreconditionFailed
|
||||
// http.StatusRequestEntityTooLarge
|
||||
// http.StatusRequestURITooLong
|
||||
// TODO(marius): http.StatusUnsupportedMediaType
|
||||
// http.StatusRequestedRangeNotSatisfiable
|
||||
// http.StatusExpectationFailed
|
||||
// http.StatusTeapot
|
||||
// http.StatusMisdirectedRequest
|
||||
// http.StatusUnprocessableEntity
|
||||
// http.StatusLocked
|
||||
// http.StatusFailedDependency
|
||||
// http.StatusTooEarly
|
||||
// http.StatusTooManyRequests
|
||||
// http.StatusRequestHeaderFieldsTooLarge
|
||||
// http.StatusUnavailableForLegalReason
|
||||
|
||||
// http.StatusInternalServerError
|
||||
if IsNotImplemented(e) {
|
||||
return http.StatusNotImplemented
|
||||
}
|
||||
if IsBadGateway(e) {
|
||||
return http.StatusBadGateway
|
||||
}
|
||||
// http.StatusServiceUnavailable
|
||||
// http.StatusGatewayTimeout
|
||||
if IsNotSupported(e) {
|
||||
return http.StatusHTTPVersionNotSupported
|
||||
}
|
||||
|
||||
if IsTimeout(e) {
|
||||
return http.StatusGatewayTimeout
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func errorFromStatus(status int) Error {
|
||||
switch status {
|
||||
case http.StatusBadRequest:
|
||||
return new(badRequest)
|
||||
case http.StatusUnauthorized:
|
||||
return new(unauthorized)
|
||||
// case http.StatusPaymentRequired:
|
||||
case http.StatusForbidden:
|
||||
return new(forbidden)
|
||||
case http.StatusNotFound:
|
||||
return new(notFound)
|
||||
case http.StatusMethodNotAllowed:
|
||||
return new(methodNotAllowed)
|
||||
case http.StatusNotAcceptable:
|
||||
return new(notValid)
|
||||
// case http.StatusProxyAuthRequired:
|
||||
// case http.StatusRequestTimeout:
|
||||
case http.StatusConflict:
|
||||
return new(conflict)
|
||||
// case http.StatusGone: // TODO(marius):
|
||||
// case http.StatusLengthRequres:
|
||||
// case http.StatusPreconditionFailed:
|
||||
// case http.StatusRequestEntityTooLarge:
|
||||
// case http.StatusRequestURITooLong:
|
||||
// case http.StatusUnsupportedMediaType: // TODO(marius):
|
||||
// case http.StatusRequestedRangeNotSatisfiable:
|
||||
// case http.StatusExpectationFailed:
|
||||
// case http.StatusTeapot:
|
||||
// case http.StatusMisdirectedRequest:
|
||||
// case http.StatusUnprocessableEntity:
|
||||
// case http.StatusLocked:
|
||||
// case http.StatusFailedDependency:
|
||||
// case http.StatusTooEarly:
|
||||
// case http.StatusTooManyRequests:
|
||||
// case http.StatusRequestHeaderFieldsTooLarge:
|
||||
// case http.StatusUnavailableForLegalReason:
|
||||
// case http.StatusInternalServerError:
|
||||
case http.StatusNotImplemented:
|
||||
return new(notImplemented)
|
||||
case http.StatusBadGateway:
|
||||
return new(badGateway)
|
||||
// case http.StatusServiceUnavailable:
|
||||
case http.StatusHTTPVersionNotSupported:
|
||||
return new(notSupported)
|
||||
case http.StatusGatewayTimeout:
|
||||
return new(badGateway)
|
||||
case http.StatusInternalServerError:
|
||||
fallthrough
|
||||
default:
|
||||
return new(Err)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(marius): get a proper ctxt
|
||||
func ctxt(r *http.Request) jsonld.Context {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
return jsonld.Context{
|
||||
jsonld.ContextElement{
|
||||
Term: "errors",
|
||||
IRI: jsonld.IRI(fmt.Sprintf("%s://%s/ns#errors", scheme, r.Host)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RenderErrors outputs the json encoded errors, with the JsonLD ctxt for current
|
||||
func RenderErrors(r *http.Request, errs ...error) (int, []byte) {
|
||||
errMap := make([]Http, 0)
|
||||
var status int
|
||||
for _, err := range errs {
|
||||
more := HttpErrors(err)
|
||||
errMap = append(errMap, more...)
|
||||
status = HttpStatus(err)
|
||||
}
|
||||
var dat []byte
|
||||
var err error
|
||||
|
||||
m := struct {
|
||||
Errors []Http `jsonld:"errors"`
|
||||
}{Errors: errMap}
|
||||
if dat, err = jsonld.WithContext(ctxt(r)).Marshal(m); err != nil {
|
||||
return http.StatusInternalServerError, dat
|
||||
}
|
||||
return status, dat
|
||||
}
|
|
@ -0,0 +1,219 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Frame represents a program counter inside a stack frame.
|
||||
// For historical reasons if Frame is interpreted as a uintptr
|
||||
// its value represents the program counter + 1.
|
||||
type Frame uintptr
|
||||
|
||||
// pc returns the program counter for this frame;
|
||||
// multiple frames may have the same PC value.
|
||||
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||
|
||||
// file returns the full path to the file that contains the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) file() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
file, _ := fn.FileLine(f.pc())
|
||||
return file
|
||||
}
|
||||
|
||||
// line returns the line number of source code of the
|
||||
// function for this Frame's pc.
|
||||
func (f Frame) line() int {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return 0
|
||||
}
|
||||
_, line := fn.FileLine(f.pc())
|
||||
return line
|
||||
}
|
||||
|
||||
// name returns the name of this function, if known.
|
||||
func (f Frame) name() string {
|
||||
fn := runtime.FuncForPC(f.pc())
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
return fn.Name()
|
||||
}
|
||||
|
||||
// Format formats the frame according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s source file
|
||||
// %d source line
|
||||
// %n function name
|
||||
// %v equivalent to %s:%d
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+s function name and path of source file relative to the compile time
|
||||
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||
// %+v equivalent to %+s:%d
|
||||
func (f Frame) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 's':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
io.WriteString(s, f.name())
|
||||
io.WriteString(s, "\n\t")
|
||||
io.WriteString(s, f.file())
|
||||
default:
|
||||
io.WriteString(s, path.Base(f.file()))
|
||||
}
|
||||
case 'd':
|
||||
io.WriteString(s, strconv.Itoa(f.line()))
|
||||
case 'n':
|
||||
io.WriteString(s, funcname(f.name()))
|
||||
case 'v':
|
||||
f.Format(s, 's')
|
||||
io.WriteString(s, ":")
|
||||
f.Format(s, 'd')
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalText formats a stacktrace Frame as a text string. The output is the
|
||||
// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs.
|
||||
func (f Frame) MarshalText() ([]byte, error) {
|
||||
name := f.name()
|
||||
if name == "unknown" {
|
||||
return []byte(name), nil
|
||||
}
|
||||
return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil
|
||||
}
|
||||
|
||||
func (f Frame) MarshalJSON() ([]byte, error) {
|
||||
name := f.name()
|
||||
w := bytes.NewBuffer(nil)
|
||||
if name == "unknown" {
|
||||
w.WriteByte('"')
|
||||
w.WriteString(name)
|
||||
w.WriteByte('"')
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
w.WriteByte('{')
|
||||
w.WriteString("\"function\": ")
|
||||
w.WriteByte('"')
|
||||
w.WriteString(funcname(name))
|
||||
w.WriteByte('"')
|
||||
w.WriteByte(',')
|
||||
w.WriteString("\"file\": ")
|
||||
w.WriteByte('"')
|
||||
w.WriteString(f.file())
|
||||
w.WriteByte('"')
|
||||
w.WriteByte(',')
|
||||
w.WriteString("\"line\": ")
|
||||
w.WriteString(fmt.Sprintf("%d", f.line()))
|
||||
w.WriteByte('}')
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||
type StackTrace []Frame
|
||||
|
||||
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||
//
|
||||
// %s lists source files for each Frame in the stack
|
||||
// %v lists the source file and line number for each Frame in the stack
|
||||
//
|
||||
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||
//
|
||||
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case s.Flag('+'):
|
||||
for _, f := range st {
|
||||
io.WriteString(s, "\n")
|
||||
f.Format(s, verb)
|
||||
}
|
||||
case s.Flag('#'):
|
||||
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||
default:
|
||||
st.formatSlice(s, verb)
|
||||
}
|
||||
case 's':
|
||||
st.formatSlice(s, verb)
|
||||
}
|
||||
}
|
||||
|
||||
func (st StackTrace) MarshalJSON() ([]byte, error) {
|
||||
w := bytes.NewBuffer(nil)
|
||||
w.WriteByte('[')
|
||||
for i, f := range st {
|
||||
b, _ := f.MarshalJSON()
|
||||
w.Write(b)
|
||||
if i == len(st)-1 {
|
||||
break
|
||||
}
|
||||
w.WriteByte(',')
|
||||
}
|
||||
w.WriteByte(']')
|
||||
return w.Bytes(), nil
|
||||
}
|
||||
|
||||
// formatSlice will format this StackTrace into the given buffer as a slice of
|
||||
// Frame, only valid when called with '%s' or '%v'.
|
||||
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
|
||||
io.WriteString(s, "[")
|
||||
for i, f := range st {
|
||||
if i > 0 {
|
||||
io.WriteString(s, " ")
|
||||
}
|
||||
f.Format(s, verb)
|
||||
}
|
||||
io.WriteString(s, "]")
|
||||
}
|
||||
|
||||
// stack represents a stack of program counters.
|
||||
type stack []uintptr
|
||||
|
||||
func (s *stack) Format(st fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
switch {
|
||||
case st.Flag('+'):
|
||||
for _, pc := range *s {
|
||||
f := Frame(pc)
|
||||
fmt.Fprintf(st, "\n%+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stack) StackTrace() StackTrace {
|
||||
f := make([]Frame, len(*s))
|
||||
for i := 0; i < len(f); i++ {
|
||||
f[i] = Frame((*s)[i])
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
func callers(skip int) *stack {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(skip+3, pcs[:])
|
||||
var st stack = pcs[0:n]
|
||||
return &st
|
||||
}
|
||||
|
||||
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||
func funcname(name string) string {
|
||||
i := strings.LastIndex(name, "/")
|
||||
name = name[i+1:]
|
||||
i = strings.Index(name, ".")
|
||||
return name[i+1:]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
image: archlinux
|
||||
packages:
|
||||
- go
|
||||
- postgresql
|
||||
sources:
|
||||
- https://github.com/go-ap/jsonld
|
||||
environment:
|
||||
GO111MODULE: 'on'
|
||||
tasks:
|
||||
- setup: |
|
||||
cd jsonld && go mod download
|
||||
- tests: |
|
||||
cd jsonld && make test
|
||||
- coverage: |
|
||||
set -a +x
|
||||
cd jsonld && make coverage
|
||||
GIT_SHA=$(git rev-parse --verify HEAD)
|
||||
GIT_BRANCH=$(git name-rev --name-only HEAD)
|
|
@ -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
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017 Marius Orcsik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,18 @@
|
|||
TEST := go test
|
||||
TEST_FLAGS ?= -v
|
||||
TEST_TARGET ?= ./...
|
||||
GO111MODULE=on
|
||||
PROJECT_NAME := $(shell basename $(PWD))
|
||||
|
||||
.PHONY: test coverage clean
|
||||
|
||||
test:
|
||||
$(TEST) $(TEST_FLAGS) $(TEST_TARGET)
|
||||
|
||||
coverage: TEST_TARGET := .
|
||||
coverage: TEST_FLAGS += -covermode=count -coverprofile $(PROJECT_NAME).coverprofile
|
||||
coverage: test
|
||||
|
||||
clean:
|
||||
$(RM) -v *.coverprofile
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# JSON-ld for Go
|
||||
|
||||
[![MIT Licensed](https://img.shields.io/github/license/go-ap/jsonld.svg)](https://raw.githubusercontent.com/go-ap/jsonld/master/LICENSE)
|
||||
[![Build Status](https://builds.sr.ht/~mariusor/jsonld.svg)](https://builds.sr.ht/~mariusor/jsonld)
|
||||
[![Test Coverage](https://img.shields.io/codecov/c/github/go-ap/jsonld.svg)](https://codecov.io/gh/go-ap/jsonld)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/go-ap/jsonld)](https://goreportcard.com/report/github.com/go-ap/jsonld)
|
||||
<!--[![Codacy Badge](https://api.codacy.com/project/badge/Grade/29664f7ae6c643bca76700143e912cd3)](https://www.codacy.com/app/go-ap/jsonld/dashboard)-->
|
||||
|
||||
Basic lib for using [activity pub](https://www.w3.org/TR/activitypub/#Overview) API in Go.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
import "github.com/go-ap/jsonld"
|
||||
```
|
|
@ -0,0 +1,219 @@
|
|||
package jsonld
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// From the JSON-LD spec 3.3
|
||||
// https://www.w3.org/TR/json-ld/#dfn-keyword
|
||||
const (
|
||||
// @context
|
||||
// Used to define the short-hand names that are used throughout a JSON-LD document.
|
||||
// These short-hand names are called terms and help developers to express specific identifiers in a compact manner.
|
||||
// The @context keyword is described in detail in section 5.1 The Context.
|
||||
ContextKw Term = "@context"
|
||||
// @id
|
||||
//Used to uniquely identify things that are being described in the document with IRIs or blank node identifiers.
|
||||
// This keyword is described in section 5.3 Node Identifiers.
|
||||
IdKw Term = "@id"
|
||||
// @value
|
||||
// Used to specify the data that is associated with a particular property in the graph.
|
||||
// This keyword is described in section 6.9 String Internationalization and section 6.4 Typed Values.
|
||||
ValueKw Term = "@value"
|
||||
// @language
|
||||
// Used to specify the language for a particular string value or the default language of a JSON-LD document.
|
||||
// This keyword is described in section 6.9 String Internationalization.
|
||||
LanguageKw Term = "@language"
|
||||
//@type
|
||||
//Used to set the data type of a node or typed value. This keyword is described in section 6.4 Typed Values.
|
||||
TypeKw Term = "@type"
|
||||
// @container
|
||||
// Used to set the default container type for a term. This keyword is described in section 6.11 Sets and Lists.
|
||||
ContainerKw Term = "@container"
|
||||
//@list
|
||||
//Used to express an ordered set of data. This keyword is described in section 6.11 Sets and Lists.
|
||||
ListKw Term = "@list"
|
||||
// @set
|
||||
// Used to express an unordered set of data and to ensure that values are always represented as arrays.
|
||||
// This keyword is described in section 6.11 Sets and Lists.
|
||||
SetKw Term = "@set"
|
||||
// @reverse
|
||||
// Used to express reverse properties. This keyword is described in section 6.12 Reverse Properties.
|
||||
ReverseKw Term = "@reverse"
|
||||
// @index
|
||||
// Used to specify that a container is used to index information and that processing should continue deeper
|
||||
// into a JSON data structure. This keyword is described in section 6.16 Data Indexing.
|
||||
IndexKw Term = "@index"
|
||||
// @base
|
||||
// Used to set the base IRI against which relative IRIs are resolved. T
|
||||
// his keyword is described in section 6.1 Base IRI.
|
||||
BaseKw Term = "@base"
|
||||
// @vocab
|
||||
// Used to expand properties and values in @type with a common prefix IRI.
|
||||
// This keyword is described in section 6.2 Default Vocabulary.
|
||||
VocabKw Term = "@vocab"
|
||||
// @graph
|
||||
// Used to express a graph. This keyword is described in section 6.13 Named Graphs.
|
||||
GraphKw Term = "@graph"
|
||||
)
|
||||
|
||||
// ContentType is the content type of JsonLD documents
|
||||
const ContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
||||
|
||||
type (
|
||||
// Ref basic type
|
||||
LangRef string
|
||||
// Term represents the JSON-LD term for @context maps
|
||||
Term string
|
||||
// IRI is a International Resource Identificator
|
||||
IRI string
|
||||
// Terms is an array of Term values
|
||||
Terms []Term
|
||||
)
|
||||
|
||||
// Nillable
|
||||
type Nillable interface {
|
||||
IsNil() bool
|
||||
}
|
||||
|
||||
type IRILike interface {
|
||||
IsCompact() bool
|
||||
IsAbsolute() bool
|
||||
IsRelative() bool
|
||||
}
|
||||
|
||||
func (i IRI) IsCompact() bool {
|
||||
return !i.IsAbsolute() && strings.Contains(string(i), ":")
|
||||
}
|
||||
func (i IRI) IsAbsolute() bool {
|
||||
return strings.Contains(string(i), "https://")
|
||||
}
|
||||
func (i IRI) IsRelative() bool {
|
||||
return !i.IsAbsolute()
|
||||
}
|
||||
|
||||
var keywords = Terms{
|
||||
BaseKw,
|
||||
ContextKw,
|
||||
ContainerKw,
|
||||
GraphKw,
|
||||
IdKw,
|
||||
IndexKw,
|
||||
LanguageKw,
|
||||
ListKw,
|
||||
ReverseKw,
|
||||
SetKw,
|
||||
TypeKw,
|
||||
ValueKw,
|
||||
VocabKw,
|
||||
}
|
||||
|
||||
const NilTerm Term = "-"
|
||||
const NilLangRef LangRef = "-"
|
||||
|
||||
type ContextObject struct {
|
||||
ID interface{} `jsonld:"@id,omitempty,collapsible"`
|
||||
Type interface{} `jsonld:"@type,omitempty,collapsible"`
|
||||
}
|
||||
|
||||
// Context is of of the basic JSON-LD elements.
|
||||
// It represents an array of ContextElements
|
||||
type Context []ContextElement
|
||||
|
||||
// ContextElement is used to map terms to IRIs or JSON objects.
|
||||
// Terms are case sensitive and any valid string that is not a reserved JSON-LD
|
||||
// keyword can be used as a term.
|
||||
type ContextElement struct {
|
||||
Term Term
|
||||
IRI IRI
|
||||
}
|
||||
|
||||
func GetContext() Context {
|
||||
return Context{}
|
||||
}
|
||||
|
||||
//type Context Collapsible
|
||||
|
||||
// Collapsible is an interface used by the JSON-LD marshaller to collapse a struct to one single value
|
||||
type Collapsible interface {
|
||||
Collapse() interface{}
|
||||
}
|
||||
|
||||
// Collapse returns the plain text collapsed value of the current Context object
|
||||
func (c Context) Collapse() interface{} {
|
||||
if len(c) == 1 && len(c[0].IRI) > 0 {
|
||||
return c[0].IRI
|
||||
}
|
||||
for _, el := range c {
|
||||
if el.Term == NilTerm {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Collapse returns the plain text collapsed value of the current IRI string
|
||||
func (i IRI) Collapse() interface{} {
|
||||
return i
|
||||
}
|
||||
|
||||
// MarshalText basic stringify function
|
||||
func (i IRI) MarshalText() ([]byte, error) {
|
||||
return []byte(i), nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON document represented by the current Context
|
||||
// This should return :
|
||||
// If only one element in the context and the element has no Term -> json marshaled string
|
||||
// If multiple elements in the context without Term -> json marshaled array of strings
|
||||
// If multiple elements where at least one doesn't have a Term and one has a Term -> json marshaled array
|
||||
// If multiple elements where all have Terms -> json marshaled object
|
||||
func (c Context) MarshalJSON() ([]byte, error) {
|
||||
mapIRI := make(map[Term]IRI, 0)
|
||||
arr := make([]interface{}, 0)
|
||||
i := 0
|
||||
if len(c) == 1 && len(c[0].IRI) > 0 {
|
||||
return json.Marshal(c[0].IRI)
|
||||
}
|
||||
for _, el := range c {
|
||||
t := el.Term
|
||||
iri := el.IRI
|
||||
if t.IsNil() {
|
||||
arr = append(arr, iri)
|
||||
i += 1
|
||||
} else {
|
||||
if len(iri) > 0 {
|
||||
mapIRI[t] = iri
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(mapIRI) > 0 {
|
||||
if len(arr) == 0 {
|
||||
return json.Marshal(mapIRI)
|
||||
}
|
||||
arr = append(arr, mapIRI)
|
||||
}
|
||||
return json.Marshal(arr)
|
||||
}
|
||||
|
||||
// UnmarshalJSON tries to load the Context from the incoming json value
|
||||
func (c *Context) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNil returns if current LangRef is equal to empty string or to its nil value
|
||||
func (l LangRef) IsNil() bool {
|
||||
return len(l) == 0 || l == NilLangRef
|
||||
}
|
||||
|
||||
// IsNil returns if current IRI is equal to empty string
|
||||
func (i IRI) IsNil() bool {
|
||||
return len(i) == 0
|
||||
}
|
||||
|
||||
// IsNil returns if current Term is equal to empty string or to its nil value
|
||||
func (i Term) IsNil() bool {
|
||||
return len(i) == 0 || i == NilTerm
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,143 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonld
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
|
||||
kelvin = '\u212a'
|
||||
smallLongEss = '\u017f'
|
||||
)
|
||||
|
||||
// foldFunc returns one of four different case folding equivalence
|
||||
// functions, from most general (and slow) to fastest:
|
||||
//
|
||||
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
|
||||
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
|
||||
// 3) asciiEqualFold, no special, but includes non-letters (including _)
|
||||
// 4) simpleLetterEqualFold, no specials, no non-letters.
|
||||
//
|
||||
// The letters S and K are special because they map to 3 runes, not just 2:
|
||||
// * S maps to s and to U+017F 'ſ' Latin small letter long s
|
||||
// * k maps to K and to U+212A 'K' Kelvin sign
|
||||
// See https://play.golang.org/p/tTxjOc0OGo
|
||||
//
|
||||
// The returned function is specialized for matching against s and
|
||||
// should only be given s. It's not curried for performance reasons.
|
||||
func foldFunc(s []byte) func(s, t []byte) bool {
|
||||
nonLetter := false
|
||||
special := false // special letter
|
||||
for _, b := range s {
|
||||
if b >= utf8.RuneSelf {
|
||||
return bytes.EqualFold
|
||||
}
|
||||
upper := b & caseMask
|
||||
if upper < 'A' || upper > 'Z' {
|
||||
nonLetter = true
|
||||
} else if upper == 'K' || upper == 'S' {
|
||||
// See above for why these letters are special.
|
||||
special = true
|
||||
}
|
||||
}
|
||||
if special {
|
||||
return equalFoldRight
|
||||
}
|
||||
if nonLetter {
|
||||
return asciiEqualFold
|
||||
}
|
||||
return simpleLetterEqualFold
|
||||
}
|
||||
|
||||
// equalFoldRight is a specialization of bytes.EqualFold when s is
|
||||
// known to be all ASCII (including punctuation), but contains an 's',
|
||||
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
|
||||
// See comments on foldFunc.
|
||||
func equalFoldRight(s, t []byte) bool {
|
||||
for _, sb := range s {
|
||||
if len(t) == 0 {
|
||||
return false
|
||||
}
|
||||
tb := t[0]
|
||||
if tb < utf8.RuneSelf {
|
||||
if sb != tb {
|
||||
sbUpper := sb & caseMask
|
||||
if 'A' <= sbUpper && sbUpper <= 'Z' {
|
||||
if sbUpper != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
t = t[1:]
|
||||
continue
|
||||
}
|
||||
// sb is ASCII and t is not. t must be either kelvin
|
||||
// sign or long s; sb must be s, S, k, or K.
|
||||
tr, size := utf8.DecodeRune(t)
|
||||
switch sb {
|
||||
case 's', 'S':
|
||||
if tr != smallLongEss {
|
||||
return false
|
||||
}
|
||||
case 'k', 'K':
|
||||
if tr != kelvin {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
t = t[size:]
|
||||
|
||||
}
|
||||
if len(t) > 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// asciiEqualFold is a specialization of bytes.EqualFold for use when
|
||||
// s is all ASCII (but may contain non-letters) and contains no
|
||||
// special-folding letters.
|
||||
// See comments on foldFunc.
|
||||
func asciiEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, sb := range s {
|
||||
tb := t[i]
|
||||
if sb == tb {
|
||||
continue
|
||||
}
|
||||
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
|
||||
if sb&caseMask != tb&caseMask {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// simpleLetterEqualFold is a specialization of bytes.EqualFold for
|
||||
// use when s is all ASCII letters (no underscores, etc) and also
|
||||
// doesn't contain 'k', 'K', 's', or 'S'.
|
||||
// See comments on foldFunc.
|
||||
func simpleLetterEqualFold(s, t []byte) bool {
|
||||
if len(s) != len(t) {
|
||||
return false
|
||||
}
|
||||
for i, b := range s {
|
||||
if b&caseMask != t[i]&caseMask {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,632 @@
|
|||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonld
|
||||
|
||||
// JSON value parser state machine.
|
||||
// Just about at the limit of what is reasonable to write by hand.
|
||||
// Some parts are a bit tedious, but overall it nicely factors out the
|
||||
// otherwise common code from the multiple scanning functions
|
||||
// in this package (Compact, Indent, checkValid, nextValue, etc).
|
||||
//
|
||||
// This file starts with two simple examples using the scanner
|
||||
// before diving into the scanner itself.
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Valid reports whether data is a valid JSON encoding.
|
||||
func Valid(data []byte) bool {
|
||||
return checkValid(data, &scanner{}) == nil
|
||||
}
|
||||
|
||||
// checkValid verifies that data is valid JSON-encoded data.
|
||||
// scan is passed in for use by checkValid to avoid an allocation.
|
||||
func checkValid(data []byte, scan *scanner) error {
|
||||
scan.reset()
|
||||
for _, c := range data {
|
||||
scan.bytes++
|
||||
if scan.step(scan, c) == scanError {
|
||||
return scan.err
|
||||
}
|
||||
}
|
||||
if scan.eof() == scanError {
|
||||
return scan.err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// nextValue splits data after the next whole JSON value,
|
||||
// returning that value and the bytes that follow it as separate slices.
|
||||
// scan is passed in for use by nextValue to avoid an allocation.
|
||||
func nextValue(data []byte, scan *scanner) (value, rest []byte, err error) {
|
||||
scan.reset()
|
||||
for i, c := range data {
|
||||
v := scan.step(scan, c)
|
||||
if v >= scanEndObject {
|
||||
switch v {
|
||||
// probe the scanner with a space to determine whether we will
|
||||
// get scanEnd on the next character. Otherwise, if the next character
|
||||
// is not a space, scanEndTop allocates a needless error.
|
||||
case scanEndObject, scanEndArray:
|
||||
if scan.step(scan, ' ') == scanEnd {
|
||||
return data[:i+1], data[i+1:], nil
|
||||
}
|
||||
case scanError:
|
||||
return nil, nil, scan.err
|
||||
case scanEnd:
|
||||
return data[:i], data[i:], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if scan.eof() == scanError {
|
||||
return nil, nil, scan.err
|
||||
}
|
||||
return data, nil, nil
|
||||
}
|
||||
|
||||
// A SyntaxError is a description of a JSON syntax error.
|
||||
type SyntaxError struct {
|
||||
msg string // description of error
|
||||
Offset int64 // error occurred after reading Offset bytes
|
||||
}
|
||||
|
||||
func (e *SyntaxError) Error() string { return e.msg }
|
||||
|
||||
// A scanner is a JSON scanning state machine.
|
||||
// Callers call scan.reset() and then pass bytes in one at a time
|
||||
// by calling scan.step(&scan, c) for each byte.
|
||||
// The return value, referred to as an opcode, tells the
|
||||
// caller about significant parsing events like beginning
|
||||
// and ending literals, objects, and arrays, so that the
|
||||
// caller can follow along if it wishes.
|
||||
// The return value scanEnd indicates that a single top-level
|
||||
// JSON value has been completed, *before* the byte that
|
||||
// just got passed in. (The indication must be delayed in order
|
||||
// to recognize the end of numbers: is 123 a whole value or
|
||||
// the beginning of 12345e+6?).
|
||||
type scanner struct {
|
||||
// The step is a func to be called to execute the next transition.
|
||||
// Also tried using an integer constant and a single func
|
||||
// with a switch, but using the func directly was 10% faster
|
||||
// on a 64-bit Mac Mini, and it's nicer to read.
|
||||
step func(*scanner, byte) int
|
||||
|
||||
// Reached end of top-level value.
|
||||
endTop bool
|
||||
|
||||
// Stack of what we're in the middle of - array values, object keys, object values.
|
||||
parseState []int
|
||||
|
||||
// Error that happened, if any.
|
||||
err error
|
||||
|
||||
// 1-byte redo (see undo method)
|
||||
redo bool
|
||||
redoCode int
|
||||
redoState func(*scanner, byte) int
|
||||
|
||||
// total bytes consumed, updated by decoder.Decode
|
||||
bytes int64
|
||||
}
|
||||
|
||||
// These values are returned by the state transition functions
|
||||
// assigned to scanner.state and the method scanner.eof.
|
||||
// They give details about the current state of the scan that
|
||||
// callers might be interested to know about.
|
||||
// It is okay to Ignore the return value of any particular
|
||||
// call to scanner.state: if one call returns scanError,
|
||||
// every subsequent call will return scanError too.
|
||||
const (
|
||||
// Continue.
|
||||
scanContinue = iota // uninteresting byte
|
||||
scanBeginLiteral // end implied by next result != scanContinue
|
||||
scanBeginObject // begin object
|
||||
scanObjectKey // just finished object key (string)
|
||||
scanObjectValue // just finished non-last object value
|
||||
scanEndObject // end object (implies scanObjectValue if possible)
|
||||
scanBeginArray // begin array
|
||||
scanArrayValue // just finished array value
|
||||
scanEndArray // end array (implies scanArrayValue if possible)
|
||||
scanSkipSpace // space byte; can skip; known to be last "continue" result
|
||||
|
||||
// Stop.
|
||||
scanEnd // top-level value ended *before* this byte; known to be first "stop" result
|
||||
scanError // hit an error, scanner.err.
|
||||
|
||||
scanFindType // need to find the "jsonld:type" element of the object.
|
||||
)
|
||||
|
||||
// These values are stored in the parseState stack.
|
||||
// They give the current state of a composite value
|
||||
// being scanned. If the parser is inside a nested value
|
||||
// the parseState describes the nested state, outermost at entry 0.
|
||||
const (
|
||||
parseObjectKey = iota // parsing object key (before colon)
|
||||
parseObjectValue // parsing object value (after colon)
|
||||
parseArrayValue // parsing array value
|
||||
)
|
||||
|
||||
// reset prepares the scanner for use.
|
||||
// It must be called before calling s.step.
|
||||
func (s *scanner) reset() {
|
||||
s.step = stateBeginValue
|
||||
s.parseState = s.parseState[0:0]
|
||||
s.err = nil
|
||||
s.redo = false
|
||||
s.endTop = false
|
||||
}
|
||||
|
||||
// eof tells the scanner that the end of input has been reached.
|
||||
// It returns a scan status just as s.step does.
|
||||
func (s *scanner) eof() int {
|
||||
if s.err != nil {
|
||||
return scanError
|
||||
}
|
||||
if s.endTop {
|
||||
return scanEnd
|
||||
}
|
||||
s.step(s, ' ')
|
||||
if s.endTop {
|
||||
return scanEnd
|
||||
}
|
||||
if s.err == nil {
|
||||
s.err = &SyntaxError{"unexpected end of JSON input", s.bytes}
|
||||
}
|
||||
return scanError
|
||||
}
|
||||
|
||||
// pushParseState pushes a new parse state p onto the parse stack.
|
||||
func (s *scanner) pushParseState(p int) {
|
||||
s.parseState = append(s.parseState, p)
|
||||
}
|
||||
|
||||
// popParseState pops a parse state (already obtained) off the stack
|
||||
// and updates s.step accordingly.
|
||||
func (s *scanner) popParseState() {
|
||||
n := len(s.parseState) - 1
|
||||
s.parseState = s.parseState[0:n]
|
||||
s.redo = false
|
||||
if n == 0 {
|
||||
s.step = stateEndTop
|
||||
s.endTop = true
|
||||
} else {
|
||||
s.step = stateEndValue
|
||||
}
|
||||
}
|
||||
|
||||
func isSpace(c byte) bool {
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n'
|
||||
}
|
||||
|
||||
// stateBeginValueOrEmpty is the state after reading `[`.
|
||||
func stateBeginValueOrEmpty(s *scanner, c byte) int {
|
||||
if c <= ' ' && isSpace(c) {
|
||||
return scanSkipSpace
|
||||
}
|
||||
if c == ']' {
|
||||
return stateEndValue(s, c)
|
||||
}
|
||||
return stateBeginValue(s, c)
|
||||
}
|
||||
|
||||
// stateBeginValue is the state at the beginning of the input.
|
||||
func stateBeginValue(s *scanner, c byte) int {
|
||||
if c <= ' ' && isSpace(c) {
|
||||
return scanSkipSpace
|
||||
}
|
||||
switch c {
|
||||
case '{':
|
||||
s.step = stateBeginStringOrEmpty
|
||||
s.pushParseState(parseObjectKey)
|
||||
return scanBeginObject
|
||||
case '[':
|
||||
s.step = stateBeginValueOrEmpty
|
||||
s.pushParseState(parseArrayValue)
|
||||
return scanBeginArray
|
||||
case '"':
|
||||
s.step = stateInString
|
||||
return scanBeginLiteral
|
||||
case '-':
|
||||
s.step = stateNeg
|
||||
return scanBeginLiteral
|
||||
case '0': // beginning of 0.123
|
||||
s.step = state0
|
||||
return scanBeginLiteral
|
||||
case 't': // beginning of true
|
||||
s.step = stateT
|
||||
return scanBeginLiteral
|
||||
case 'f': // beginning of false
|
||||
s.step = stateF
|
||||
return scanBeginLiteral
|
||||
case 'n': // beginning of null
|
||||
s.step = stateN
|
||||
return scanBeginLiteral
|
||||
}
|
||||
if '1' <= c && c <= '9' { // beginning of 1234.5
|
||||
s.step = state1
|
||||
return scanBeginLiteral
|
||||
}
|
||||
return s.error(c, "looking for beginning of value")
|
||||
}
|
||||
|
||||
// stateBeginStringOrEmpty is the state after reading `{`.
|
||||
func stateBeginStringOrEmpty(s *scanner, c byte) int {
|
||||
if c <= ' ' && isSpace(c) {
|
||||
return scanSkipSpace
|
||||
}
|
||||
if c == '}' {
|
||||
n := len(s.parseState)
|
||||
s.parseState[n-1] = parseObjectValue
|
||||
return stateEndValue(s, c)
|
||||
}
|
||||
return stateBeginString(s, c)
|
||||
}
|
||||
|
||||
// stateBeginString is the state after reading `{"key": value,`.
|
||||
func stateBeginString(s *scanner, c byte) int {
|
||||
if c <= ' ' && isSpace(c) {
|
||||
return scanSkipSpace
|
||||
}
|
||||
if c == '"' {
|
||||
s.step = stateInString
|
||||
return scanBeginLiteral
|
||||
}
|
||||
return s.error(c, "looking for beginning of object key string")
|
||||
}
|
||||
|
||||
// stateEndValue is the state after completing a value,
|
||||
// such as after reading `{}` or `true` or `["x"`.
|
||||
func stateEndValue(s *scanner, c byte) int {
|
||||
n := len(s.parseState)
|
||||
if n == 0 {
|
||||
// Completed top-level before the current byte.
|
||||
s.step = stateEndTop
|
||||
s.endTop = true
|
||||
return stateEndTop(s, c)
|
||||
}
|
||||
if c <= ' ' && isSpace(c) {
|
||||
s.step = stateEndValue
|
||||
return scanSkipSpace
|
||||
}
|
||||
ps := s.parseState[n-1]
|
||||
switch ps {
|
||||
case parseObjectKey:
|
||||
if c == ':' {
|
||||
s.parseState[n-1] = parseObjectValue
|
||||
s.step = stateBeginValue
|
||||
return scanObjectKey
|
||||
}
|
||||
return s.error(c, "after object key")
|
||||
case parseObjectValue:
|
||||
if c == ',' {
|
||||
s.parseState[n-1] = parseObjectKey
|
||||
s.step = stateBeginString
|
||||
return scanObjectValue
|
||||
}
|
||||
if c == '}' {
|
||||
s.popParseState()
|
||||
return scanEndObject
|
||||
}
|
||||
return s.error(c, "after object key:value pair")
|
||||
case parseArrayValue:
|
||||
if c == ',' {
|
||||
s.step = stateBeginValue
|
||||
return scanArrayValue
|
||||
}
|
||||
if c == ']' {
|
||||
s.popParseState()
|
||||
return scanEndArray
|
||||
}
|
||||
return s.error(c, "after array element")
|
||||
}
|
||||
return s.error(c, "")
|
||||
}
|
||||
|
||||
// stateEndTop is the state after finishing the top-level value,
|
||||
// such as after reading `{}` or `[1,2,3]`.
|
||||
// Only space characters should be seen now.
|
||||
func stateEndTop(s *scanner, c byte) int {
|
||||
if c != ' ' && c != '\t' && c != '\r' && c != '\n' {
|
||||
// Complain about non-space byte on next call.
|
||||
s.error(c, "after top-level value")
|
||||
}
|
||||
return scanEnd
|
||||
}
|
||||
|
||||
// stateInString is the state after reading `"`.
|
||||
func stateInString(s *scanner, c byte) int {
|
||||
if c == '"' {
|
||||
s.step = stateEndValue
|
||||
return scanContinue
|
||||
}
|
||||
if c == '\\' {
|
||||
s.step = stateInStringEsc
|
||||
return scanContinue
|
||||
}
|
||||
if c < 0x20 {
|
||||
return s.error(c, "in string literal")
|
||||
}
|
||||
return scanContinue
|
||||
}
|
||||
|
||||
// stateInStringEsc is the state after reading `"\` during a quoted string.
|
||||
func stateInStringEsc(s *scanner, c byte) int {
|
||||
switch c {
|
||||
case 'b', 'f', 'n', 'r', 't', '\\', '/', '"':
|
||||
s.step = stateInString
|
||||
return scanContinue
|
||||
case 'u':
|
||||
s.step = stateInStringEscU
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in string escape code")
|
||||
}
|
||||
|
||||
// stateInStringEscU is the state after reading `"\u` during a quoted string.
|
||||
func stateInStringEscU(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
|
||||
s.step = stateInStringEscU1
|
||||
return scanContinue
|
||||
}
|
||||
// numbers
|
||||
return s.error(c, "in \\u hexadecimal character escape")
|
||||
}
|
||||
|
||||
// stateInStringEscU1 is the state after reading `"\u1` during a quoted string.
|
||||
func stateInStringEscU1(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
|
||||
s.step = stateInStringEscU12
|
||||
return scanContinue
|
||||
}
|
||||
// numbers
|
||||
return s.error(c, "in \\u hexadecimal character escape")
|
||||
}
|
||||
|
||||
// stateInStringEscU12 is the state after reading `"\u12` during a quoted string.
|
||||
func stateInStringEscU12(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
|
||||
s.step = stateInStringEscU123
|
||||
return scanContinue
|
||||
}
|
||||
// numbers
|
||||
return s.error(c, "in \\u hexadecimal character escape")
|
||||
}
|
||||
|
||||
// stateInStringEscU123 is the state after reading `"\u123` during a quoted string.
|
||||
func stateInStringEscU123(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F' {
|
||||
s.step = stateInString
|
||||
return scanContinue
|
||||
}
|
||||
// numbers
|
||||
return s.error(c, "in \\u hexadecimal character escape")
|
||||
}
|
||||
|
||||
// stateNeg is the state after reading `-` during a number.
|
||||
func stateNeg(s *scanner, c byte) int {
|
||||
if c == '0' {
|
||||
s.step = state0
|
||||
return scanContinue
|
||||
}
|
||||
if '1' <= c && c <= '9' {
|
||||
s.step = state1
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in numeric literal")
|
||||
}
|
||||
|
||||
// state1 is the state after reading a non-zero integer during a number,
|
||||
// such as after reading `1` or `100` but not `0`.
|
||||
func state1(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' {
|
||||
s.step = state1
|
||||
return scanContinue
|
||||
}
|
||||
return state0(s, c)
|
||||
}
|
||||
|
||||
// state0 is the state after reading `0` during a number.
|
||||
func state0(s *scanner, c byte) int {
|
||||
if c == '.' {
|
||||
s.step = stateDot
|
||||
return scanContinue
|
||||
}
|
||||
if c == 'e' || c == 'E' {
|
||||
s.step = stateE
|
||||
return scanContinue
|
||||
}
|
||||
return stateEndValue(s, c)
|
||||
}
|
||||
|
||||
// stateDot is the state after reading the integer and decimal point in a number,
|
||||
// such as after reading `1.`.
|
||||
func stateDot(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' {
|
||||
s.step = stateDot0
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "after decimal point in numeric literal")
|
||||
}
|
||||
|
||||
// stateDot0 is the state after reading the integer, decimal point, and subsequent
|
||||
// digits of a number, such as after reading `3.14`.
|
||||
func stateDot0(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' {
|
||||
return scanContinue
|
||||
}
|
||||
if c == 'e' || c == 'E' {
|
||||
s.step = stateE
|
||||
return scanContinue
|
||||
}
|
||||
return stateEndValue(s, c)
|
||||
}
|
||||
|
||||
// stateE is the state after reading the mantissa and e in a number,
|
||||
// such as after reading `314e` or `0.314e`.
|
||||
func stateE(s *scanner, c byte) int {
|
||||
if c == '+' || c == '-' {
|
||||
s.step = stateESign
|
||||
return scanContinue
|
||||
}
|
||||
return stateESign(s, c)
|
||||
}
|
||||
|
||||
// stateESign is the state after reading the mantissa, e, and sign in a number,
|
||||
// such as after reading `314e-` or `0.314e+`.
|
||||
func stateESign(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' {
|
||||
s.step = stateE0
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in exponent of numeric literal")
|
||||
}
|
||||
|
||||
// stateE0 is the state after reading the mantissa, e, optional sign,
|
||||
// and at least one digit of the exponent in a number,
|
||||
// such as after reading `314e-2` or `0.314e+1` or `3.14e0`.
|
||||
func stateE0(s *scanner, c byte) int {
|
||||
if '0' <= c && c <= '9' {
|
||||
return scanContinue
|
||||
}
|
||||
return stateEndValue(s, c)
|
||||
}
|
||||
|
||||
// stateT is the state after reading `t`.
|
||||
func stateT(s *scanner, c byte) int {
|
||||
if c == 'r' {
|
||||
s.step = stateTr
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal true (expecting 'r')")
|
||||
}
|
||||
|
||||
// stateTr is the state after reading `tr`.
|
||||
func stateTr(s *scanner, c byte) int {
|
||||
if c == 'u' {
|
||||
s.step = stateTru
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal true (expecting 'u')")
|
||||
}
|
||||
|
||||
// stateTru is the state after reading `tru`.
|
||||
func stateTru(s *scanner, c byte) int {
|
||||
if c == 'e' {
|
||||
s.step = stateEndValue
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal true (expecting 'e')")
|
||||
}
|
||||
|
||||
// stateF is the state after reading `f`.
|
||||
func stateF(s *scanner, c byte) int {
|
||||
if c == 'a' {
|
||||
s.step = stateFa
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal false (expecting 'a')")
|
||||
}
|
||||
|
||||
// stateFa is the state after reading `fa`.
|
||||
func stateFa(s *scanner, c byte) int {
|
||||
if c == 'l' {
|
||||
s.step = stateFal
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal false (expecting 'l')")
|
||||
}
|
||||
|
||||
// stateFal is the state after reading `fal`.
|
||||
func stateFal(s *scanner, c byte) int {
|
||||
if c == 's' {
|
||||
s.step = stateFals
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal false (expecting 's')")
|
||||
}
|
||||
|
||||
// stateFals is the state after reading `fals`.
|
||||
func stateFals(s *scanner, c byte) int {
|
||||
if c == 'e' {
|
||||
s.step = stateEndValue
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal false (expecting 'e')")
|
||||
}
|
||||
|
||||
// stateN is the state after reading `n`.
|
||||
func stateN(s *scanner, c byte) int {
|
||||
if c == 'u' {
|
||||
s.step = stateNu
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal null (expecting 'u')")
|
||||
}
|
||||
|
||||
// stateNu is the state after reading `nu`.
|
||||
func stateNu(s *scanner, c byte) int {
|
||||
if c == 'l' {
|
||||
s.step = stateNul
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal null (expecting 'l')")
|
||||
}
|
||||
|
||||
// stateNul is the state after reading `nul`.
|
||||
func stateNul(s *scanner, c byte) int {
|
||||
if c == 'l' {
|
||||
s.step = stateEndValue
|
||||
return scanContinue
|
||||
}
|
||||
return s.error(c, "in literal null (expecting 'l')")
|
||||
}
|
||||
|
||||
// stateError is the state after reaching a syntax error,
|
||||
// such as after reading `[1}` or `5.1.2`.
|
||||
func stateError(s *scanner, c byte) int {
|
||||
return scanError
|
||||
}
|
||||
|
||||
// error records an error and switches to the error state.
|
||||
func (s *scanner) error(c byte, context string) int {
|
||||
s.step = stateError
|
||||
s.err = &SyntaxError{"invalid character " + quoteChar(c) + " " + context, s.bytes}
|
||||
return scanError
|
||||
}
|
||||
|
||||
// quoteChar formats c as a quoted character literal
|
||||
func quoteChar(c byte) string {
|
||||
// special cases - different from quoted strings
|
||||
if c == '\'' {
|
||||
return `'\''`
|
||||
}
|
||||
if c == '"' {
|
||||
return `'"'`
|
||||
}
|
||||
|
||||
// use quoted string with different quotation marks
|
||||
s := strconv.Quote(string(c))
|
||||
return "'" + s[1:len(s)-1] + "'"
|
||||
}
|
||||
|
||||
// undo causes the scanner to return scanCode from the next state transition.
|
||||
// This gives callers a simple 1-byte undo mechanism.
|
||||
func (s *scanner) undo(scanCode int) {
|
||||
if s.redo {
|
||||
panic("json: invalid use of scanner")
|
||||
}
|
||||
s.redoCode = scanCode
|
||||
s.redoState = s.step
|
||||
s.step = stateRedo
|
||||
s.redo = true
|
||||
}
|
||||
|
||||
// stateRedo helps implement the scanner's 1-byte undo.
|
||||
func stateRedo(s *scanner, c byte) int {
|
||||
s.redo = false
|
||||
s.step = s.redoState
|
||||
return s.redoCode
|
||||
}
|
|
@ -0,0 +1,218 @@
|
|||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonld
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// safeSet holds the value true if the ASCII character with the given array
|
||||
// position can be represented inside a JSON string without any further
|
||||
// escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), and the backslash character ("\").
|
||||
var safeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': true,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': true,
|
||||
'=': true,
|
||||
'>': true,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
|
||||
// htmlSafeSet holds the value true if the ASCII character with the given
|
||||
// array position can be safely represented inside a JSON string, embedded
|
||||
// inside of HTML <script> tags, without any additional escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), the backslash character ("\"), HTML opening and closing
|
||||
// tags ("<" and ">"), and the ampersand ("&").
|
||||
var htmlSafeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': false,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': false,
|
||||
'=': true,
|
||||
'>': false,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package jsonld
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// tagOptions is the string following a comma in a struct field's "json"
|
||||
// tag, or the empty string. It does not include the leading comma.
|
||||
type tagOptions string
|
||||
|
||||
// parseTag splits a struct field's json tag into its name and
|
||||
// comma-separated options.
|
||||
func parseTag(tag string) (string, tagOptions) {
|
||||
if idx := strings.Index(tag, ","); idx != -1 {
|
||||
return tag[:idx], tagOptions(tag[idx+1:])
|
||||
}
|
||||
return tag, tagOptions("")
|
||||
}
|
||||
|
||||
// Contains reports whether a comma-separated list of options
|
||||
// contains a particular substr flag. substr must be surrounded by a
|
||||
// string boundary or commas.
|
||||
func (o tagOptions) Contains(optionName string) bool {
|
||||
if len(o) == 0 {
|
||||
return false
|
||||
}
|
||||
s := string(o)
|
||||
for s != "" {
|
||||
var next string
|
||||
i := strings.Index(s, ",")
|
||||
if i >= 0 {
|
||||
s, next = s[:i], s[i+1:]
|
||||
}
|
||||
if s == optionName {
|
||||
return true
|
||||
}
|
||||
s = next
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
tags
|
|
@ -0,0 +1,19 @@
|
|||
language: go
|
||||
|
||||
go:
|
||||
- 1.10.x
|
||||
|
||||
script:
|
||||
# build test for supported platforms
|
||||
- GOOS=linux go build
|
||||
- GOOS=darwin go build
|
||||
- GOOS=freebsd go build
|
||||
- GOOS=windows go build
|
||||
|
||||
# run tests on a standard platform
|
||||
- go test -v ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
- go test -v ./... -race
|
||||
|
||||
after_success:
|
||||
# Upload coverage results to codecov.io
|
||||
- bash <(curl -s https://codecov.io/bash)
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Aliaksandr Valialkin
|
||||
|
||||
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.
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
[![Build Status](https://travis-ci.org/valyala/fastjson.svg)](https://travis-ci.org/valyala/fastjson)
|
||||
[![GoDoc](https://godoc.org/github.com/valyala/fastjson?status.svg)](http://godoc.org/github.com/valyala/fastjson)
|
||||
[![Go Report](https://goreportcard.com/badge/github.com/valyala/fastjson)](https://goreportcard.com/report/github.com/valyala/fastjson)
|
||||
[![codecov](https://codecov.io/gh/valyala/fastjson/branch/master/graph/badge.svg)](https://codecov.io/gh/valyala/fastjson)
|
||||
|
||||
# fastjson - fast JSON parser and validator for Go
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Fast. As usual, up to 15x faster than the standard [encoding/json](https://golang.org/pkg/encoding/json/).
|
||||
See [benchmarks](#benchmarks).
|
||||
* Parses arbitrary JSON without schema, reflection, struct magic and code generation
|
||||
contrary to [easyjson](https://github.com/mailru/easyjson).
|
||||
* Provides simple [API](http://godoc.org/github.com/valyala/fastjson).
|
||||
* Outperforms [jsonparser](https://github.com/buger/jsonparser) and [gjson](https://github.com/tidwall/gjson)
|
||||
when accessing multiple unrelated fields, since `fastjson` parses the input JSON only once.
|
||||
* Validates the parsed JSON unlike [jsonparser](https://github.com/buger/jsonparser)
|
||||
and [gjson](https://github.com/tidwall/gjson).
|
||||
* May quickly extract a part of the original JSON with `Value.Get(...).MarshalTo` and modify it
|
||||
with [Del](https://godoc.org/github.com/valyala/fastjson#Value.Del)
|
||||
and [Set](https://godoc.org/github.com/valyala/fastjson#Value.Set) functions.
|
||||
* May parse array containing values with distinct types (aka non-homogenous types).
|
||||
For instance, `fastjson` easily parses the following JSON array `[123, "foo", [456], {"k": "v"}, null]`.
|
||||
* `fastjson` preserves the original order of object items when calling
|
||||
[Object.Visit](https://godoc.org/github.com/valyala/fastjson#Object.Visit).
|
||||
|
||||
|
||||
## Known limitations
|
||||
|
||||
* Requies extra care to work with - references to certain objects recursively
|
||||
returned by [Parser](https://godoc.org/github.com/valyala/fastjson#Parser)
|
||||
must be released before the next call to [Parse](https://godoc.org/github.com/valyala/fastjson#Parser.Parse).
|
||||
Otherwise the program may work improperly. The same applies to objects returned by [Arena](https://godoc.org/github.com/valyala/fastjson#Arena).
|
||||
Adhere recommendations from [docs](https://godoc.org/github.com/valyala/fastjson).
|
||||
* Cannot parse JSON from `io.Reader`. There is [Scanner](https://godoc.org/github.com/valyala/fastjson#Scanner)
|
||||
for parsing stream of JSON values from a string.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
One-liner accessing a single field:
|
||||
```go
|
||||
s := []byte(`{"foo": [123, "bar"]}`)
|
||||
fmt.Printf("foo.0=%d\n", fastjson.GetInt(s, "foo", "0"))
|
||||
|
||||
// Output:
|
||||
// foo.0=123
|
||||
```
|
||||
|
||||
Accessing multiple fields with error handling:
|
||||
```go
|
||||
var p fastjson.Parser
|
||||
v, err := p.Parse(`{
|
||||
"str": "bar",
|
||||
"int": 123,
|
||||
"float": 1.23,
|
||||
"bool": true,
|
||||
"arr": [1, "foo", {}]
|
||||
}`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("foo=%s\n", v.GetStringBytes("str"))
|
||||
fmt.Printf("int=%d\n", v.GetInt("int"))
|
||||
fmt.Printf("float=%f\n", v.GetFloat64("float"))
|
||||
fmt.Printf("bool=%v\n", v.GetBool("bool"))
|
||||
fmt.Printf("arr.1=%s\n", v.GetStringBytes("arr", "1"))
|
||||
|
||||
// Output:
|
||||
// foo=bar
|
||||
// int=123
|
||||
// float=1.230000
|
||||
// bool=true
|
||||
// arr.1=foo
|
||||
```
|
||||
|
||||
See also [examples](https://godoc.org/github.com/valyala/fastjson#pkg-examples).
|
||||
|
||||
|
||||
## Security
|
||||
|
||||
* `fastjson` shouldn't crash or panic when parsing input strings specially crafted
|
||||
by an attacker. It must return error on invalid input JSON.
|
||||
* `fastjson` requires up to `sizeof(Value) * len(inputJSON)` bytes of memory
|
||||
for parsing `inputJSON` string. Limit the maximum size of the `inputJSON`
|
||||
before parsing it in order to limit the maximum memory usage.
|
||||
|
||||
|
||||
## Performance optimization tips
|
||||
|
||||
* Re-use [Parser](https://godoc.org/github.com/valyala/fastjson#Parser) and [Scanner](https://godoc.org/github.com/valyala/fastjson#Scanner)
|
||||
for parsing many JSONs. This reduces memory allocations overhead.
|
||||
[ParserPool](https://godoc.org/github.com/valyala/fastjson#ParserPool) may be useful in this case.
|
||||
* Prefer calling `Value.Get*` on the value returned from [Parser](https://godoc.org/github.com/valyala/fastjson#Parser)
|
||||
instead of calling `Get*` one-liners when multiple fields
|
||||
must be obtained from JSON, since each `Get*` one-liner re-parses
|
||||
the input JSON again.
|
||||
* Prefer calling once [Value.Get](https://godoc.org/github.com/valyala/fastjson#Value.Get)
|
||||
for common prefix paths and then calling `Value.Get*` on the returned value
|
||||
for distinct suffix paths.
|
||||
* Prefer iterating over array returned from [Value.GetArray](https://godoc.org/github.com/valyala/fastjson#Object.Visit)
|
||||
with a range loop instead of calling `Value.Get*` for each array item.
|
||||
|
||||
## Fuzzing
|
||||
Install [go-fuzz](https://github.com/dvyukov/go-fuzz) & optionally the go-fuzz-corpus.
|
||||
|
||||
```bash
|
||||
go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
```
|
||||
|
||||
Build using `go-fuzz-build` and run `go-fuzz` with an optional corpus.
|
||||
|
||||
```bash
|
||||
mkdir -p workdir/corpus
|
||||
cp $GOPATH/src/github.com/dvyukov/go-fuzz-corpus/json/corpus/* workdir/corpus
|
||||
go-fuzz-build github.com/valyala/fastjson
|
||||
go-fuzz -bin=fastjson-fuzz.zip -workdir=workdir
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Go 1.12 has been used for benchmarking.
|
||||
|
||||
Legend:
|
||||
|
||||
* `small` - parse [small.json](testdata/small.json) (190 bytes).
|
||||
* `medium` - parse [medium.json](testdata/medium.json) (2.3KB).
|
||||
* `large` - parse [large.json](testdata/large.json) (28KB).
|
||||
* `canada` - parse [canada.json](testdata/canada.json) (2.2MB).
|
||||
* `citm` - parse [citm_catalog.json](testdata/citm_catalog.json) (1.7MB).
|
||||
* `twitter` - parse [twitter.json](testdata/twitter.json) (617KB).
|
||||
|
||||
* `stdjson-map` - parse into a `map[string]interface{}` using `encoding/json`.
|
||||
* `stdjson-struct` - parse into a struct containing
|
||||
a subset of fields of the parsed JSON, using `encoding/json`.
|
||||
* `stdjson-empty-struct` - parse into an empty struct using `encoding/json`.
|
||||
This is the fastest possible solution for `encoding/json`, may be used
|
||||
for json validation. See also benchmark results for json validation.
|
||||
* `fastjson` - parse using `fastjson` without fields access.
|
||||
* `fastjson-get` - parse using `fastjson` with fields access similar to `stdjson-struct`.
|
||||
|
||||
```
|
||||
$ GOMAXPROCS=1 go test github.com/valyala/fastjson -bench='Parse$'
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/valyala/fastjson
|
||||
BenchmarkParse/small/stdjson-map 200000 7305 ns/op 26.01 MB/s 960 B/op 51 allocs/op
|
||||
BenchmarkParse/small/stdjson-struct 500000 3431 ns/op 55.37 MB/s 224 B/op 4 allocs/op
|
||||
BenchmarkParse/small/stdjson-empty-struct 500000 2273 ns/op 83.58 MB/s 168 B/op 2 allocs/op
|
||||
BenchmarkParse/small/fastjson 5000000 347 ns/op 547.53 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkParse/small/fastjson-get 2000000 620 ns/op 306.39 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkParse/medium/stdjson-map 30000 40672 ns/op 57.26 MB/s 10196 B/op 208 allocs/op
|
||||
BenchmarkParse/medium/stdjson-struct 30000 47792 ns/op 48.73 MB/s 9174 B/op 258 allocs/op
|
||||
BenchmarkParse/medium/stdjson-empty-struct 100000 22096 ns/op 105.40 MB/s 280 B/op 5 allocs/op
|
||||
BenchmarkParse/medium/fastjson 500000 3025 ns/op 769.90 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkParse/medium/fastjson-get 500000 3211 ns/op 725.20 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkParse/large/stdjson-map 2000 614079 ns/op 45.79 MB/s 210734 B/op 2785 allocs/op
|
||||
BenchmarkParse/large/stdjson-struct 5000 298554 ns/op 94.18 MB/s 15616 B/op 353 allocs/op
|
||||
BenchmarkParse/large/stdjson-empty-struct 5000 268577 ns/op 104.69 MB/s 280 B/op 5 allocs/op
|
||||
BenchmarkParse/large/fastjson 50000 35210 ns/op 798.56 MB/s 5 B/op 0 allocs/op
|
||||
BenchmarkParse/large/fastjson-get 50000 35171 ns/op 799.46 MB/s 5 B/op 0 allocs/op
|
||||
BenchmarkParse/canada/stdjson-map 20 68147307 ns/op 33.03 MB/s 12260502 B/op 392539 allocs/op
|
||||
BenchmarkParse/canada/stdjson-struct 20 68044518 ns/op 33.08 MB/s 12260123 B/op 392534 allocs/op
|
||||
BenchmarkParse/canada/stdjson-empty-struct 100 17709250 ns/op 127.11 MB/s 280 B/op 5 allocs/op
|
||||
BenchmarkParse/canada/fastjson 300 4182404 ns/op 538.22 MB/s 254902 B/op 381 allocs/op
|
||||
BenchmarkParse/canada/fastjson-get 300 4274744 ns/op 526.60 MB/s 254902 B/op 381 allocs/op
|
||||
BenchmarkParse/citm/stdjson-map 50 27772612 ns/op 62.19 MB/s 5214163 B/op 95402 allocs/op
|
||||
BenchmarkParse/citm/stdjson-struct 100 14936191 ns/op 115.64 MB/s 1989 B/op 75 allocs/op
|
||||
BenchmarkParse/citm/stdjson-empty-struct 100 14946034 ns/op 115.56 MB/s 280 B/op 5 allocs/op
|
||||
BenchmarkParse/citm/fastjson 1000 1879714 ns/op 918.87 MB/s 17628 B/op 30 allocs/op
|
||||
BenchmarkParse/citm/fastjson-get 1000 1881598 ns/op 917.94 MB/s 17628 B/op 30 allocs/op
|
||||
BenchmarkParse/twitter/stdjson-map 100 11289146 ns/op 55.94 MB/s 2187878 B/op 31266 allocs/op
|
||||
BenchmarkParse/twitter/stdjson-struct 300 5779442 ns/op 109.27 MB/s 408 B/op 6 allocs/op
|
||||
BenchmarkParse/twitter/stdjson-empty-struct 300 5738504 ns/op 110.05 MB/s 408 B/op 6 allocs/op
|
||||
BenchmarkParse/twitter/fastjson 2000 774042 ns/op 815.86 MB/s 2541 B/op 2 allocs/op
|
||||
BenchmarkParse/twitter/fastjson-get 2000 777833 ns/op 811.89 MB/s 2541 B/op 2 allocs/op
|
||||
```
|
||||
|
||||
Benchmark results for json validation:
|
||||
|
||||
```
|
||||
$ GOMAXPROCS=1 go test github.com/valyala/fastjson -bench='Validate$'
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
pkg: github.com/valyala/fastjson
|
||||
BenchmarkValidate/small/stdjson 2000000 955 ns/op 198.83 MB/s 72 B/op 2 allocs/op
|
||||
BenchmarkValidate/small/fastjson 5000000 384 ns/op 493.60 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkValidate/medium/stdjson 200000 10799 ns/op 215.66 MB/s 184 B/op 5 allocs/op
|
||||
BenchmarkValidate/medium/fastjson 300000 3809 ns/op 611.30 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkValidate/large/stdjson 10000 133064 ns/op 211.31 MB/s 184 B/op 5 allocs/op
|
||||
BenchmarkValidate/large/fastjson 30000 45268 ns/op 621.14 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkValidate/canada/stdjson 200 8470904 ns/op 265.74 MB/s 184 B/op 5 allocs/op
|
||||
BenchmarkValidate/canada/fastjson 500 2973377 ns/op 757.07 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkValidate/citm/stdjson 200 7273172 ns/op 237.48 MB/s 184 B/op 5 allocs/op
|
||||
BenchmarkValidate/citm/fastjson 1000 1684430 ns/op 1025.39 MB/s 0 B/op 0 allocs/op
|
||||
BenchmarkValidate/twitter/stdjson 500 2849439 ns/op 221.63 MB/s 312 B/op 6 allocs/op
|
||||
BenchmarkValidate/twitter/fastjson 2000 1036796 ns/op 609.10 MB/s 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
## FAQ
|
||||
|
||||
* Q: _There are a ton of other high-perf packages for JSON parsing in Go. Why creating yet another package?_
|
||||
A: Because other packages require either rigid JSON schema via struct magic
|
||||
and code generation or perform poorly when multiple unrelated fields
|
||||
must be obtained from the parsed JSON.
|
||||
Additionally, `fastjson` provides nicer [API](http://godoc.org/github.com/valyala/fastjson).
|
||||
|
||||
* Q: _What is the main purpose for `fastjson`?_
|
||||
A: High-perf JSON parsing for [RTB](https://www.iab.com/wp-content/uploads/2015/05/OpenRTB_API_Specification_Version_2_3_1.pdf)
|
||||
and other [JSON-RPC](https://en.wikipedia.org/wiki/JSON-RPC) services.
|
||||
|
||||
* Q: _Why fastjson doesn't provide fast marshaling (serialization)?_
|
||||
A: Actually it provides some sort of marshaling - see [Value.MarshalTo](https://godoc.org/github.com/valyala/fastjson#Value.MarshalTo).
|
||||
But I'd recommend using [quicktemplate](https://github.com/valyala/quicktemplate#use-cases)
|
||||
for high-performance JSON marshaling :)
|
||||
|
||||
* Q: _`fastjson` crashes my program!_
|
||||
A: There is high probability of improper use.
|
||||
* Make sure you don't hold references to objects recursively returned by `Parser` / `Scanner`
|
||||
beyond the next `Parser.Parse` / `Scanner.Next` call
|
||||
if such restriction is mentioned in [docs](https://github.com/valyala/fastjson/issues/new).
|
||||
* Make sure you don't access `fastjson` objects from concurrently running goroutines
|
||||
if such restriction is mentioned in [docs](https://github.com/valyala/fastjson/issues/new).
|
||||
* Build and run your program with [-race](https://golang.org/doc/articles/race_detector.html) flag.
|
||||
Make sure the race detector detects zero races.
|
||||
* If your program continue crashing after fixing issues mentioned above, [file a bug](https://github.com/valyala/fastjson/issues/new).
|
|
@ -0,0 +1,126 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Arena may be used for fast creation and re-use of Values.
|
||||
//
|
||||
// Typical Arena lifecycle:
|
||||
//
|
||||
// 1) Construct Values via the Arena and Value.Set* calls.
|
||||
// 2) Marshal the constructed Values with Value.MarshalTo call.
|
||||
// 3) Reset all the constructed Values at once by Arena.Reset call.
|
||||
// 4) Go to 1 and re-use the Arena.
|
||||
//
|
||||
// It is unsafe calling Arena methods from concurrent goroutines.
|
||||
// Use per-goroutine Arenas or ArenaPool instead.
|
||||
type Arena struct {
|
||||
b []byte
|
||||
c cache
|
||||
}
|
||||
|
||||
// Reset resets all the Values allocated by a.
|
||||
//
|
||||
// Values previously allocated by a cannot be used after the Reset call.
|
||||
func (a *Arena) Reset() {
|
||||
a.b = a.b[:0]
|
||||
a.c.reset()
|
||||
}
|
||||
|
||||
// NewObject returns new empty object value.
|
||||
//
|
||||
// New entries may be added to the returned object via Set call.
|
||||
//
|
||||
// The returned object is valid until Reset is called on a.
|
||||
func (a *Arena) NewObject() *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = TypeObject
|
||||
v.o.reset()
|
||||
return v
|
||||
}
|
||||
|
||||
// NewArray returns new empty array value.
|
||||
//
|
||||
// New entries may be added to the returned array via Set* calls.
|
||||
//
|
||||
// The returned array is valid until Reset is called on a.
|
||||
func (a *Arena) NewArray() *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = TypeArray
|
||||
v.a = v.a[:0]
|
||||
return v
|
||||
}
|
||||
|
||||
// NewString returns new string value containing s.
|
||||
//
|
||||
// The returned string is valid until Reset is called on a.
|
||||
func (a *Arena) NewString(s string) *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = typeRawString
|
||||
bLen := len(a.b)
|
||||
a.b = escapeString(a.b, s)
|
||||
v.s = b2s(a.b[bLen+1 : len(a.b)-1])
|
||||
return v
|
||||
}
|
||||
|
||||
// NewStringBytes returns new string value containing b.
|
||||
//
|
||||
// The returned string is valid until Reset is called on a.
|
||||
func (a *Arena) NewStringBytes(b []byte) *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = typeRawString
|
||||
bLen := len(a.b)
|
||||
a.b = escapeString(a.b, b2s(b))
|
||||
v.s = b2s(a.b[bLen+1 : len(a.b)-1])
|
||||
return v
|
||||
}
|
||||
|
||||
// NewNumberFloat64 returns new number value containing f.
|
||||
//
|
||||
// The returned number is valid until Reset is called on a.
|
||||
func (a *Arena) NewNumberFloat64(f float64) *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = TypeNumber
|
||||
bLen := len(a.b)
|
||||
a.b = strconv.AppendFloat(a.b, f, 'g', -1, 64)
|
||||
v.s = b2s(a.b[bLen:])
|
||||
return v
|
||||
}
|
||||
|
||||
// NewNumberInt returns new number value containing n.
|
||||
//
|
||||
// The returned number is valid until Reset is called on a.
|
||||
func (a *Arena) NewNumberInt(n int) *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = TypeNumber
|
||||
bLen := len(a.b)
|
||||
a.b = strconv.AppendInt(a.b, int64(n), 10)
|
||||
v.s = b2s(a.b[bLen:])
|
||||
return v
|
||||
}
|
||||
|
||||
// NewNumberString returns new number value containing s.
|
||||
//
|
||||
// The returned number is valid until Reset is called on a.
|
||||
func (a *Arena) NewNumberString(s string) *Value {
|
||||
v := a.c.getValue()
|
||||
v.t = TypeNumber
|
||||
v.s = s
|
||||
return v
|
||||
}
|
||||
|
||||
// NewNull returns null value.
|
||||
func (a *Arena) NewNull() *Value {
|
||||
return valueNull
|
||||
}
|
||||
|
||||
// NewTrue returns true value.
|
||||
func (a *Arena) NewTrue() *Value {
|
||||
return valueTrue
|
||||
}
|
||||
|
||||
// NewFalse return false value.
|
||||
func (a *Arena) NewFalse() *Value {
|
||||
return valueFalse
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
Package fastjson provides fast JSON parsing.
|
||||
|
||||
Arbitrary JSON may be parsed by fastjson without the need for creating structs
|
||||
or for generating go code. Just parse JSON and get the required fields with
|
||||
Get* functions.
|
||||
|
||||
*/
|
||||
package fastjson
|
|
@ -0,0 +1,515 @@
|
|||
package fastfloat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParseUint64BestEffort parses uint64 number s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseUint(s, 10, 64), but is faster.
|
||||
//
|
||||
// 0 is returned if the number cannot be parsed.
|
||||
// See also ParseUint64, which returns parse error if the number cannot be parsed.
|
||||
func ParseUint64BestEffort(s string) uint64 {
|
||||
if len(s) == 0 {
|
||||
return 0
|
||||
}
|
||||
i := uint(0)
|
||||
d := uint64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for uint64.
|
||||
// Fall back to slow parsing.
|
||||
dd, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return dd
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0
|
||||
}
|
||||
if i < uint(len(s)) {
|
||||
// Unparsed tail left.
|
||||
return 0
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ParseUint64 parses uint64 from s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseUint(s, 10, 64), but is faster.
|
||||
//
|
||||
// See also ParseUint64BestEffort.
|
||||
func ParseUint64(s string) (uint64, error) {
|
||||
if len(s) == 0 {
|
||||
return 0, fmt.Errorf("cannot parse uint64 from empty string")
|
||||
}
|
||||
i := uint(0)
|
||||
d := uint64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for uint64.
|
||||
// Fall back to slow parsing.
|
||||
dd, err := strconv.ParseUint(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dd, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0, fmt.Errorf("cannot parse uint64 from %q", s)
|
||||
}
|
||||
if i < uint(len(s)) {
|
||||
// Unparsed tail left.
|
||||
return 0, fmt.Errorf("unparsed tail left after parsing uint64 from %q: %q", s, s[i:])
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// ParseInt64BestEffort parses int64 number s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseInt(s, 10, 64), but is faster.
|
||||
//
|
||||
// 0 is returned if the number cannot be parsed.
|
||||
// See also ParseInt64, which returns parse error if the number cannot be parsed.
|
||||
func ParseInt64BestEffort(s string) int64 {
|
||||
if len(s) == 0 {
|
||||
return 0
|
||||
}
|
||||
i := uint(0)
|
||||
minus := s[0] == '-'
|
||||
if minus {
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
d := int64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + int64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for int64.
|
||||
// Fall back to slow parsing.
|
||||
dd, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return dd
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0
|
||||
}
|
||||
if i < uint(len(s)) {
|
||||
// Unparsed tail left.
|
||||
return 0
|
||||
}
|
||||
if minus {
|
||||
d = -d
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// ParseInt64 parses int64 number s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseInt(s, 10, 64), but is faster.
|
||||
//
|
||||
// See also ParseInt64BestEffort.
|
||||
func ParseInt64(s string) (int64, error) {
|
||||
if len(s) == 0 {
|
||||
return 0, fmt.Errorf("cannot parse int64 from empty string")
|
||||
}
|
||||
i := uint(0)
|
||||
minus := s[0] == '-'
|
||||
if minus {
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0, fmt.Errorf("cannot parse int64 from %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
d := int64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + int64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for int64.
|
||||
// Fall back to slow parsing.
|
||||
dd, err := strconv.ParseInt(s, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return dd, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0, fmt.Errorf("cannot parse int64 from %q", s)
|
||||
}
|
||||
if i < uint(len(s)) {
|
||||
// Unparsed tail left.
|
||||
return 0, fmt.Errorf("unparsed tail left after parsing int64 form %q: %q", s, s[i:])
|
||||
}
|
||||
if minus {
|
||||
d = -d
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Exact powers of 10.
|
||||
//
|
||||
// This works faster than math.Pow10, since it avoids additional multiplication.
|
||||
var float64pow10 = [...]float64{
|
||||
1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16,
|
||||
}
|
||||
|
||||
// ParseBestEffort parses floating-point number s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseFloat(s, 64), but is faster.
|
||||
//
|
||||
// 0 is returned if the number cannot be parsed.
|
||||
// See also Parse, which returns parse error if the number cannot be parsed.
|
||||
func ParseBestEffort(s string) float64 {
|
||||
if len(s) == 0 {
|
||||
return 0
|
||||
}
|
||||
i := uint(0)
|
||||
minus := s[0] == '-'
|
||||
if minus {
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// the integer part might be elided to remain compliant
|
||||
// with https://go.dev/ref/spec#Floating-point_literals
|
||||
if s[i] == '.' && (i+1 >= uint(len(s)) || s[i+1] < '0' || s[i+1] > '9') {
|
||||
return 0
|
||||
}
|
||||
|
||||
d := uint64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for uint64.
|
||||
// Fall back to slow parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j && s[i] != '.' {
|
||||
s = s[i:]
|
||||
if strings.HasPrefix(s, "+") {
|
||||
s = s[1:]
|
||||
}
|
||||
// "infinity" is needed for OpenMetrics support.
|
||||
// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
|
||||
if strings.EqualFold(s, "inf") || strings.EqualFold(s, "infinity") {
|
||||
if minus {
|
||||
return -inf
|
||||
}
|
||||
return inf
|
||||
}
|
||||
if strings.EqualFold(s, "nan") {
|
||||
return nan
|
||||
}
|
||||
return 0
|
||||
}
|
||||
f := float64(d)
|
||||
if i >= uint(len(s)) {
|
||||
// Fast path - just integer.
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
if s[i] == '.' {
|
||||
// Parse fractional part.
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
// the fractional part may be elided to remain compliant
|
||||
// with https://go.dev/ref/spec#Floating-point_literals
|
||||
return f
|
||||
}
|
||||
k := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i-j >= uint(len(float64pow10)) {
|
||||
// The mantissa is out of range. Fall back to standard parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i < k {
|
||||
return 0
|
||||
}
|
||||
// Convert the entire mantissa to a float at once to avoid rounding errors.
|
||||
f = float64(d) / float64pow10[i-k]
|
||||
if i >= uint(len(s)) {
|
||||
// Fast path - parsed fractional number.
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
||||
if s[i] == 'e' || s[i] == 'E' {
|
||||
// Parse exponent part.
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0
|
||||
}
|
||||
expMinus := false
|
||||
if s[i] == '+' || s[i] == '-' {
|
||||
expMinus = s[i] == '-'
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
exp := int16(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
exp = exp*10 + int16(s[i]-'0')
|
||||
i++
|
||||
if exp > 300 {
|
||||
// The exponent may be too big for float64.
|
||||
// Fall back to standard parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0
|
||||
}
|
||||
if expMinus {
|
||||
exp = -exp
|
||||
}
|
||||
f *= math.Pow10(int(exp))
|
||||
if i >= uint(len(s)) {
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Parse parses floating-point number s.
|
||||
//
|
||||
// It is equivalent to strconv.ParseFloat(s, 64), but is faster.
|
||||
//
|
||||
// See also ParseBestEffort.
|
||||
func Parse(s string) (float64, error) {
|
||||
if len(s) == 0 {
|
||||
return 0, fmt.Errorf("cannot parse float64 from empty string")
|
||||
}
|
||||
i := uint(0)
|
||||
minus := s[0] == '-'
|
||||
if minus {
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0, fmt.Errorf("cannot parse float64 from %q", s)
|
||||
}
|
||||
}
|
||||
|
||||
// the integer part might be elided to remain compliant
|
||||
// with https://go.dev/ref/spec#Floating-point_literals
|
||||
if s[i] == '.' && (i+1 >= uint(len(s)) || s[i+1] < '0' || s[i+1] > '9') {
|
||||
return 0, fmt.Errorf("missing integer and fractional part in %q", s)
|
||||
}
|
||||
|
||||
d := uint64(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i > 18 {
|
||||
// The integer part may be out of range for uint64.
|
||||
// Fall back to slow parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j && s[i] != '.' {
|
||||
ss := s[i:]
|
||||
if strings.HasPrefix(ss, "+") {
|
||||
ss = ss[1:]
|
||||
}
|
||||
// "infinity" is needed for OpenMetrics support.
|
||||
// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
|
||||
if strings.EqualFold(ss, "inf") || strings.EqualFold(ss, "infinity") {
|
||||
if minus {
|
||||
return -inf, nil
|
||||
}
|
||||
return inf, nil
|
||||
}
|
||||
if strings.EqualFold(ss, "nan") {
|
||||
return nan, nil
|
||||
}
|
||||
return 0, fmt.Errorf("unparsed tail left after parsing float64 from %q: %q", s, ss)
|
||||
}
|
||||
f := float64(d)
|
||||
if i >= uint(len(s)) {
|
||||
// Fast path - just integer.
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
if s[i] == '.' {
|
||||
// Parse fractional part.
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
// the fractional part might be elided to remain compliant
|
||||
// with https://go.dev/ref/spec#Floating-point_literals
|
||||
return f, nil
|
||||
}
|
||||
k := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
d = d*10 + uint64(s[i]-'0')
|
||||
i++
|
||||
if i-j >= uint(len(float64pow10)) {
|
||||
// The mantissa is out of range. Fall back to standard parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0, fmt.Errorf("cannot parse mantissa in %q: %s", s, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i < k {
|
||||
return 0, fmt.Errorf("cannot find mantissa in %q", s)
|
||||
}
|
||||
// Convert the entire mantissa to a float at once to avoid rounding errors.
|
||||
f = float64(d) / float64pow10[i-k]
|
||||
if i >= uint(len(s)) {
|
||||
// Fast path - parsed fractional number.
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
if s[i] == 'e' || s[i] == 'E' {
|
||||
// Parse exponent part.
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0, fmt.Errorf("cannot parse exponent in %q", s)
|
||||
}
|
||||
expMinus := false
|
||||
if s[i] == '+' || s[i] == '-' {
|
||||
expMinus = s[i] == '-'
|
||||
i++
|
||||
if i >= uint(len(s)) {
|
||||
return 0, fmt.Errorf("cannot parse exponent in %q", s)
|
||||
}
|
||||
}
|
||||
exp := int16(0)
|
||||
j := i
|
||||
for i < uint(len(s)) {
|
||||
if s[i] >= '0' && s[i] <= '9' {
|
||||
exp = exp*10 + int16(s[i]-'0')
|
||||
i++
|
||||
if exp > 300 {
|
||||
// The exponent may be too big for float64.
|
||||
// Fall back to standard parsing.
|
||||
f, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil && !math.IsInf(f, 0) {
|
||||
return 0, fmt.Errorf("cannot parse exponent in %q: %s", s, err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if i <= j {
|
||||
return 0, fmt.Errorf("cannot parse exponent in %q", s)
|
||||
}
|
||||
if expMinus {
|
||||
exp = -exp
|
||||
}
|
||||
f *= math.Pow10(int(exp))
|
||||
if i >= uint(len(s)) {
|
||||
if minus {
|
||||
f = -f
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("cannot parse float64 from %q", s)
|
||||
}
|
||||
|
||||
var inf = math.Inf(1)
|
||||
var nan = math.NaN()
|
|
@ -0,0 +1,22 @@
|
|||
// +build gofuzz
|
||||
|
||||
package fastjson
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
err := ValidateBytes(data)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
v := MustParseBytes(data)
|
||||
|
||||
dst := make([]byte, 0)
|
||||
dst = v.MarshalTo(dst)
|
||||
|
||||
err = ValidateBytes(dst)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package fastjson
|
||||
|
||||
var handyPool ParserPool
|
||||
|
||||
// GetString returns string value for the field identified by keys path
|
||||
// in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// An empty string is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster for obtaining multiple fields from JSON.
|
||||
func GetString(data []byte, keys ...string) string {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return ""
|
||||
}
|
||||
sb := v.GetStringBytes(keys...)
|
||||
str := string(sb)
|
||||
handyPool.Put(p)
|
||||
return str
|
||||
}
|
||||
|
||||
// GetBytes returns string value for the field identified by keys path
|
||||
// in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// nil is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster for obtaining multiple fields from JSON.
|
||||
func GetBytes(data []byte, keys ...string) []byte {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return nil
|
||||
}
|
||||
sb := v.GetStringBytes(keys...)
|
||||
|
||||
// Make a copy of sb, since sb belongs to p.
|
||||
var b []byte
|
||||
if sb != nil {
|
||||
b = append(b, sb...)
|
||||
}
|
||||
|
||||
handyPool.Put(p)
|
||||
return b
|
||||
}
|
||||
|
||||
// GetInt returns int value for the field identified by keys path
|
||||
// in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster for obtaining multiple fields from JSON.
|
||||
func GetInt(data []byte, keys ...string) int {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return 0
|
||||
}
|
||||
n := v.GetInt(keys...)
|
||||
handyPool.Put(p)
|
||||
return n
|
||||
}
|
||||
|
||||
// GetFloat64 returns float64 value for the field identified by keys path
|
||||
// in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster for obtaining multiple fields from JSON.
|
||||
func GetFloat64(data []byte, keys ...string) float64 {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return 0
|
||||
}
|
||||
f := v.GetFloat64(keys...)
|
||||
handyPool.Put(p)
|
||||
return f
|
||||
}
|
||||
|
||||
// GetBool returns boolean value for the field identified by keys path
|
||||
// in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// False is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster for obtaining multiple fields from JSON.
|
||||
func GetBool(data []byte, keys ...string) bool {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return false
|
||||
}
|
||||
b := v.GetBool(keys...)
|
||||
handyPool.Put(p)
|
||||
return b
|
||||
}
|
||||
|
||||
// Exists returns true if the field identified by keys path exists in JSON data.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// False is returned on error. Use Parser for proper error handling.
|
||||
//
|
||||
// Parser is faster when multiple fields must be checked in the JSON.
|
||||
func Exists(data []byte, keys ...string) bool {
|
||||
p := handyPool.Get()
|
||||
v, err := p.ParseBytes(data)
|
||||
if err != nil {
|
||||
handyPool.Put(p)
|
||||
return false
|
||||
}
|
||||
ok := v.Exists(keys...)
|
||||
handyPool.Put(p)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Parse parses json string s.
|
||||
//
|
||||
// The function is slower than the Parser.Parse for re-used Parser.
|
||||
func Parse(s string) (*Value, error) {
|
||||
var p Parser
|
||||
return p.Parse(s)
|
||||
}
|
||||
|
||||
// MustParse parses json string s.
|
||||
//
|
||||
// The function panics if s cannot be parsed.
|
||||
// The function is slower than the Parser.Parse for re-used Parser.
|
||||
func MustParse(s string) *Value {
|
||||
v, err := Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ParseBytes parses b containing json.
|
||||
//
|
||||
// The function is slower than the Parser.ParseBytes for re-used Parser.
|
||||
func ParseBytes(b []byte) (*Value, error) {
|
||||
var p Parser
|
||||
return p.ParseBytes(b)
|
||||
}
|
||||
|
||||
// MustParseBytes parses b containing json.
|
||||
//
|
||||
// The function panics if b cannot be parsed.
|
||||
// The function is slower than the Parser.ParseBytes for re-used Parser.
|
||||
func MustParseBytes(b []byte) *Value {
|
||||
v, err := ParseBytes(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
|
@ -0,0 +1,976 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/valyala/fastjson/fastfloat"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// Parser parses JSON.
|
||||
//
|
||||
// Parser may be re-used for subsequent parsing.
|
||||
//
|
||||
// Parser cannot be used from concurrent goroutines.
|
||||
// Use per-goroutine parsers or ParserPool instead.
|
||||
type Parser struct {
|
||||
// b contains working copy of the string to be parsed.
|
||||
b []byte
|
||||
|
||||
// c is a cache for json values.
|
||||
c cache
|
||||
}
|
||||
|
||||
// Parse parses s containing JSON.
|
||||
//
|
||||
// The returned value is valid until the next call to Parse*.
|
||||
//
|
||||
// Use Scanner if a stream of JSON values must be parsed.
|
||||
func (p *Parser) Parse(s string) (*Value, error) {
|
||||
s = skipWS(s)
|
||||
p.b = append(p.b[:0], s...)
|
||||
p.c.reset()
|
||||
|
||||
v, tail, err := parseValue(b2s(p.b), &p.c, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))
|
||||
}
|
||||
tail = skipWS(tail)
|
||||
if len(tail) > 0 {
|
||||
return nil, fmt.Errorf("unexpected tail: %q", startEndString(tail))
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// ParseBytes parses b containing JSON.
|
||||
//
|
||||
// The returned Value is valid until the next call to Parse*.
|
||||
//
|
||||
// Use Scanner if a stream of JSON values must be parsed.
|
||||
func (p *Parser) ParseBytes(b []byte) (*Value, error) {
|
||||
return p.Parse(b2s(b))
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
vs []Value
|
||||
}
|
||||
|
||||
func (c *cache) reset() {
|
||||
c.vs = c.vs[:0]
|
||||
}
|
||||
|
||||
func (c *cache) getValue() *Value {
|
||||
if cap(c.vs) > len(c.vs) {
|
||||
c.vs = c.vs[:len(c.vs)+1]
|
||||
} else {
|
||||
c.vs = append(c.vs, Value{})
|
||||
}
|
||||
// Do not reset the value, since the caller must properly init it.
|
||||
return &c.vs[len(c.vs)-1]
|
||||
}
|
||||
|
||||
func skipWS(s string) string {
|
||||
if len(s) == 0 || s[0] > 0x20 {
|
||||
// Fast path.
|
||||
return s
|
||||
}
|
||||
return skipWSSlow(s)
|
||||
}
|
||||
|
||||
func skipWSSlow(s string) string {
|
||||
if len(s) == 0 || s[0] != 0x20 && s[0] != 0x0A && s[0] != 0x09 && s[0] != 0x0D {
|
||||
return s
|
||||
}
|
||||
for i := 1; i < len(s); i++ {
|
||||
if s[i] != 0x20 && s[i] != 0x0A && s[i] != 0x09 && s[i] != 0x0D {
|
||||
return s[i:]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
k string
|
||||
v *Value
|
||||
}
|
||||
|
||||
// MaxDepth is the maximum depth for nested JSON.
|
||||
const MaxDepth = 300
|
||||
|
||||
func parseValue(s string, c *cache, depth int) (*Value, string, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, s, fmt.Errorf("cannot parse empty string")
|
||||
}
|
||||
depth++
|
||||
if depth > MaxDepth {
|
||||
return nil, s, fmt.Errorf("too big depth for the nested JSON; it exceeds %d", MaxDepth)
|
||||
}
|
||||
|
||||
if s[0] == '{' {
|
||||
v, tail, err := parseObject(s[1:], c, depth)
|
||||
if err != nil {
|
||||
return nil, tail, fmt.Errorf("cannot parse object: %s", err)
|
||||
}
|
||||
return v, tail, nil
|
||||
}
|
||||
if s[0] == '[' {
|
||||
v, tail, err := parseArray(s[1:], c, depth)
|
||||
if err != nil {
|
||||
return nil, tail, fmt.Errorf("cannot parse array: %s", err)
|
||||
}
|
||||
return v, tail, nil
|
||||
}
|
||||
if s[0] == '"' {
|
||||
ss, tail, err := parseRawString(s[1:])
|
||||
if err != nil {
|
||||
return nil, tail, fmt.Errorf("cannot parse string: %s", err)
|
||||
}
|
||||
v := c.getValue()
|
||||
v.t = typeRawString
|
||||
v.s = ss
|
||||
return v, tail, nil
|
||||
}
|
||||
if s[0] == 't' {
|
||||
if len(s) < len("true") || s[:len("true")] != "true" {
|
||||
return nil, s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return valueTrue, s[len("true"):], nil
|
||||
}
|
||||
if s[0] == 'f' {
|
||||
if len(s) < len("false") || s[:len("false")] != "false" {
|
||||
return nil, s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return valueFalse, s[len("false"):], nil
|
||||
}
|
||||
if s[0] == 'n' {
|
||||
if len(s) < len("null") || s[:len("null")] != "null" {
|
||||
// Try parsing NaN
|
||||
if len(s) >= 3 && strings.EqualFold(s[:3], "nan") {
|
||||
v := c.getValue()
|
||||
v.t = TypeNumber
|
||||
v.s = s[:3]
|
||||
return v, s[3:], nil
|
||||
}
|
||||
return nil, s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return valueNull, s[len("null"):], nil
|
||||
}
|
||||
|
||||
ns, tail, err := parseRawNumber(s)
|
||||
if err != nil {
|
||||
return nil, tail, fmt.Errorf("cannot parse number: %s", err)
|
||||
}
|
||||
v := c.getValue()
|
||||
v.t = TypeNumber
|
||||
v.s = ns
|
||||
return v, tail, nil
|
||||
}
|
||||
|
||||
func parseArray(s string, c *cache, depth int) (*Value, string, error) {
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return nil, s, fmt.Errorf("missing ']'")
|
||||
}
|
||||
|
||||
if s[0] == ']' {
|
||||
v := c.getValue()
|
||||
v.t = TypeArray
|
||||
v.a = v.a[:0]
|
||||
return v, s[1:], nil
|
||||
}
|
||||
|
||||
a := c.getValue()
|
||||
a.t = TypeArray
|
||||
a.a = a.a[:0]
|
||||
for {
|
||||
var v *Value
|
||||
var err error
|
||||
|
||||
s = skipWS(s)
|
||||
v, s, err = parseValue(s, c, depth)
|
||||
if err != nil {
|
||||
return nil, s, fmt.Errorf("cannot parse array value: %s", err)
|
||||
}
|
||||
a.a = append(a.a, v)
|
||||
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return nil, s, fmt.Errorf("unexpected end of array")
|
||||
}
|
||||
if s[0] == ',' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
if s[0] == ']' {
|
||||
s = s[1:]
|
||||
return a, s, nil
|
||||
}
|
||||
return nil, s, fmt.Errorf("missing ',' after array value")
|
||||
}
|
||||
}
|
||||
|
||||
func parseObject(s string, c *cache, depth int) (*Value, string, error) {
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return nil, s, fmt.Errorf("missing '}'")
|
||||
}
|
||||
|
||||
if s[0] == '}' {
|
||||
v := c.getValue()
|
||||
v.t = TypeObject
|
||||
v.o.reset()
|
||||
return v, s[1:], nil
|
||||
}
|
||||
|
||||
o := c.getValue()
|
||||
o.t = TypeObject
|
||||
o.o.reset()
|
||||
for {
|
||||
var err error
|
||||
kv := o.o.getKV()
|
||||
|
||||
// Parse key.
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 || s[0] != '"' {
|
||||
return nil, s, fmt.Errorf(`cannot find opening '"" for object key`)
|
||||
}
|
||||
kv.k, s, err = parseRawKey(s[1:])
|
||||
if err != nil {
|
||||
return nil, s, fmt.Errorf("cannot parse object key: %s", err)
|
||||
}
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 || s[0] != ':' {
|
||||
return nil, s, fmt.Errorf("missing ':' after object key")
|
||||
}
|
||||
s = s[1:]
|
||||
|
||||
// Parse value
|
||||
s = skipWS(s)
|
||||
kv.v, s, err = parseValue(s, c, depth)
|
||||
if err != nil {
|
||||
return nil, s, fmt.Errorf("cannot parse object value: %s", err)
|
||||
}
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return nil, s, fmt.Errorf("unexpected end of object")
|
||||
}
|
||||
if s[0] == ',' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
if s[0] == '}' {
|
||||
return o, s[1:], nil
|
||||
}
|
||||
return nil, s, fmt.Errorf("missing ',' after object value")
|
||||
}
|
||||
}
|
||||
|
||||
func escapeString(dst []byte, s string) []byte {
|
||||
if !hasSpecialChars(s) {
|
||||
// Fast path - nothing to escape.
|
||||
dst = append(dst, '"')
|
||||
dst = append(dst, s...)
|
||||
dst = append(dst, '"')
|
||||
return dst
|
||||
}
|
||||
|
||||
// Slow path.
|
||||
return strconv.AppendQuote(dst, s)
|
||||
}
|
||||
|
||||
func hasSpecialChars(s string) bool {
|
||||
if strings.IndexByte(s, '"') >= 0 || strings.IndexByte(s, '\\') >= 0 {
|
||||
return true
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < 0x20 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func unescapeStringBestEffort(s string) string {
|
||||
n := strings.IndexByte(s, '\\')
|
||||
if n < 0 {
|
||||
// Fast path - nothing to unescape.
|
||||
return s
|
||||
}
|
||||
|
||||
// Slow path - unescape string.
|
||||
b := s2b(s) // It is safe to do, since s points to a byte slice in Parser.b.
|
||||
b = b[:n]
|
||||
s = s[n+1:]
|
||||
for len(s) > 0 {
|
||||
ch := s[0]
|
||||
s = s[1:]
|
||||
switch ch {
|
||||
case '"':
|
||||
b = append(b, '"')
|
||||
case '\\':
|
||||
b = append(b, '\\')
|
||||
case '/':
|
||||
b = append(b, '/')
|
||||
case 'b':
|
||||
b = append(b, '\b')
|
||||
case 'f':
|
||||
b = append(b, '\f')
|
||||
case 'n':
|
||||
b = append(b, '\n')
|
||||
case 'r':
|
||||
b = append(b, '\r')
|
||||
case 't':
|
||||
b = append(b, '\t')
|
||||
case 'u':
|
||||
if len(s) < 4 {
|
||||
// Too short escape sequence. Just store it unchanged.
|
||||
b = append(b, "\\u"...)
|
||||
break
|
||||
}
|
||||
xs := s[:4]
|
||||
x, err := strconv.ParseUint(xs, 16, 16)
|
||||
if err != nil {
|
||||
// Invalid escape sequence. Just store it unchanged.
|
||||
b = append(b, "\\u"...)
|
||||
break
|
||||
}
|
||||
s = s[4:]
|
||||
if !utf16.IsSurrogate(rune(x)) {
|
||||
b = append(b, string(rune(x))...)
|
||||
break
|
||||
}
|
||||
|
||||
// Surrogate.
|
||||
// See https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Surrogates
|
||||
if len(s) < 6 || s[0] != '\\' || s[1] != 'u' {
|
||||
b = append(b, "\\u"...)
|
||||
b = append(b, xs...)
|
||||
break
|
||||
}
|
||||
x1, err := strconv.ParseUint(s[2:6], 16, 16)
|
||||
if err != nil {
|
||||
b = append(b, "\\u"...)
|
||||
b = append(b, xs...)
|
||||
break
|
||||
}
|
||||
r := utf16.DecodeRune(rune(x), rune(x1))
|
||||
b = append(b, string(r)...)
|
||||
s = s[6:]
|
||||
default:
|
||||
// Unknown escape sequence. Just store it unchanged.
|
||||
b = append(b, '\\', ch)
|
||||
}
|
||||
n = strings.IndexByte(s, '\\')
|
||||
if n < 0 {
|
||||
b = append(b, s...)
|
||||
break
|
||||
}
|
||||
b = append(b, s[:n]...)
|
||||
s = s[n+1:]
|
||||
}
|
||||
return b2s(b)
|
||||
}
|
||||
|
||||
// parseRawKey is similar to parseRawString, but is optimized
|
||||
// for small-sized keys without escape sequences.
|
||||
func parseRawKey(s string) (string, string, error) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '"' {
|
||||
// Fast path.
|
||||
return s[:i], s[i+1:], nil
|
||||
}
|
||||
if s[i] == '\\' {
|
||||
// Slow path.
|
||||
return parseRawString(s)
|
||||
}
|
||||
}
|
||||
return s, "", fmt.Errorf(`missing closing '"'`)
|
||||
}
|
||||
|
||||
func parseRawString(s string) (string, string, error) {
|
||||
n := strings.IndexByte(s, '"')
|
||||
if n < 0 {
|
||||
return s, "", fmt.Errorf(`missing closing '"'`)
|
||||
}
|
||||
if n == 0 || s[n-1] != '\\' {
|
||||
// Fast path. No escaped ".
|
||||
return s[:n], s[n+1:], nil
|
||||
}
|
||||
|
||||
// Slow path - possible escaped " found.
|
||||
ss := s
|
||||
for {
|
||||
i := n - 1
|
||||
for i > 0 && s[i-1] == '\\' {
|
||||
i--
|
||||
}
|
||||
if uint(n-i)%2 == 0 {
|
||||
return ss[:len(ss)-len(s)+n], s[n+1:], nil
|
||||
}
|
||||
s = s[n+1:]
|
||||
|
||||
n = strings.IndexByte(s, '"')
|
||||
if n < 0 {
|
||||
return ss, "", fmt.Errorf(`missing closing '"'`)
|
||||
}
|
||||
if n == 0 || s[n-1] != '\\' {
|
||||
return ss[:len(ss)-len(s)+n], s[n+1:], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseRawNumber(s string) (string, string, error) {
|
||||
// The caller must ensure len(s) > 0
|
||||
|
||||
// Find the end of the number.
|
||||
for i := 0; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'e' || ch == 'E' || ch == '+' {
|
||||
continue
|
||||
}
|
||||
if i == 0 || i == 1 && (s[0] == '-' || s[0] == '+') {
|
||||
if len(s[i:]) >= 3 {
|
||||
xs := s[i : i+3]
|
||||
if strings.EqualFold(xs, "inf") || strings.EqualFold(xs, "nan") {
|
||||
return s[:i+3], s[i+3:], nil
|
||||
}
|
||||
}
|
||||
return "", s, fmt.Errorf("unexpected char: %q", s[:1])
|
||||
}
|
||||
ns := s[:i]
|
||||
s = s[i:]
|
||||
return ns, s, nil
|
||||
}
|
||||
return s, "", nil
|
||||
}
|
||||
|
||||
// Object represents JSON object.
|
||||
//
|
||||
// Object cannot be used from concurrent goroutines.
|
||||
// Use per-goroutine parsers or ParserPool instead.
|
||||
type Object struct {
|
||||
kvs []kv
|
||||
keysUnescaped bool
|
||||
}
|
||||
|
||||
func (o *Object) reset() {
|
||||
o.kvs = o.kvs[:0]
|
||||
o.keysUnescaped = false
|
||||
}
|
||||
|
||||
// MarshalTo appends marshaled o to dst and returns the result.
|
||||
func (o *Object) MarshalTo(dst []byte) []byte {
|
||||
dst = append(dst, '{')
|
||||
for i, kv := range o.kvs {
|
||||
if o.keysUnescaped {
|
||||
dst = escapeString(dst, kv.k)
|
||||
} else {
|
||||
dst = append(dst, '"')
|
||||
dst = append(dst, kv.k...)
|
||||
dst = append(dst, '"')
|
||||
}
|
||||
dst = append(dst, ':')
|
||||
dst = kv.v.MarshalTo(dst)
|
||||
if i != len(o.kvs)-1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
}
|
||||
dst = append(dst, '}')
|
||||
return dst
|
||||
}
|
||||
|
||||
// String returns string representation for the o.
|
||||
//
|
||||
// This function is for debugging purposes only. It isn't optimized for speed.
|
||||
// See MarshalTo instead.
|
||||
func (o *Object) String() string {
|
||||
b := o.MarshalTo(nil)
|
||||
// It is safe converting b to string without allocation, since b is no longer
|
||||
// reachable after this line.
|
||||
return b2s(b)
|
||||
}
|
||||
|
||||
func (o *Object) getKV() *kv {
|
||||
if cap(o.kvs) > len(o.kvs) {
|
||||
o.kvs = o.kvs[:len(o.kvs)+1]
|
||||
} else {
|
||||
o.kvs = append(o.kvs, kv{})
|
||||
}
|
||||
return &o.kvs[len(o.kvs)-1]
|
||||
}
|
||||
|
||||
func (o *Object) unescapeKeys() {
|
||||
if o.keysUnescaped {
|
||||
return
|
||||
}
|
||||
kvs := o.kvs
|
||||
for i := range kvs {
|
||||
kv := &kvs[i]
|
||||
kv.k = unescapeStringBestEffort(kv.k)
|
||||
}
|
||||
o.keysUnescaped = true
|
||||
}
|
||||
|
||||
// Len returns the number of items in the o.
|
||||
func (o *Object) Len() int {
|
||||
return len(o.kvs)
|
||||
}
|
||||
|
||||
// Get returns the value for the given key in the o.
|
||||
//
|
||||
// Returns nil if the value for the given key isn't found.
|
||||
//
|
||||
// The returned value is valid until Parse is called on the Parser returned o.
|
||||
func (o *Object) Get(key string) *Value {
|
||||
if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 {
|
||||
// Fast path - try searching for the key without object keys unescaping.
|
||||
for _, kv := range o.kvs {
|
||||
if kv.k == key {
|
||||
return kv.v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path - unescape object keys.
|
||||
o.unescapeKeys()
|
||||
|
||||
for _, kv := range o.kvs {
|
||||
if kv.k == key {
|
||||
return kv.v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Visit calls f for each item in the o in the original order
|
||||
// of the parsed JSON.
|
||||
//
|
||||
// f cannot hold key and/or v after returning.
|
||||
func (o *Object) Visit(f func(key []byte, v *Value)) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
|
||||
o.unescapeKeys()
|
||||
|
||||
for _, kv := range o.kvs {
|
||||
f(s2b(kv.k), kv.v)
|
||||
}
|
||||
}
|
||||
|
||||
// Value represents any JSON value.
|
||||
//
|
||||
// Call Type in order to determine the actual type of the JSON value.
|
||||
//
|
||||
// Value cannot be used from concurrent goroutines.
|
||||
// Use per-goroutine parsers or ParserPool instead.
|
||||
type Value struct {
|
||||
o Object
|
||||
a []*Value
|
||||
s string
|
||||
t Type
|
||||
}
|
||||
|
||||
// MarshalTo appends marshaled v to dst and returns the result.
|
||||
func (v *Value) MarshalTo(dst []byte) []byte {
|
||||
switch v.t {
|
||||
case typeRawString:
|
||||
dst = append(dst, '"')
|
||||
dst = append(dst, v.s...)
|
||||
dst = append(dst, '"')
|
||||
return dst
|
||||
case TypeObject:
|
||||
return v.o.MarshalTo(dst)
|
||||
case TypeArray:
|
||||
dst = append(dst, '[')
|
||||
for i, vv := range v.a {
|
||||
dst = vv.MarshalTo(dst)
|
||||
if i != len(v.a)-1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
case TypeString:
|
||||
return escapeString(dst, v.s)
|
||||
case TypeNumber:
|
||||
return append(dst, v.s...)
|
||||
case TypeTrue:
|
||||
return append(dst, "true"...)
|
||||
case TypeFalse:
|
||||
return append(dst, "false"...)
|
||||
case TypeNull:
|
||||
return append(dst, "null"...)
|
||||
default:
|
||||
panic(fmt.Errorf("BUG: unexpected Value type: %d", v.t))
|
||||
}
|
||||
}
|
||||
|
||||
// String returns string representation of the v.
|
||||
//
|
||||
// The function is for debugging purposes only. It isn't optimized for speed.
|
||||
// See MarshalTo instead.
|
||||
//
|
||||
// Don't confuse this function with StringBytes, which must be called
|
||||
// for obtaining the underlying JSON string for the v.
|
||||
func (v *Value) String() string {
|
||||
b := v.MarshalTo(nil)
|
||||
// It is safe converting b to string without allocation, since b is no longer
|
||||
// reachable after this line.
|
||||
return b2s(b)
|
||||
}
|
||||
|
||||
// Type represents JSON type.
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// TypeNull is JSON null.
|
||||
TypeNull Type = 0
|
||||
|
||||
// TypeObject is JSON object type.
|
||||
TypeObject Type = 1
|
||||
|
||||
// TypeArray is JSON array type.
|
||||
TypeArray Type = 2
|
||||
|
||||
// TypeString is JSON string type.
|
||||
TypeString Type = 3
|
||||
|
||||
// TypeNumber is JSON number type.
|
||||
TypeNumber Type = 4
|
||||
|
||||
// TypeTrue is JSON true.
|
||||
TypeTrue Type = 5
|
||||
|
||||
// TypeFalse is JSON false.
|
||||
TypeFalse Type = 6
|
||||
|
||||
typeRawString Type = 7
|
||||
)
|
||||
|
||||
// String returns string representation of t.
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
case TypeObject:
|
||||
return "object"
|
||||
case TypeArray:
|
||||
return "array"
|
||||
case TypeString:
|
||||
return "string"
|
||||
case TypeNumber:
|
||||
return "number"
|
||||
case TypeTrue:
|
||||
return "true"
|
||||
case TypeFalse:
|
||||
return "false"
|
||||
case TypeNull:
|
||||
return "null"
|
||||
|
||||
// typeRawString is skipped intentionally,
|
||||
// since it shouldn't be visible to user.
|
||||
default:
|
||||
panic(fmt.Errorf("BUG: unknown Value type: %d", t))
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns the type of the v.
|
||||
func (v *Value) Type() Type {
|
||||
if v.t == typeRawString {
|
||||
v.s = unescapeStringBestEffort(v.s)
|
||||
v.t = TypeString
|
||||
}
|
||||
return v.t
|
||||
}
|
||||
|
||||
// Exists returns true if the field exists for the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
func (v *Value) Exists(keys ...string) bool {
|
||||
v = v.Get(keys...)
|
||||
return v != nil
|
||||
}
|
||||
|
||||
// Get returns value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// nil is returned for non-existing keys path.
|
||||
//
|
||||
// The returned value is valid until Parse is called on the Parser returned v.
|
||||
func (v *Value) Get(keys ...string) *Value {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
if v.t == TypeObject {
|
||||
v = v.o.Get(key)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
} else if v.t == TypeArray {
|
||||
n, err := strconv.Atoi(key)
|
||||
if err != nil || n < 0 || n >= len(v.a) {
|
||||
return nil
|
||||
}
|
||||
v = v.a[n]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// GetObject returns object value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// nil is returned for non-existing keys path or for invalid value type.
|
||||
//
|
||||
// The returned object is valid until Parse is called on the Parser returned v.
|
||||
func (v *Value) GetObject(keys ...string) *Object {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.t != TypeObject {
|
||||
return nil
|
||||
}
|
||||
return &v.o
|
||||
}
|
||||
|
||||
// GetArray returns array value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// nil is returned for non-existing keys path or for invalid value type.
|
||||
//
|
||||
// The returned array is valid until Parse is called on the Parser returned v.
|
||||
func (v *Value) GetArray(keys ...string) []*Value {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.t != TypeArray {
|
||||
return nil
|
||||
}
|
||||
return v.a
|
||||
}
|
||||
|
||||
// GetFloat64 returns float64 value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetFloat64(keys ...string) float64 {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeNumber {
|
||||
return 0
|
||||
}
|
||||
return fastfloat.ParseBestEffort(v.s)
|
||||
}
|
||||
|
||||
// GetInt returns int value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetInt(keys ...string) int {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeNumber {
|
||||
return 0
|
||||
}
|
||||
n := fastfloat.ParseInt64BestEffort(v.s)
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// GetUint returns uint value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetUint(keys ...string) uint {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeNumber {
|
||||
return 0
|
||||
}
|
||||
n := fastfloat.ParseUint64BestEffort(v.s)
|
||||
nn := uint(n)
|
||||
if uint64(nn) != n {
|
||||
return 0
|
||||
}
|
||||
return nn
|
||||
}
|
||||
|
||||
// GetInt64 returns int64 value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetInt64(keys ...string) int64 {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeNumber {
|
||||
return 0
|
||||
}
|
||||
return fastfloat.ParseInt64BestEffort(v.s)
|
||||
}
|
||||
|
||||
// GetUint64 returns uint64 value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// 0 is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetUint64(keys ...string) uint64 {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeNumber {
|
||||
return 0
|
||||
}
|
||||
return fastfloat.ParseUint64BestEffort(v.s)
|
||||
}
|
||||
|
||||
// GetStringBytes returns string value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// nil is returned for non-existing keys path or for invalid value type.
|
||||
//
|
||||
// The returned string is valid until Parse is called on the Parser returned v.
|
||||
func (v *Value) GetStringBytes(keys ...string) []byte {
|
||||
v = v.Get(keys...)
|
||||
if v == nil || v.Type() != TypeString {
|
||||
return nil
|
||||
}
|
||||
return s2b(v.s)
|
||||
}
|
||||
|
||||
// GetBool returns bool value by the given keys path.
|
||||
//
|
||||
// Array indexes may be represented as decimal numbers in keys.
|
||||
//
|
||||
// false is returned for non-existing keys path or for invalid value type.
|
||||
func (v *Value) GetBool(keys ...string) bool {
|
||||
v = v.Get(keys...)
|
||||
if v != nil && v.t == TypeTrue {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Object returns the underlying JSON object for the v.
|
||||
//
|
||||
// The returned object is valid until Parse is called on the Parser returned v.
|
||||
//
|
||||
// Use GetObject if you don't need error handling.
|
||||
func (v *Value) Object() (*Object, error) {
|
||||
if v.t != TypeObject {
|
||||
return nil, fmt.Errorf("value doesn't contain object; it contains %s", v.Type())
|
||||
}
|
||||
return &v.o, nil
|
||||
}
|
||||
|
||||
// Array returns the underlying JSON array for the v.
|
||||
//
|
||||
// The returned array is valid until Parse is called on the Parser returned v.
|
||||
//
|
||||
// Use GetArray if you don't need error handling.
|
||||
func (v *Value) Array() ([]*Value, error) {
|
||||
if v.t != TypeArray {
|
||||
return nil, fmt.Errorf("value doesn't contain array; it contains %s", v.Type())
|
||||
}
|
||||
return v.a, nil
|
||||
}
|
||||
|
||||
// StringBytes returns the underlying JSON string for the v.
|
||||
//
|
||||
// The returned string is valid until Parse is called on the Parser returned v.
|
||||
//
|
||||
// Use GetStringBytes if you don't need error handling.
|
||||
func (v *Value) StringBytes() ([]byte, error) {
|
||||
if v.Type() != TypeString {
|
||||
return nil, fmt.Errorf("value doesn't contain string; it contains %s", v.Type())
|
||||
}
|
||||
return s2b(v.s), nil
|
||||
}
|
||||
|
||||
// Float64 returns the underlying JSON number for the v.
|
||||
//
|
||||
// Use GetFloat64 if you don't need error handling.
|
||||
func (v *Value) Float64() (float64, error) {
|
||||
if v.Type() != TypeNumber {
|
||||
return 0, fmt.Errorf("value doesn't contain number; it contains %s", v.Type())
|
||||
}
|
||||
return fastfloat.Parse(v.s)
|
||||
}
|
||||
|
||||
// Int returns the underlying JSON int for the v.
|
||||
//
|
||||
// Use GetInt if you don't need error handling.
|
||||
func (v *Value) Int() (int, error) {
|
||||
if v.Type() != TypeNumber {
|
||||
return 0, fmt.Errorf("value doesn't contain number; it contains %s", v.Type())
|
||||
}
|
||||
n, err := fastfloat.ParseInt64(v.s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0, fmt.Errorf("number %q doesn't fit int", v.s)
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
// Uint returns the underlying JSON uint for the v.
|
||||
//
|
||||
// Use GetInt if you don't need error handling.
|
||||
func (v *Value) Uint() (uint, error) {
|
||||
if v.Type() != TypeNumber {
|
||||
return 0, fmt.Errorf("value doesn't contain number; it contains %s", v.Type())
|
||||
}
|
||||
n, err := fastfloat.ParseUint64(v.s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nn := uint(n)
|
||||
if uint64(nn) != n {
|
||||
return 0, fmt.Errorf("number %q doesn't fit uint", v.s)
|
||||
}
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
// Int64 returns the underlying JSON int64 for the v.
|
||||
//
|
||||
// Use GetInt64 if you don't need error handling.
|
||||
func (v *Value) Int64() (int64, error) {
|
||||
if v.Type() != TypeNumber {
|
||||
return 0, fmt.Errorf("value doesn't contain number; it contains %s", v.Type())
|
||||
}
|
||||
return fastfloat.ParseInt64(v.s)
|
||||
}
|
||||
|
||||
// Uint64 returns the underlying JSON uint64 for the v.
|
||||
//
|
||||
// Use GetInt64 if you don't need error handling.
|
||||
func (v *Value) Uint64() (uint64, error) {
|
||||
if v.Type() != TypeNumber {
|
||||
return 0, fmt.Errorf("value doesn't contain number; it contains %s", v.Type())
|
||||
}
|
||||
return fastfloat.ParseUint64(v.s)
|
||||
}
|
||||
|
||||
// Bool returns the underlying JSON bool for the v.
|
||||
//
|
||||
// Use GetBool if you don't need error handling.
|
||||
func (v *Value) Bool() (bool, error) {
|
||||
if v.t == TypeTrue {
|
||||
return true, nil
|
||||
}
|
||||
if v.t == TypeFalse {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("value doesn't contain bool; it contains %s", v.Type())
|
||||
}
|
||||
|
||||
var (
|
||||
valueTrue = &Value{t: TypeTrue}
|
||||
valueFalse = &Value{t: TypeFalse}
|
||||
valueNull = &Value{t: TypeNull}
|
||||
)
|
|
@ -0,0 +1,52 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ParserPool may be used for pooling Parsers for similarly typed JSONs.
|
||||
type ParserPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// Get returns a Parser from pp.
|
||||
//
|
||||
// The Parser must be Put to pp after use.
|
||||
func (pp *ParserPool) Get() *Parser {
|
||||
v := pp.pool.Get()
|
||||
if v == nil {
|
||||
return &Parser{}
|
||||
}
|
||||
return v.(*Parser)
|
||||
}
|
||||
|
||||
// Put returns p to pp.
|
||||
//
|
||||
// p and objects recursively returned from p cannot be used after p
|
||||
// is put into pp.
|
||||
func (pp *ParserPool) Put(p *Parser) {
|
||||
pp.pool.Put(p)
|
||||
}
|
||||
|
||||
// ArenaPool may be used for pooling Arenas for similarly typed JSONs.
|
||||
type ArenaPool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// Get returns an Arena from ap.
|
||||
//
|
||||
// The Arena must be Put to ap after use.
|
||||
func (ap *ArenaPool) Get() *Arena {
|
||||
v := ap.pool.Get()
|
||||
if v == nil {
|
||||
return &Arena{}
|
||||
}
|
||||
return v.(*Arena)
|
||||
}
|
||||
|
||||
// Put returns a to ap.
|
||||
//
|
||||
// a and objects created by a cannot be used after a is put into ap.
|
||||
func (ap *ArenaPool) Put(a *Arena) {
|
||||
ap.pool.Put(a)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Scanner scans a series of JSON values. Values may be delimited by whitespace.
|
||||
//
|
||||
// Scanner may parse JSON lines ( http://jsonlines.org/ ).
|
||||
//
|
||||
// Scanner may be re-used for subsequent parsing.
|
||||
//
|
||||
// Scanner cannot be used from concurrent goroutines.
|
||||
//
|
||||
// Use Parser for parsing only a single JSON value.
|
||||
type Scanner struct {
|
||||
// b contains a working copy of json value passed to Init.
|
||||
b []byte
|
||||
|
||||
// s points to the next JSON value to parse.
|
||||
s string
|
||||
|
||||
// err contains the last error.
|
||||
err error
|
||||
|
||||
// v contains the last parsed JSON value.
|
||||
v *Value
|
||||
|
||||
// c is used for caching JSON values.
|
||||
c cache
|
||||
}
|
||||
|
||||
// Init initializes sc with the given s.
|
||||
//
|
||||
// s may contain multiple JSON values, which may be delimited by whitespace.
|
||||
func (sc *Scanner) Init(s string) {
|
||||
sc.b = append(sc.b[:0], s...)
|
||||
sc.s = b2s(sc.b)
|
||||
sc.err = nil
|
||||
sc.v = nil
|
||||
}
|
||||
|
||||
// InitBytes initializes sc with the given b.
|
||||
//
|
||||
// b may contain multiple JSON values, which may be delimited by whitespace.
|
||||
func (sc *Scanner) InitBytes(b []byte) {
|
||||
sc.Init(b2s(b))
|
||||
}
|
||||
|
||||
// Next parses the next JSON value from s passed to Init.
|
||||
//
|
||||
// Returns true on success. The parsed value is available via Value call.
|
||||
//
|
||||
// Returns false either on error or on the end of s.
|
||||
// Call Error in order to determine the cause of the returned false.
|
||||
func (sc *Scanner) Next() bool {
|
||||
if sc.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
sc.s = skipWS(sc.s)
|
||||
if len(sc.s) == 0 {
|
||||
sc.err = errEOF
|
||||
return false
|
||||
}
|
||||
|
||||
sc.c.reset()
|
||||
v, tail, err := parseValue(sc.s, &sc.c, 0)
|
||||
if err != nil {
|
||||
sc.err = err
|
||||
return false
|
||||
}
|
||||
|
||||
sc.s = tail
|
||||
sc.v = v
|
||||
return true
|
||||
}
|
||||
|
||||
// Error returns the last error.
|
||||
func (sc *Scanner) Error() error {
|
||||
if sc.err == errEOF {
|
||||
return nil
|
||||
}
|
||||
return sc.err
|
||||
}
|
||||
|
||||
// Value returns the last parsed value.
|
||||
//
|
||||
// The value is valid until the Next call.
|
||||
func (sc *Scanner) Value() *Value {
|
||||
return sc.v
|
||||
}
|
||||
|
||||
var errEOF = errors.New("end of s")
|
|
@ -0,0 +1,110 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Del deletes the entry with the given key from o.
|
||||
func (o *Object) Del(key string) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 {
|
||||
// Fast path - try searching for the key without object keys unescaping.
|
||||
for i, kv := range o.kvs {
|
||||
if kv.k == key {
|
||||
o.kvs = append(o.kvs[:i], o.kvs[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path - unescape object keys before item search.
|
||||
o.unescapeKeys()
|
||||
|
||||
for i, kv := range o.kvs {
|
||||
if kv.k == key {
|
||||
o.kvs = append(o.kvs[:i], o.kvs[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Del deletes the entry with the given key from array or object v.
|
||||
func (v *Value) Del(key string) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
if v.t == TypeObject {
|
||||
v.o.Del(key)
|
||||
return
|
||||
}
|
||||
if v.t == TypeArray {
|
||||
n, err := strconv.Atoi(key)
|
||||
if err != nil || n < 0 || n >= len(v.a) {
|
||||
return
|
||||
}
|
||||
v.a = append(v.a[:n], v.a[n+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets (key, value) entry in the o.
|
||||
//
|
||||
// The value must be unchanged during o lifetime.
|
||||
func (o *Object) Set(key string, value *Value) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
if value == nil {
|
||||
value = valueNull
|
||||
}
|
||||
o.unescapeKeys()
|
||||
|
||||
// Try substituting already existing entry with the given key.
|
||||
for i := range o.kvs {
|
||||
kv := &o.kvs[i]
|
||||
if kv.k == key {
|
||||
kv.v = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add new entry.
|
||||
kv := o.getKV()
|
||||
kv.k = key
|
||||
kv.v = value
|
||||
}
|
||||
|
||||
// Set sets (key, value) entry in the array or object v.
|
||||
//
|
||||
// The value must be unchanged during v lifetime.
|
||||
func (v *Value) Set(key string, value *Value) {
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
if v.t == TypeObject {
|
||||
v.o.Set(key, value)
|
||||
return
|
||||
}
|
||||
if v.t == TypeArray {
|
||||
idx, err := strconv.Atoi(key)
|
||||
if err != nil || idx < 0 {
|
||||
return
|
||||
}
|
||||
v.SetArrayItem(idx, value)
|
||||
}
|
||||
}
|
||||
|
||||
// SetArrayItem sets the value in the array v at idx position.
|
||||
//
|
||||
// The value must be unchanged during v lifetime.
|
||||
func (v *Value) SetArrayItem(idx int, value *Value) {
|
||||
if v == nil || v.t != TypeArray {
|
||||
return
|
||||
}
|
||||
for idx >= len(v.a) {
|
||||
v.a = append(v.a, valueNull)
|
||||
}
|
||||
v.a[idx] = value
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
func s2b(s string) (b []byte) {
|
||||
strh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
sh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
sh.Data = strh.Data
|
||||
sh.Len = strh.Len
|
||||
sh.Cap = strh.Len
|
||||
return b
|
||||
}
|
||||
|
||||
const maxStartEndStringLen = 80
|
||||
|
||||
func startEndString(s string) string {
|
||||
if len(s) <= maxStartEndStringLen {
|
||||
return s
|
||||
}
|
||||
start := s[:40]
|
||||
end := s[len(s)-40:]
|
||||
return start + "..." + end
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
package fastjson
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Validate validates JSON s.
|
||||
func Validate(s string) error {
|
||||
s = skipWS(s)
|
||||
|
||||
tail, err := validateValue(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))
|
||||
}
|
||||
tail = skipWS(tail)
|
||||
if len(tail) > 0 {
|
||||
return fmt.Errorf("unexpected tail: %q", startEndString(tail))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateBytes validates JSON b.
|
||||
func ValidateBytes(b []byte) error {
|
||||
return Validate(b2s(b))
|
||||
}
|
||||
|
||||
func validateValue(s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("cannot parse empty string")
|
||||
}
|
||||
|
||||
if s[0] == '{' {
|
||||
tail, err := validateObject(s[1:])
|
||||
if err != nil {
|
||||
return tail, fmt.Errorf("cannot parse object: %s", err)
|
||||
}
|
||||
return tail, nil
|
||||
}
|
||||
if s[0] == '[' {
|
||||
tail, err := validateArray(s[1:])
|
||||
if err != nil {
|
||||
return tail, fmt.Errorf("cannot parse array: %s", err)
|
||||
}
|
||||
return tail, nil
|
||||
}
|
||||
if s[0] == '"' {
|
||||
sv, tail, err := validateString(s[1:])
|
||||
if err != nil {
|
||||
return tail, fmt.Errorf("cannot parse string: %s", err)
|
||||
}
|
||||
// Scan the string for control chars.
|
||||
for i := 0; i < len(sv); i++ {
|
||||
if sv[i] < 0x20 {
|
||||
return tail, fmt.Errorf("string cannot contain control char 0x%02X", sv[i])
|
||||
}
|
||||
}
|
||||
return tail, nil
|
||||
}
|
||||
if s[0] == 't' {
|
||||
if len(s) < len("true") || s[:len("true")] != "true" {
|
||||
return s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return s[len("true"):], nil
|
||||
}
|
||||
if s[0] == 'f' {
|
||||
if len(s) < len("false") || s[:len("false")] != "false" {
|
||||
return s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return s[len("false"):], nil
|
||||
}
|
||||
if s[0] == 'n' {
|
||||
if len(s) < len("null") || s[:len("null")] != "null" {
|
||||
return s, fmt.Errorf("unexpected value found: %q", s)
|
||||
}
|
||||
return s[len("null"):], nil
|
||||
}
|
||||
|
||||
tail, err := validateNumber(s)
|
||||
if err != nil {
|
||||
return tail, fmt.Errorf("cannot parse number: %s", err)
|
||||
}
|
||||
return tail, nil
|
||||
}
|
||||
|
||||
func validateArray(s string) (string, error) {
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing ']'")
|
||||
}
|
||||
if s[0] == ']' {
|
||||
return s[1:], nil
|
||||
}
|
||||
|
||||
for {
|
||||
var err error
|
||||
|
||||
s = skipWS(s)
|
||||
s, err = validateValue(s)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("cannot parse array value: %s", err)
|
||||
}
|
||||
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("unexpected end of array")
|
||||
}
|
||||
if s[0] == ',' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
if s[0] == ']' {
|
||||
s = s[1:]
|
||||
return s, nil
|
||||
}
|
||||
return s, fmt.Errorf("missing ',' after array value")
|
||||
}
|
||||
}
|
||||
|
||||
func validateObject(s string) (string, error) {
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing '}'")
|
||||
}
|
||||
if s[0] == '}' {
|
||||
return s[1:], nil
|
||||
}
|
||||
|
||||
for {
|
||||
var err error
|
||||
|
||||
// Parse key.
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 || s[0] != '"' {
|
||||
return s, fmt.Errorf(`cannot find opening '"" for object key`)
|
||||
}
|
||||
|
||||
var key string
|
||||
key, s, err = validateKey(s[1:])
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("cannot parse object key: %s", err)
|
||||
}
|
||||
// Scan the key for control chars.
|
||||
for i := 0; i < len(key); i++ {
|
||||
if key[i] < 0x20 {
|
||||
return s, fmt.Errorf("object key cannot contain control char 0x%02X", key[i])
|
||||
}
|
||||
}
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 || s[0] != ':' {
|
||||
return s, fmt.Errorf("missing ':' after object key")
|
||||
}
|
||||
s = s[1:]
|
||||
|
||||
// Parse value
|
||||
s = skipWS(s)
|
||||
s, err = validateValue(s)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("cannot parse object value: %s", err)
|
||||
}
|
||||
s = skipWS(s)
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("unexpected end of object")
|
||||
}
|
||||
if s[0] == ',' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
if s[0] == '}' {
|
||||
return s[1:], nil
|
||||
}
|
||||
return s, fmt.Errorf("missing ',' after object value")
|
||||
}
|
||||
}
|
||||
|
||||
// validateKey is similar to validateString, but is optimized
|
||||
// for typical object keys, which are quite small and have no escape sequences.
|
||||
func validateKey(s string) (string, string, error) {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '"' {
|
||||
// Fast path - the key doesn't contain escape sequences.
|
||||
return s[:i], s[i+1:], nil
|
||||
}
|
||||
if s[i] == '\\' {
|
||||
// Slow path - the key contains escape sequences.
|
||||
return validateString(s)
|
||||
}
|
||||
}
|
||||
return "", s, fmt.Errorf(`missing closing '"'`)
|
||||
}
|
||||
|
||||
func validateString(s string) (string, string, error) {
|
||||
// Try fast path - a string without escape sequences.
|
||||
if n := strings.IndexByte(s, '"'); n >= 0 && strings.IndexByte(s[:n], '\\') < 0 {
|
||||
return s[:n], s[n+1:], nil
|
||||
}
|
||||
|
||||
// Slow path - escape sequences are present.
|
||||
rs, tail, err := parseRawString(s)
|
||||
if err != nil {
|
||||
return rs, tail, err
|
||||
}
|
||||
for {
|
||||
n := strings.IndexByte(rs, '\\')
|
||||
if n < 0 {
|
||||
return rs, tail, nil
|
||||
}
|
||||
n++
|
||||
if n >= len(rs) {
|
||||
return rs, tail, fmt.Errorf("BUG: parseRawString returned invalid string with trailing backslash: %q", rs)
|
||||
}
|
||||
ch := rs[n]
|
||||
rs = rs[n+1:]
|
||||
switch ch {
|
||||
case '"', '\\', '/', 'b', 'f', 'n', 'r', 't':
|
||||
// Valid escape sequences - see http://json.org/
|
||||
break
|
||||
case 'u':
|
||||
if len(rs) < 4 {
|
||||
return rs, tail, fmt.Errorf(`too short escape sequence: \u%s`, rs)
|
||||
}
|
||||
xs := rs[:4]
|
||||
_, err := strconv.ParseUint(xs, 16, 16)
|
||||
if err != nil {
|
||||
return rs, tail, fmt.Errorf(`invalid escape sequence \u%s: %s`, xs, err)
|
||||
}
|
||||
rs = rs[4:]
|
||||
default:
|
||||
return rs, tail, fmt.Errorf(`unknown escape sequence \%c`, ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateNumber(s string) (string, error) {
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("zero-length number")
|
||||
}
|
||||
if s[0] == '-' {
|
||||
s = s[1:]
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing number after minus")
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i <= 0 {
|
||||
return s, fmt.Errorf("expecting 0..9 digit, got %c", s[0])
|
||||
}
|
||||
if s[0] == '0' && i != 1 {
|
||||
return s, fmt.Errorf("unexpected number starting from 0")
|
||||
}
|
||||
if i >= len(s) {
|
||||
return "", nil
|
||||
}
|
||||
if s[i] == '.' {
|
||||
// Validate fractional part
|
||||
s = s[i+1:]
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing fractional part")
|
||||
}
|
||||
i = 0
|
||||
for i < len(s) {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i == 0 {
|
||||
return s, fmt.Errorf("expecting 0..9 digit in fractional part, got %c", s[0])
|
||||
}
|
||||
if i >= len(s) {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
if s[i] == 'e' || s[i] == 'E' {
|
||||
// Validate exponent part
|
||||
s = s[i+1:]
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing exponent part")
|
||||
}
|
||||
if s[0] == '-' || s[0] == '+' {
|
||||
s = s[1:]
|
||||
if len(s) == 0 {
|
||||
return s, fmt.Errorf("missing exponent part")
|
||||
}
|
||||
}
|
||||
i = 0
|
||||
for i < len(s) {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i == 0 {
|
||||
return s, fmt.Errorf("expecting 0..9 digit in exponent part, got %c", s[0])
|
||||
}
|
||||
if i >= len(s) {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
return s[i:], nil
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
# git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078
|
||||
## explicit; go 1.14
|
||||
git.sr.ht/~mariusor/go-xsd-duration
|
||||
# github.com/BurntSushi/toml v1.3.2
|
||||
## explicit; go 1.16
|
||||
github.com/BurntSushi/toml
|
||||
|
@ -8,6 +11,15 @@ github.com/adrg/frontmatter
|
|||
# github.com/caarlos0/env/v10 v10.0.0
|
||||
## explicit; go 1.17
|
||||
github.com/caarlos0/env/v10
|
||||
# github.com/go-ap/activitypub v0.0.0-20240211124657-820024a66b78
|
||||
## explicit; go 1.18
|
||||
github.com/go-ap/activitypub
|
||||
# github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7
|
||||
## explicit; go 1.13
|
||||
github.com/go-ap/errors
|
||||
# github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
|
||||
## explicit; go 1.13
|
||||
github.com/go-ap/jsonld
|
||||
# github.com/google/go-cmp v0.6.0
|
||||
## explicit; go 1.13
|
||||
github.com/google/go-cmp/cmp
|
||||
|
@ -18,6 +30,10 @@ github.com/google/go-cmp/cmp/internal/value
|
|||
# github.com/valyala/bytebufferpool v1.0.0
|
||||
## explicit
|
||||
github.com/valyala/bytebufferpool
|
||||
# github.com/valyala/fastjson v1.6.4
|
||||
## explicit; go 1.12
|
||||
github.com/valyala/fastjson
|
||||
github.com/valyala/fastjson/fastfloat
|
||||
# github.com/valyala/quicktemplate v1.7.0
|
||||
## explicit; go 1.11
|
||||
github.com/valyala/quicktemplate
|
||||
|
|
|
@ -43,7 +43,7 @@ func NewBaseOf(site *domain.Site) BaseOf {
|
|||
{% endfunc %}
|
||||
|
||||
{% func (b BaseOf) Dir() %}
|
||||
{%s b.site.Language.Dir() %}
|
||||
{%s b.site.Language.Dir().String() %}
|
||||
{% endfunc %}
|
||||
|
||||
{% func Template(p Pager) %}
|
||||
|
|
|
@ -206,7 +206,7 @@ func (b BaseOf) Lang() string {
|
|||
//line web/template/baseof.qtpl:45
|
||||
func (b BaseOf) StreamDir(qw422016 *qt422016.Writer) {
|
||||
//line web/template/baseof.qtpl:46
|
||||
qw422016.E().S(b.site.Language.Dir())
|
||||
qw422016.E().S(b.site.Language.Dir().String())
|
||||
//line web/template/baseof.qtpl:47
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
{% code
|
||||
type Page struct {
|
||||
BaseOf
|
||||
page *domain.Page
|
||||
page *domain.Entry
|
||||
}
|
||||
|
||||
func NewPage(base BaseOf, page *domain.Page) Page {
|
||||
func NewPage(base BaseOf, page *domain.Entry) Page {
|
||||
return Page{
|
||||
BaseOf: base,
|
||||
page: page,
|
||||
|
@ -35,7 +35,7 @@ func NewPage(base BaseOf, page *domain.Page) Page {
|
|||
|
||||
{% func (p Page) Dir() %}
|
||||
{% if p.page.Language != domain.LanguageUnd %}
|
||||
{%s p.page.Language.Dir() %}
|
||||
{%s p.page.Language.Dir().String() %}
|
||||
{% else %}
|
||||
{%= p.BaseOf.Lang() %}
|
||||
{% endif %}
|
||||
|
|
|
@ -25,10 +25,10 @@ var (
|
|||
//line web/template/page.qtpl:6
|
||||
type Page struct {
|
||||
BaseOf
|
||||
page *domain.Page
|
||||
page *domain.Entry
|
||||
}
|
||||
|
||||
func NewPage(base BaseOf, page *domain.Page) Page {
|
||||
func NewPage(base BaseOf, page *domain.Entry) Page {
|
||||
return Page{
|
||||
BaseOf: base,
|
||||
page: page,
|
||||
|
@ -122,7 +122,7 @@ func (p Page) StreamDir(qw422016 *qt422016.Writer) {
|
|||
//line web/template/page.qtpl:37
|
||||
if p.page.Language != domain.LanguageUnd {
|
||||
//line web/template/page.qtpl:38
|
||||
qw422016.E().S(p.page.Language.Dir())
|
||||
qw422016.E().S(p.page.Language.Dir().String())
|
||||
//line web/template/page.qtpl:39
|
||||
} else {
|
||||
//line web/template/page.qtpl:40
|
||||
|
|
Loading…
Reference in New Issue