2018-10-11 09:26:00 +00:00
|
|
|
package activitystreams
|
2018-07-24 21:11:08 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding"
|
|
|
|
"encoding/json"
|
2018-11-21 11:25:29 +00:00
|
|
|
"fmt"
|
2018-08-06 11:10:34 +00:00
|
|
|
"net/url"
|
2018-07-24 21:11:08 +00:00
|
|
|
"reflect"
|
2018-08-06 17:11:24 +00:00
|
|
|
"time"
|
2018-07-24 21:11:08 +00:00
|
|
|
|
|
|
|
"github.com/buger/jsonparser"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2018-10-04 18:33:32 +00:00
|
|
|
apUnmarshalerType = reflect.TypeOf(new(Item)).Elem()
|
2018-10-29 16:26:21 +00:00
|
|
|
unmarshalerType = reflect.TypeOf(new(json.Unmarshaler)).Elem()
|
2018-07-24 21:11:08 +00:00
|
|
|
textUnmarshalerType = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem()
|
|
|
|
)
|
|
|
|
|
2019-01-30 16:39:26 +00:00
|
|
|
// ItemTyperFunc will return an instance of a struct that implements activitystreams.Item
|
|
|
|
// The default for this package is JSONGetItemByType but can be overwritten
|
|
|
|
var ItemTyperFunc TyperFunction = JSONGetItemByType
|
2018-07-24 21:11:08 +00:00
|
|
|
|
2019-01-30 16:39:26 +00:00
|
|
|
// TyperFunction is the type of the function which returns an activitystreams.Item struct instance
|
|
|
|
// for a specific ActivityVocabularyType
|
|
|
|
type TyperFunction func(ActivityVocabularyType) (Item, error)
|
2018-07-24 21:11:08 +00:00
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetObjectID(data []byte) ObjectID {
|
2018-07-24 21:11:08 +00:00
|
|
|
i, err := jsonparser.GetString(data, "id")
|
|
|
|
if err != nil {
|
|
|
|
return ObjectID("")
|
|
|
|
}
|
|
|
|
return ObjectID(i)
|
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetType(data []byte) ActivityVocabularyType {
|
2018-07-24 21:11:08 +00:00
|
|
|
t, err := jsonparser.GetString(data, "type")
|
|
|
|
typ := ActivityVocabularyType(t)
|
|
|
|
if err != nil {
|
|
|
|
return ActivityVocabularyType("")
|
|
|
|
}
|
|
|
|
return typ
|
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetMimeType(data []byte) MimeType {
|
2018-07-24 21:11:08 +00:00
|
|
|
t, err := jsonparser.GetString(data, "mediaType")
|
|
|
|
if err != nil {
|
|
|
|
return MimeType("")
|
|
|
|
}
|
|
|
|
return MimeType(t)
|
|
|
|
}
|
2018-11-21 11:25:29 +00:00
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetInt(data []byte, prop string) int64 {
|
2018-08-05 11:54:01 +00:00
|
|
|
val, err := jsonparser.GetInt(data, prop)
|
|
|
|
if err != nil {
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
2018-07-24 21:11:08 +00:00
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetString(data []byte, prop string) string {
|
2018-08-05 14:33:51 +00:00
|
|
|
val, err := jsonparser.GetString(data, prop)
|
|
|
|
if err != nil {
|
|
|
|
}
|
|
|
|
return val
|
|
|
|
}
|
|
|
|
|
2019-02-21 16:54:11 +00:00
|
|
|
func JSONGetNaturalLanguageField(data []byte, prop string) NaturalLanguageValues {
|
|
|
|
n := NaturalLanguageValues{}
|
2018-07-24 21:11:08 +00:00
|
|
|
val, typ, _, err := jsonparser.Get(data, prop)
|
|
|
|
if err != nil {
|
2018-08-31 11:38:15 +00:00
|
|
|
return nil
|
2018-07-24 21:11:08 +00:00
|
|
|
}
|
|
|
|
switch typ {
|
|
|
|
case jsonparser.Object:
|
|
|
|
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
|
2018-08-31 11:38:15 +00:00
|
|
|
if dataType == jsonparser.String {
|
|
|
|
n.Append(LangRef(key), string(value))
|
|
|
|
}
|
2018-07-24 21:11:08 +00:00
|
|
|
return err
|
2018-08-31 11:38:15 +00:00
|
|
|
})
|
2018-07-24 21:11:08 +00:00
|
|
|
case jsonparser.String:
|
2018-08-31 11:38:15 +00:00
|
|
|
n.Append(NilLangRef, string(val))
|
2018-07-24 21:11:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return n
|
|
|
|
}
|
2018-08-05 13:25:54 +00:00
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetTime(data []byte, prop string) time.Time {
|
2018-08-06 17:11:24 +00:00
|
|
|
t := time.Time{}
|
|
|
|
str, _ := jsonparser.GetUnsafeString(data, prop)
|
|
|
|
t.UnmarshalText([]byte(str))
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetDuration(data []byte, prop string) time.Duration {
|
2018-12-23 16:12:26 +00:00
|
|
|
str, _ := jsonparser.GetUnsafeString(data, prop)
|
|
|
|
d, _ := time.ParseDuration(str)
|
|
|
|
return d
|
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONUnmarshalToItem(data []byte) Item {
|
2018-08-06 11:10:34 +00:00
|
|
|
if _, err := url.ParseRequestURI(string(data)); err == nil {
|
|
|
|
// try to see if it's an IRI
|
|
|
|
return IRI(data)
|
|
|
|
}
|
|
|
|
|
2019-01-30 16:39:26 +00:00
|
|
|
i, err := ItemTyperFunc(JSONGetType(data))
|
2018-08-05 14:27:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
p := reflect.PtrTo(reflect.TypeOf(i))
|
2018-08-31 15:57:16 +00:00
|
|
|
if reflect.TypeOf(i).Implements(unmarshalerType) || p.Implements(unmarshalerType) {
|
2018-10-29 16:26:21 +00:00
|
|
|
err = i.(json.Unmarshaler).UnmarshalJSON(data)
|
2018-08-05 14:27:39 +00:00
|
|
|
}
|
2018-08-31 15:57:16 +00:00
|
|
|
if reflect.TypeOf(i).Implements(textUnmarshalerType) || p.Implements(textUnmarshalerType) {
|
2018-08-05 14:27:39 +00:00
|
|
|
err = i.(encoding.TextUnmarshaler).UnmarshalText(data)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2018-08-05 13:25:54 +00:00
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetItem(data []byte, prop string) Item {
|
2018-08-31 15:57:16 +00:00
|
|
|
val, typ, _, err := jsonparser.Get(data, prop)
|
2018-08-05 13:25:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
2018-08-31 15:57:16 +00:00
|
|
|
switch typ {
|
|
|
|
case jsonparser.String:
|
|
|
|
if _, err = url.ParseRequestURI(string(val)); err == nil {
|
|
|
|
// try to see if it's an IRI
|
|
|
|
return IRI(val)
|
|
|
|
}
|
|
|
|
case jsonparser.Object:
|
2019-01-30 15:19:29 +00:00
|
|
|
return JSONUnmarshalToItem(val)
|
2018-08-31 15:57:16 +00:00
|
|
|
case jsonparser.Number:
|
|
|
|
fallthrough
|
|
|
|
case jsonparser.Array:
|
|
|
|
fallthrough
|
|
|
|
case jsonparser.Boolean:
|
|
|
|
fallthrough
|
|
|
|
case jsonparser.Null:
|
|
|
|
fallthrough
|
|
|
|
case jsonparser.Unknown:
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return nil
|
2018-08-05 13:25:54 +00:00
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetItems(data []byte, prop string) ItemCollection {
|
2018-07-25 09:54:12 +00:00
|
|
|
val, typ, _, err := jsonparser.Get(data, prop)
|
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-08-05 11:54:01 +00:00
|
|
|
var it ItemCollection
|
2018-07-25 09:54:12 +00:00
|
|
|
switch typ {
|
2018-08-05 11:54:01 +00:00
|
|
|
case jsonparser.Array:
|
2018-08-05 14:20:26 +00:00
|
|
|
jsonparser.ArrayEach(data, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
2019-01-30 15:19:29 +00:00
|
|
|
i := JSONUnmarshalToItem(value)
|
2018-08-31 15:57:16 +00:00
|
|
|
if i != nil {
|
|
|
|
it.Append(i)
|
|
|
|
}
|
2018-08-05 11:54:01 +00:00
|
|
|
}, prop)
|
2018-07-25 09:54:12 +00:00
|
|
|
case jsonparser.Object:
|
2018-08-05 14:20:26 +00:00
|
|
|
jsonparser.ObjectEach(data, func(key []byte, value []byte, dataType jsonparser.ValueType, offset int) error {
|
2019-01-30 15:19:29 +00:00
|
|
|
i := JSONUnmarshalToItem(value)
|
2018-08-31 15:57:16 +00:00
|
|
|
if i != nil {
|
|
|
|
it.Append(i)
|
|
|
|
}
|
2018-07-25 09:54:12 +00:00
|
|
|
return err
|
|
|
|
}, prop)
|
|
|
|
case jsonparser.String:
|
2018-08-05 13:25:54 +00:00
|
|
|
s, _ := jsonparser.GetString(val)
|
2018-10-11 18:13:34 +00:00
|
|
|
it.Append(IRI(s))
|
2018-07-25 09:54:12 +00:00
|
|
|
}
|
|
|
|
return it
|
|
|
|
}
|
2018-11-21 11:25:29 +00:00
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetURIItem(data []byte, prop string) Item {
|
2018-10-31 15:26:51 +00:00
|
|
|
val, typ, _, err := jsonparser.Get(data, prop)
|
2018-07-24 21:11:08 +00:00
|
|
|
if err != nil {
|
2018-10-31 15:26:51 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch typ {
|
|
|
|
case jsonparser.Object:
|
2019-01-30 15:19:29 +00:00
|
|
|
return JSONGetItem(data, prop)
|
2018-10-31 15:26:51 +00:00
|
|
|
case jsonparser.Array:
|
|
|
|
var it ItemCollection
|
|
|
|
jsonparser.ArrayEach(val, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
|
|
|
|
if _, err := url.Parse(string(value)); err == nil {
|
|
|
|
it.Append(IRI(value))
|
|
|
|
return
|
|
|
|
}
|
2019-01-30 16:39:26 +00:00
|
|
|
i, err := ItemTyperFunc(JSONGetType(value))
|
2018-10-31 15:26:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = i.(json.Unmarshaler).UnmarshalJSON(value)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
it.Append(i)
|
|
|
|
})
|
|
|
|
|
|
|
|
return it
|
|
|
|
case jsonparser.String:
|
|
|
|
return IRI(val)
|
2018-07-24 21:11:08 +00:00
|
|
|
}
|
2018-10-31 15:26:51 +00:00
|
|
|
|
|
|
|
return nil
|
2018-07-24 21:11:08 +00:00
|
|
|
}
|
|
|
|
|
2019-01-30 15:19:29 +00:00
|
|
|
func JSONGetLangRefField(data []byte, prop string) LangRef {
|
2018-07-24 21:11:08 +00:00
|
|
|
val, err := jsonparser.GetString(data, prop)
|
|
|
|
if err != nil {
|
|
|
|
return LangRef("")
|
|
|
|
}
|
|
|
|
return LangRef(val)
|
|
|
|
}
|
|
|
|
|
2019-05-04 22:15:23 +00:00
|
|
|
func JSONGetIRI(data []byte, prop string) IRI {
|
|
|
|
val, err := jsonparser.GetString(data, prop)
|
|
|
|
if err != nil {
|
|
|
|
return IRI("")
|
|
|
|
}
|
|
|
|
return IRI(val)
|
|
|
|
}
|
|
|
|
|
2018-11-22 12:35:20 +00:00
|
|
|
// 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) {
|
2019-01-30 15:19:29 +00:00
|
|
|
return JSONUnmarshalToItem(data), nil
|
2018-11-22 12:35:20 +00:00
|
|
|
}
|
|
|
|
|
2019-01-30 16:39:26 +00:00
|
|
|
func JSONGetItemByType(typ ActivityVocabularyType) (Item, error) {
|
2018-11-21 11:25:29 +00:00
|
|
|
var ret Item
|
|
|
|
var err error
|
|
|
|
|
|
|
|
switch typ {
|
|
|
|
case ObjectType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case LinkType:
|
|
|
|
ret = &Link{}
|
|
|
|
o := ret.(*Link)
|
|
|
|
o.Type = typ
|
|
|
|
case ActivityType:
|
|
|
|
ret = &Activity{}
|
|
|
|
o := ret.(*Activity)
|
|
|
|
o.Type = typ
|
|
|
|
case IntransitiveActivityType:
|
|
|
|
ret = &IntransitiveActivity{}
|
|
|
|
o := ret.(*IntransitiveActivity)
|
|
|
|
o.Type = typ
|
|
|
|
case ActorType:
|
2019-01-30 15:19:29 +00:00
|
|
|
ret = &Object{}
|
|
|
|
o := ret.(*Object)
|
2018-11-21 11:25:29 +00:00
|
|
|
o.Type = typ
|
|
|
|
case CollectionType:
|
|
|
|
ret = &Collection{}
|
|
|
|
o := ret.(*Collection)
|
|
|
|
o.Type = typ
|
|
|
|
case OrderedCollectionType:
|
2018-12-02 15:50:40 +00:00
|
|
|
ret = &OrderedCollection{}
|
|
|
|
o := ret.(*OrderedCollection)
|
|
|
|
o.Type = typ
|
|
|
|
case CollectionPageType:
|
|
|
|
ret = &CollectionPage{}
|
|
|
|
o := ret.(*CollectionPage)
|
|
|
|
o.Type = typ
|
|
|
|
case OrderedCollectionPageType:
|
|
|
|
ret = &OrderedCollectionPage{}
|
|
|
|
o := ret.(*OrderedCollectionPage)
|
2018-11-21 11:25:29 +00:00
|
|
|
o.Type = typ
|
|
|
|
case ArticleType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case AudioType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case DocumentType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case EventType:
|
2019-02-19 10:22:10 +00:00
|
|
|
ret = ObjectNew(typ)
|
2018-11-21 11:25:29 +00:00
|
|
|
case ImageType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case NoteType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case PageType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case PlaceType:
|
2019-05-12 18:03:10 +00:00
|
|
|
p := &Place{}
|
2019-05-12 17:33:58 +00:00
|
|
|
p.Type = typ
|
|
|
|
ret = p
|
2018-11-21 11:25:29 +00:00
|
|
|
case ProfileType:
|
2019-05-12 18:03:10 +00:00
|
|
|
p := &Profile{}
|
2019-05-12 17:33:58 +00:00
|
|
|
p.Type = typ
|
|
|
|
ret = p
|
2018-11-21 11:25:29 +00:00
|
|
|
case RelationshipType:
|
2019-05-12 18:03:10 +00:00
|
|
|
r := &Relationship{}
|
2019-05-12 17:33:58 +00:00
|
|
|
r.Type = typ
|
|
|
|
ret = r
|
2018-11-21 11:25:29 +00:00
|
|
|
case TombstoneType:
|
2019-05-12 18:03:10 +00:00
|
|
|
t := &Tombstone{}
|
2019-05-12 17:33:58 +00:00
|
|
|
t.Type = typ
|
|
|
|
ret = t
|
2018-11-21 11:25:29 +00:00
|
|
|
case VideoType:
|
|
|
|
ret = ObjectNew(typ)
|
|
|
|
case MentionType:
|
|
|
|
ret = &Mention{}
|
|
|
|
o := ret.(*Mention)
|
|
|
|
o.Type = typ
|
|
|
|
case ApplicationType:
|
|
|
|
ret = &Application{}
|
|
|
|
o := ret.(*Application)
|
|
|
|
o.Type = typ
|
|
|
|
case GroupType:
|
|
|
|
ret = &Group{}
|
|
|
|
o := ret.(*Group)
|
|
|
|
o.Type = typ
|
|
|
|
case OrganizationType:
|
|
|
|
ret = &Organization{}
|
|
|
|
o := ret.(*Organization)
|
|
|
|
o.Type = typ
|
|
|
|
case PersonType:
|
|
|
|
ret = &Person{}
|
|
|
|
o := ret.(*Person)
|
|
|
|
o.Type = typ
|
|
|
|
case ServiceType:
|
|
|
|
ret = &Service{}
|
|
|
|
o := ret.(*Service)
|
|
|
|
o.Type = typ
|
|
|
|
case AcceptType:
|
|
|
|
ret = &Accept{}
|
|
|
|
o := ret.(*Accept)
|
|
|
|
o.Type = typ
|
|
|
|
case AddType:
|
|
|
|
ret = &Add{}
|
|
|
|
o := ret.(*Add)
|
|
|
|
o.Type = typ
|
|
|
|
case AnnounceType:
|
|
|
|
ret = &Announce{}
|
|
|
|
o := ret.(*Announce)
|
|
|
|
o.Type = typ
|
|
|
|
case ArriveType:
|
|
|
|
ret = &Arrive{}
|
|
|
|
o := ret.(*Arrive)
|
|
|
|
o.Type = typ
|
|
|
|
case BlockType:
|
|
|
|
ret = &Block{}
|
|
|
|
o := ret.(*Block)
|
|
|
|
o.Type = typ
|
|
|
|
case CreateType:
|
|
|
|
ret = &Create{}
|
|
|
|
o := ret.(*Create)
|
|
|
|
o.Type = typ
|
|
|
|
case DeleteType:
|
|
|
|
ret = &Delete{}
|
|
|
|
o := ret.(*Delete)
|
|
|
|
o.Type = typ
|
|
|
|
case DislikeType:
|
|
|
|
ret = &Dislike{}
|
|
|
|
o := ret.(*Dislike)
|
|
|
|
o.Type = typ
|
|
|
|
case FlagType:
|
|
|
|
ret = &Flag{}
|
|
|
|
o := ret.(*Flag)
|
|
|
|
o.Type = typ
|
|
|
|
case FollowType:
|
|
|
|
ret = &Follow{}
|
|
|
|
o := ret.(*Follow)
|
|
|
|
o.Type = typ
|
|
|
|
case IgnoreType:
|
|
|
|
ret = &Ignore{}
|
|
|
|
o := ret.(*Ignore)
|
|
|
|
o.Type = typ
|
|
|
|
case InviteType:
|
|
|
|
ret = &Invite{}
|
|
|
|
o := ret.(*Invite)
|
|
|
|
o.Type = typ
|
|
|
|
case JoinType:
|
|
|
|
ret = &Join{}
|
|
|
|
o := ret.(*Join)
|
|
|
|
o.Type = typ
|
|
|
|
case LeaveType:
|
|
|
|
ret = &Leave{}
|
|
|
|
o := ret.(*Leave)
|
|
|
|
o.Type = typ
|
|
|
|
case LikeType:
|
|
|
|
ret = &Like{}
|
|
|
|
o := ret.(*Like)
|
|
|
|
o.Type = typ
|
|
|
|
case ListenType:
|
|
|
|
ret = &Listen{}
|
|
|
|
o := ret.(*Listen)
|
|
|
|
o.Type = typ
|
|
|
|
case MoveType:
|
|
|
|
ret = &Move{}
|
|
|
|
o := ret.(*Move)
|
|
|
|
o.Type = typ
|
|
|
|
case OfferType:
|
|
|
|
ret = &Offer{}
|
|
|
|
o := ret.(*Offer)
|
|
|
|
o.Type = typ
|
|
|
|
case QuestionType:
|
|
|
|
ret = &Question{}
|
|
|
|
o := ret.(*Question)
|
|
|
|
o.Type = typ
|
|
|
|
case RejectType:
|
|
|
|
ret = &Reject{}
|
|
|
|
o := ret.(*Reject)
|
|
|
|
o.Type = typ
|
|
|
|
case ReadType:
|
|
|
|
ret = &Read{}
|
|
|
|
o := ret.(*Read)
|
|
|
|
o.Type = typ
|
|
|
|
case RemoveType:
|
|
|
|
ret = &Remove{}
|
|
|
|
o := ret.(*Remove)
|
|
|
|
o.Type = typ
|
|
|
|
case TentativeRejectType:
|
|
|
|
ret = &TentativeReject{}
|
|
|
|
o := ret.(*TentativeReject)
|
|
|
|
o.Type = typ
|
|
|
|
case TentativeAcceptType:
|
|
|
|
ret = &TentativeAccept{}
|
|
|
|
o := ret.(*TentativeAccept)
|
|
|
|
o.Type = typ
|
|
|
|
case TravelType:
|
|
|
|
ret = &Travel{}
|
|
|
|
o := ret.(*Travel)
|
|
|
|
o.Type = typ
|
|
|
|
case UndoType:
|
|
|
|
ret = &Undo{}
|
|
|
|
o := ret.(*Undo)
|
|
|
|
o.Type = typ
|
|
|
|
case UpdateType:
|
|
|
|
ret = &Update{}
|
|
|
|
o := ret.(*Update)
|
|
|
|
o.Type = typ
|
|
|
|
case ViewType:
|
|
|
|
ret = &View{}
|
|
|
|
o := ret.(*View)
|
|
|
|
o.Type = typ
|
|
|
|
case "":
|
|
|
|
// when no type is available use a plain Object
|
|
|
|
ret = &Object{}
|
|
|
|
default:
|
|
|
|
ret = nil
|
2019-01-30 15:19:29 +00:00
|
|
|
err = fmt.Errorf("unrecognized ActivityStreams type %s", typ)
|
2018-11-21 11:25:29 +00:00
|
|
|
}
|
|
|
|
return ret, err
|
|
|
|
}
|