diff --git a/README.md b/README.md index ea1dd30d..580f6a95 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,10 @@ Version API 5.131. - [API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api) - 500+ methods - - Ability to change the request handler - Ability to modify HTTP client - Request Limiter + - Support [zstd](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api#VK.EnableZstd) +and [MessagePack](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api#VK.EnableMessagePack) - Token pool - [OAuth](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api/oauth) - [Callback API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/callback) diff --git a/api/README.md b/api/README.md index 1d12943c..5d2d0c76 100644 --- a/api/README.md +++ b/api/README.md @@ -80,6 +80,54 @@ if errors.As(err, &e) { Для Execute существует отдельная ошибка `ExecuteErrors` +### Поддержка MessagePack и zstd + +> Результат перехода с gzip (JSON) на zstd (msgpack): +> +> - в 7 раз быстрее сжатие (–1 мкс); +> - на 10% меньше размер данных (8 Кбайт вместо 9 Кбайт); +> - продуктовый эффект не статзначимый :( +> +> [Как мы отказались от JPEG, JSON, TCP и ускорили ВКонтакте в два раза](https://habr.com/ru/company/vk/blog/594633/) + +VK API способно возвращать ответ в виде [MessagePack](https://msgpack.org/). +Это эффективный формат двоичной сериализации, похожий на JSON, только быстрее +и меньше по размеру. + +ВНИМАНИЕ, C MessagePack НЕКОТОРЫЕ МЕТОДЫ МОГУТ ВОЗВРАЩАТЬ +СЛОМАННУЮ КОДИРОВКУ. + +Для сжатия, вместо классического gzip, можно использовать +[zstd](https://github.com/facebook/zstd). Сейчас vksdk поддерживает zstd без +словаря. Если кто знает как получать словарь, +[отпишитесь сюда](https://github.com/SevereCloud/vksdk/issues/180). + +```go +vk := api.NewVK(os.Getenv("USER_TOKEN")) + +method := "store.getStickersKeywords" +params := api.Params{ + "aliases": true, + "all_products": true, + "need_stickers": true, +} + +r, err := vk.Request(method, params) // Content-Length: 44758 +if err != nil { + log.Fatal(err) +} +log.Println("json:", len(r)) // json: 814231 + +vk.EnableMessagePack() // Включаем поддержку MessagePack +vk.EnableZstd() // Включаем поддержку zstd + +r, err = vk.Request(method, params) // Content-Length: 35755 +if err != nil { + log.Fatal(err) +} +log.Println("msgpack:", len(r)) // msgpack: 650775 +``` + ### Запрос любого метода Пример запроса [users.get](https://vk.com/dev/users.get) diff --git a/api/ads.go b/api/ads.go index 5dc8fea7..544143b3 100644 --- a/api/ads.go +++ b/api/ads.go @@ -1,9 +1,11 @@ package api // import "github.com/SevereCloud/vksdk/v2/api" import ( + "bytes" "encoding/json" "github.com/SevereCloud/vksdk/v2/object" + "github.com/vmihailenco/msgpack/v5" ) // AdsAddOfficeUsersItem struct. @@ -21,6 +23,23 @@ func (r *AdsAddOfficeUsersItem) UnmarshalJSON(data []byte) (err error) { return } +// DecodeMsgpack func. +func (r *AdsAddOfficeUsersItem) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if msgpack.Unmarshal(data, &r.OK) != nil { + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + return d.Decode(&r.Error) + } + + return nil +} + // AdsAddOfficeUsersResponse struct. type AdsAddOfficeUsersResponse []AdsAddOfficeUsersItem diff --git a/api/ads_test.go b/api/ads_test.go index e41b47cd..0ae74d15 100644 --- a/api/ads_test.go +++ b/api/ads_test.go @@ -5,6 +5,8 @@ import ( "github.com/SevereCloud/vksdk/v2/api" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) func TestAdsResponse_UnmarshalJSON(t *testing.T) { @@ -32,6 +34,46 @@ func TestAdsResponse_UnmarshalJSON(t *testing.T) { ) } +func TestAdsResponse_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, expected api.AdsAddOfficeUsersItem, wantErr string) { + var r api.AdsAddOfficeUsersItem + + err := msgpack.Unmarshal(data, &r) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, expected, r) + } + + f([]byte{msgpcode.False}, api.AdsAddOfficeUsersItem{OK: false}, "") + f([]byte{msgpcode.True}, api.AdsAddOfficeUsersItem{OK: true}, "") + f( + []byte{ + 0x82, 0xAA, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x5F, 0x63, 0x6F, 0x64, + 0x65, 0x64, 0xAA, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x5F, 0x64, 0x65, + 0x73, 0x63, 0xD9, 0x48, 0x4F, 0x6E, 0x65, 0x20, 0x6F, 0x66, 0x20, + 0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x72, 0x61, 0x6D, 0x65, 0x74, + 0x65, 0x72, 0x73, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, + 0x65, 0x64, 0x20, 0x77, 0x61, 0x73, 0x20, 0x6D, 0x69, 0x73, 0x73, + 0x69, 0x6E, 0x67, 0x20, 0x6F, 0x72, 0x20, 0x69, 0x6E, 0x76, 0x61, + 0x6C, 0x69, 0x64, 0x3A, 0x20, 0x64, 0x61, 0x74, 0x61, 0x5B, 0x31, + 0x5D, 0x5B, 0x75, 0x73, 0x65, 0x72, 0x5F, 0x69, 0x64, 0x5D, + }, + api.AdsAddOfficeUsersItem{ + OK: false, + Error: api.AdsError{ + Code: 100, + Desc: "One of the parameters specified was missing or invalid: data[1][user_id]", + }, + }, + "", + ) + f(nil, api.AdsAddOfficeUsersItem{}, "EOF") +} + func TestVK_AdsGetAccounts(t *testing.T) { t.Parallel() diff --git a/api/api.go b/api/api.go index f94d3177..760bc7e0 100644 --- a/api/api.go +++ b/api/api.go @@ -7,9 +7,11 @@ package api // import "github.com/SevereCloud/vksdk/v2/api" import ( "bytes" + "compress/gzip" "context" "encoding/json" "fmt" + "io" "mime" "net/http" "net/url" @@ -21,6 +23,8 @@ import ( "github.com/SevereCloud/vksdk/v2" "github.com/SevereCloud/vksdk/v2/internal" "github.com/SevereCloud/vksdk/v2/object" + "github.com/klauspost/compress/zstd" + "github.com/vmihailenco/msgpack/v5" ) // Api constants. @@ -91,6 +95,9 @@ type VK struct { UserAgent string Handler func(method string, params ...Params) (Response, error) + msgpack bool + zstd bool + mux sync.Mutex lastTime time.Time rps int @@ -98,9 +105,9 @@ type VK struct { // Response struct. type Response struct { - Response json.RawMessage `json:"response"` - Error Error `json:"error"` - ExecuteErrors ExecuteErrors `json:"execute_errors"` + Response object.RawMessage `json:"response"` + Error Error `json:"error"` + ExecuteErrors ExecuteErrors `json:"execute_errors"` } // NewVK returns a new VK. @@ -243,24 +250,52 @@ func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, er return response, err } + acceptEncoding := "gzip" + if vk.zstd { + acceptEncoding = "zstd" + } + req.Header.Set("User-Agent", vk.UserAgent) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept-Encoding", acceptEncoding) + + var reader io.Reader + resp, err := vk.Client.Do(req) if err != nil { return response, err } - mediatype, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) - if mediatype != "application/json" { - _ = resp.Body.Close() - return response, &InvalidContentType{mediatype} + switch resp.Header.Get("Content-Encoding") { + case "zstd": + reader, _ = zstd.NewReader(resp.Body) + case "gzip": + reader, _ = gzip.NewReader(resp.Body) + default: + reader = resp.Body } - err = json.NewDecoder(resp.Body).Decode(&response) - if err != nil { + mediatype, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) + switch mediatype { + case "application/json": + err = json.NewDecoder(reader).Decode(&response) + if err != nil { + _ = resp.Body.Close() + return response, err + } + case "application/x-msgpack": + dec := msgpack.NewDecoder(reader) + dec.SetCustomStructTag("json") + + err = dec.Decode(&response) + if err != nil { + _ = resp.Body.Close() + return response, err + } + default: _ = resp.Body.Close() - return response, err + return response, &InvalidContentType{mediatype} } _ = resp.Body.Close() @@ -291,6 +326,10 @@ func (vk *VK) Request(method string, sliceParams ...Params) ([]byte, error) { sliceParams = append(sliceParams, reqParams) + if vk.msgpack { + method += ".msgpack" + } + resp, err := vk.Handler(method, sliceParams...) return resp.Response, err @@ -303,7 +342,32 @@ func (vk *VK) RequestUnmarshal(method string, obj interface{}, sliceParams ...Pa return err } - return json.Unmarshal(rawResponse, &obj) + if vk.msgpack { + dec := msgpack.NewDecoder(bytes.NewReader(rawResponse)) + dec.SetCustomStructTag("json") + + err = dec.Decode(&obj) + } else { + err = json.Unmarshal(rawResponse, &obj) + } + + return err +} + +// EnableMessagePack enable using MessagePack instead of JSON. +// +// THIS IS EXPERIMENTAL FUNCTION! Broken encoding returned in some methods. +// +// See https://msgpack.org +func (vk *VK) EnableMessagePack() { + vk.msgpack = true +} + +// EnableZstd enable using zstd instead of gzip. +// +// This not use dict. +func (vk *VK) EnableZstd() { + vk.zstd = true } func fmtReflectValue(value reflect.Value, depth int) string { diff --git a/api/api_test.go b/api/api_test.go index c4d894a0..6e60bdcb 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -322,3 +322,23 @@ func TestContext(t *testing.T) { _, err := vkUser.UsersGet(p) assert.EqualError(t, err, "Post \"https://api.vk.com/method/users.get\": context deadline exceeded") } + +func TestVK_EnableMessagePack(t *testing.T) { + t.Parallel() + + vk := api.NewVK("") + vk.EnableMessagePack() + + _, err := vk.UsersGet(nil) + assert.ErrorIs(t, err, api.ErrAuth) +} + +func TestVK_EnableZstd(t *testing.T) { + t.Parallel() + + vk := api.NewVK("") + vk.EnableZstd() + + _, err := vk.UsersGet(nil) + assert.ErrorIs(t, err, api.ErrAuth) +} diff --git a/api/auth.go b/api/auth.go index f927a6ff..62a08c07 100644 --- a/api/auth.go +++ b/api/auth.go @@ -1,5 +1,9 @@ package api // import "github.com/SevereCloud/vksdk/v2/api" +import ( + "github.com/SevereCloud/vksdk/v2/object" +) + // AuthCheckPhone checks a user's phone number for correctness. // // https://vk.com/dev/auth.checkPhone @@ -24,3 +28,58 @@ func (vk *VK) AuthRestore(params Params) (response AuthRestoreResponse, err erro err = vk.RequestUnmarshal("auth.restore", &response, params) return } + +// AuthGetProfileInfoBySilentTokenResponse struct. +type AuthGetProfileInfoBySilentTokenResponse struct { + Success []object.AuthSilentTokenProfile `json:"success"` + Errors []AuthSilentTokenError `json:"errors"` +} + +// AuthGetProfileInfoBySilentToken method. +// +// https://platform.vk.com/?p=DocsDashboard&docs=tokens_silent-token +func (vk *VK) AuthGetProfileInfoBySilentToken(params Params) (response AuthGetProfileInfoBySilentTokenResponse, err error) { + err = vk.RequestUnmarshal("auth.getProfileInfoBySilentToken", &response, params) + return +} + +// ExchangeSilentTokenSource call conditions exchangeSilentToken. +// +// 0 Unknown +// 1 Silent authentication +// 2 Auth by login and password +// 3 Extended registration +// 4 Auth by exchange token +// 5 Auth by exchange token on reset password +// 6 Auth by exchange token on unblock +// 7 Auth by exchange token on reset session +// 8 Auth by exchange token on change password +// 9 Finish phone validation on authentication +// 10 Auth by code +// 11 Auth by external oauth +// 12 Reactivation +// 15 Auth by SDK temporary access-token +type ExchangeSilentTokenSource int + +// AuthExchangeSilentAuthTokenResponse struct. +type AuthExchangeSilentAuthTokenResponse struct { + AccessToken string `json:"access_token"` + AccessTokenID string `json:"access_token_id"` + UserID int `json:"user_id"` + Phone string `json:"phone"` + PhoneValidated interface{} `json:"phone_validated"` + IsPartial bool `json:"is_partial"` + IsService bool `json:"is_service"` + AdditionalSignupRequired bool `json:"additional_signup_required"` + Email string `json:"email"` + Source ExchangeSilentTokenSource `json:"source"` + SourceDescription string `json:"source_description"` +} + +// AuthExchangeSilentAuthToken method. +// +// https://platform.vk.com/?p=DocsDashboard&docs=tokens_access-token +func (vk *VK) AuthExchangeSilentAuthToken(params Params) (response AuthExchangeSilentAuthTokenResponse, err error) { + err = vk.RequestUnmarshal("auth.exchangeSilentAuthToken", &response, params) + return +} diff --git a/api/errors.go b/api/errors.go index 828ef81e..aa6ea509 100644 --- a/api/errors.go +++ b/api/errors.go @@ -1000,3 +1000,31 @@ func (e AdsError) Is(target error) bool { return false } + +// AuthSilentTokenError struct. +type AuthSilentTokenError struct { + Token string `json:"token"` + Code ErrorType `json:"code"` + Description string `json:"description"` +} + +// Error returns the description of a AuthSilentTokenError. +func (e AuthSilentTokenError) Error() string { + return "api: " + e.Description +} + +// Is unwraps its first argument sequentially looking for an error that matches +// the second. +func (e AuthSilentTokenError) Is(target error) bool { + var tError *AuthSilentTokenError + if errors.As(target, &tError) { + return e.Code == tError.Code && e.Description == tError.Description + } + + var tErrorType ErrorType + if errors.As(target, &tErrorType) { + return e.Code == tErrorType + } + + return false +} diff --git a/api/errors_test.go b/api/errors_test.go index 68af50ea..7ab186fe 100644 --- a/api/errors_test.go +++ b/api/errors_test.go @@ -101,3 +101,30 @@ func TestAdsError_Is(t *testing.T) { f(&api.AdsError{Code: api.ErrorType(1), Desc: "123"}, &api.AdsError{Code: api.ErrorType(1), Desc: "321"}, false) f(&api.AdsError{Code: api.ErrorType(1)}, &streaming.Error{}, false) } + +func TestAuthSilentTokenError_Error(t *testing.T) { + t.Parallel() + + err := api.AuthSilentTokenError{ + Code: 1, + Description: "test message", + } + assert.EqualError(t, err, "api: test message") +} + +func TestAuthSilentTokenError_Is(t *testing.T) { + t.Parallel() + + f := func(err *api.AuthSilentTokenError, target error, want bool) { + assert.Equal(t, want, errors.Is(err, target)) + } + + f(&api.AuthSilentTokenError{Code: api.ErrorType(1)}, &api.AuthSilentTokenError{Code: api.ErrorType(1)}, true) + f(&api.AuthSilentTokenError{Code: api.ErrAuth}, api.ErrAuth, true) + f(&api.AuthSilentTokenError{Code: api.ErrorType(1), Description: "123"}, &api.AuthSilentTokenError{Code: api.ErrorType(1), Description: "123"}, true) + + f(&api.AuthSilentTokenError{Code: api.ErrorType(1)}, &api.AuthSilentTokenError{Code: api.ErrorType(2)}, false) + f(&api.AuthSilentTokenError{Code: api.ErrorType(1)}, api.ErrorType(2), false) + f(&api.AuthSilentTokenError{Code: api.ErrorType(1), Description: "123"}, &api.AuthSilentTokenError{Code: api.ErrorType(1), Description: "321"}, false) + f(&api.AuthSilentTokenError{Code: api.ErrorType(1)}, &streaming.Error{}, false) +} diff --git a/api/execute.go b/api/execute.go index cc52cd0d..1ee04cee 100644 --- a/api/execute.go +++ b/api/execute.go @@ -1,6 +1,11 @@ package api -import "encoding/json" +import ( + "bytes" + "encoding/json" + + "github.com/vmihailenco/msgpack/v5" +) // ExecuteWithArgs a universal method for calling a sequence of other methods // while saving and filtering interim results. @@ -26,9 +31,19 @@ func (vk *VK) ExecuteWithArgs(code string, params Params, obj interface{}) error return err } - jsonErr := json.Unmarshal(resp.Response, &obj) - if jsonErr != nil { - return jsonErr + var decoderErr error + + if vk.msgpack { + dec := msgpack.NewDecoder(bytes.NewReader(resp.Response)) + dec.SetCustomStructTag("json") + + decoderErr = dec.Decode(&obj) + } else { + decoderErr = json.Unmarshal(resp.Response, &obj) + } + + if decoderErr != nil { + return decoderErr } if resp.ExecuteErrors != nil { diff --git a/api/messages.go b/api/messages.go index 0a8f5613..a74266fe 100644 --- a/api/messages.go +++ b/api/messages.go @@ -1,7 +1,10 @@ package api // import "github.com/SevereCloud/vksdk/v2/api" import ( + "strconv" + "github.com/SevereCloud/vksdk/v2/object" + "github.com/vmihailenco/msgpack/v5" ) // MessagesAddChatUser adds a new user to a chat. @@ -31,11 +34,34 @@ func (vk *VK) MessagesCreateChat(params Params) (response int, err error) { // MessagesDeleteResponse struct. type MessagesDeleteResponse map[string]int +// DecodeMsgpack funcion. +func (resp *MessagesDeleteResponse) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + var respMap map[int]int + + err = msgpack.Unmarshal(data, &respMap) + if err != nil { + return err + } + + *resp = make(MessagesDeleteResponse) + for key, val := range respMap { + (*resp)[strconv.Itoa(key)] = val + } + + return nil +} + // MessagesDelete deletes one or more messages. // // https://vk.com/dev/messages.delete func (vk *VK) MessagesDelete(params Params) (response MessagesDeleteResponse, err error) { err = vk.RequestUnmarshal("messages.delete", &response, params) + return } diff --git a/api/oauth/group_test.go b/api/oauth/group_test.go index e35695ac..6602bb79 100644 --- a/api/oauth/group_test.go +++ b/api/oauth/group_test.go @@ -16,7 +16,7 @@ func TestNewGroupTokensFromJSON(t *testing.T) { t.Helper() token, err := oauth.NewGroupTokensFromJSON(data) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } @@ -53,7 +53,7 @@ func TestNewGroupTokensFromURL(t *testing.T) { u, _ := url.Parse(rawurl) token, err := oauth.NewGroupTokensFromURL(u) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } diff --git a/api/oauth/user_test.go b/api/oauth/user_test.go index 14e8c4c7..e68f0f0b 100644 --- a/api/oauth/user_test.go +++ b/api/oauth/user_test.go @@ -14,7 +14,7 @@ func TestParseJSON(t *testing.T) { f := func(data []byte, wantToken *oauth.UserToken, wantErr string) { token, err := oauth.NewUserTokenFromJSON(data) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } @@ -47,7 +47,7 @@ func TestParseURL(t *testing.T) { u, _ := url.Parse(rawurl) token, err := oauth.NewUserTokenFromURL(u) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } diff --git a/api/params/auth.go b/api/params/auth.go index 58393a06..dc6021fc 100644 --- a/api/params/auth.go +++ b/api/params/auth.go @@ -68,3 +68,57 @@ func (b *AuthRestoreBuilder) LastName(v string) *AuthRestoreBuilder { b.Params["last_name"] = v return b } + +// AuthGetProfileInfoBySilentTokenBuilder builder. +// +// https://platform.vk.com/?p=DocsDashboard&docs=tokens_silent-token +type AuthGetProfileInfoBySilentTokenBuilder struct { + api.Params +} + +// NewAuthGetProfileInfoBySilentTokenBuilder func. +func NewAuthGetProfileInfoBySilentTokenBuilder() *AuthGetProfileInfoBySilentTokenBuilder { + return &AuthGetProfileInfoBySilentTokenBuilder{api.Params{ + "token": "", + "uuid": "", + "event": "", + }} +} + +// Add token, uuid, event to params. +func (b *AuthGetProfileInfoBySilentTokenBuilder) Add(token, uuid, event string) *AuthGetProfileInfoBySilentTokenBuilder { + separator := "," + if b.Params["token"] == "" { + separator = "" + } + + b.Params["token"] = b.Params["token"].(string) + separator + token + b.Params["uuid"] = b.Params["uuid"].(string) + separator + uuid + b.Params["event"] = b.Params["event"].(string) + separator + event + + return b +} + +// AuthExchangeSilentAuthTokenBuilder builder. +// +// https://platform.vk.com/?p=DocsDashboard&docs=tokens_silent-token +type AuthExchangeSilentAuthTokenBuilder struct { + api.Params +} + +// NewAuthExchangeSilentAuthTokenBuilder func. +func NewAuthExchangeSilentAuthTokenBuilder() *AuthExchangeSilentAuthTokenBuilder { + return &AuthExchangeSilentAuthTokenBuilder{api.Params{}} +} + +// Token parameter. +func (b *AuthExchangeSilentAuthTokenBuilder) Token(v string) *AuthExchangeSilentAuthTokenBuilder { + b.Params["token"] = v + return b +} + +// UUID parameter. +func (b *AuthExchangeSilentAuthTokenBuilder) UUID(v string) *AuthExchangeSilentAuthTokenBuilder { + b.Params["uuid"] = v + return b +} diff --git a/api/params/auth_test.go b/api/params/auth_test.go index c76231c2..92b1dcf8 100644 --- a/api/params/auth_test.go +++ b/api/params/auth_test.go @@ -34,3 +34,31 @@ func TestAuthRestoreBuilder(t *testing.T) { assert.Equal(t, b.Params["phone"], "text") assert.Equal(t, b.Params["last_name"], "text") } + +func TestAuthGetProfileInfoBySilentTokenBuilder(t *testing.T) { + t.Parallel() + + b := params.NewAuthGetProfileInfoBySilentTokenBuilder() + + b.Add("token", "uuid", "event") + assert.Equal(t, b.Params["token"], "token") + assert.Equal(t, b.Params["uuid"], "uuid") + assert.Equal(t, b.Params["event"], "event") + + b.Add("token2", "uuid2", "event2") + assert.Equal(t, b.Params["token"], "token,token2") + assert.Equal(t, b.Params["uuid"], "uuid,uuid2") + assert.Equal(t, b.Params["event"], "event,event2") +} + +func TestAuthExchangeSilentAuthTokenBuilder(t *testing.T) { + t.Parallel() + + b := params.NewAuthExchangeSilentAuthTokenBuilder() + + b.Token("token") + b.UUID("uuid") + + assert.Equal(t, b.Params["token"], "token") + assert.Equal(t, b.Params["uuid"], "uuid") +} diff --git a/api/streaming_test.go b/api/streaming_test.go index 44e1cd53..5d8f6844 100644 --- a/api/streaming_test.go +++ b/api/streaming_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/SevereCloud/vksdk/v2/api" + "github.com/stretchr/testify/assert" ) func TestVK_StreamingGetServerURL(t *testing.T) { @@ -72,9 +73,7 @@ func TestVK_StreamingGetStem(t *testing.T) { t.Errorf("%v", err) } - if response.Stem != "собак" { - t.Error("Stem wrong") - } + assert.Equal(t, "собак", response.Stem) } func TestVK_StreamingSetSettings(t *testing.T) { diff --git a/api/utils.go b/api/utils.go index 63508fe2..965c26f2 100644 --- a/api/utils.go +++ b/api/utils.go @@ -1,9 +1,8 @@ package api // import "github.com/SevereCloud/vksdk/v2/api" import ( - "encoding/json" - "github.com/SevereCloud/vksdk/v2/object" + "github.com/vmihailenco/msgpack/v5" ) // UtilsCheckLinkResponse struct. @@ -89,17 +88,34 @@ func (vk *VK) UtilsGetShortLink(params Params) (response UtilsGetShortLinkRespon // UtilsResolveScreenNameResponse struct. type UtilsResolveScreenNameResponse object.UtilsDomainResolved +// UnmarshalJSON UtilsResolveScreenNameResponse. +// +// BUG(VK): UtilsResolveScreenNameResponse return []. +func (resp *UtilsResolveScreenNameResponse) UnmarshalJSON(data []byte) error { + var p object.UtilsDomainResolved + err := p.UnmarshalJSON(data) + + *resp = UtilsResolveScreenNameResponse(p) + + return err +} + +// DecodeMsgpack UtilsResolveScreenNameResponse. +// +// BUG(VK): UtilsResolveScreenNameResponse return []. +func (resp *UtilsResolveScreenNameResponse) DecodeMsgpack(dec *msgpack.Decoder) error { + var p object.UtilsDomainResolved + err := p.DecodeMsgpack(dec) + + *resp = UtilsResolveScreenNameResponse(p) + + return err +} + // UtilsResolveScreenName detects a type of object (e.g., user, community, application) and its ID by screen name. // // https://vk.com/dev/utils.resolveScreenName func (vk *VK) UtilsResolveScreenName(params Params) (response UtilsResolveScreenNameResponse, err error) { - rawResponse, err := vk.Request("utils.resolveScreenName", params) - // Если короткое имя screen_name не занято, то будет возвращён пустой объект. - if err != nil || string(rawResponse) == "[]" { - return - } - - err = json.Unmarshal(rawResponse, &response) - + err = vk.RequestUnmarshal("utils.resolveScreenName", &response, params) return } diff --git a/doc.go b/doc.go index 504cdf75..ff749d80 100644 --- a/doc.go +++ b/doc.go @@ -7,6 +7,6 @@ package vksdk // Module constants. const ( - Version = "2.12.0" + Version = "2.13.0" API = "5.131" ) diff --git a/go.mod b/go.mod index 9a6eb3c7..f323939b 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.16 require ( github.com/gorilla/schema v1.2.0 github.com/gorilla/websocket v1.4.2 + github.com/klauspost/compress v1.14.1 github.com/stretchr/testify v1.7.0 + github.com/vmihailenco/msgpack/v5 v5.3.5 golang.org/x/text v0.3.7 ) diff --git a/go.sum b/go.sum index c9e238ae..4cd4a468 100644 --- a/go.sum +++ b/go.sum @@ -4,11 +4,18 @@ github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.14.1 h1:hLQYb23E8/fO+1u53d02A97a8UnsddcvYzq4ERRU4ds= +github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/object/auth.go b/object/auth.go new file mode 100644 index 00000000..78b3e10c --- /dev/null +++ b/object/auth.go @@ -0,0 +1,17 @@ +package object // import "github.com/SevereCloud/vksdk/v2/object" + +// AuthSilentTokenProfile struct. +type AuthSilentTokenProfile struct { + Token string `json:"token"` + Expires int `json:"expires"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Photo50 string `json:"photo_50"` + Photo100 string `json:"photo_100"` + Photo200 string `json:"photo_200"` + Phone string `json:"phone"` + PhoneValidated interface{} `json:"phone_validated"` // int | bool + UserID int `json:"user_id"` + IsPartial BaseBoolInt `json:"is_partial"` + IsService BaseBoolInt `json:"is_service"` +} diff --git a/object/groups.go b/object/groups.go index 8a4accf4..6cabae01 100644 --- a/object/groups.go +++ b/object/groups.go @@ -5,6 +5,9 @@ import ( "encoding/json" "fmt" "reflect" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) // GroupsAddress WorkInfoStatus of information about timetable. @@ -325,6 +328,36 @@ func (personal *GroupsCountersGroup) UnmarshalJSON(data []byte) error { return nil } +// DecodeMsgpack GroupsCountersGroup. +// +// BUG(VK): GroupsCountersGroup return []. +func (personal *GroupsCountersGroup) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) { + return nil + } + + type renamedGroupsCountersGroup GroupsCountersGroup + + var r renamedGroupsCountersGroup + + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + err = d.Decode(&r) + if err != nil { + return err + } + + *personal = GroupsCountersGroup(r) + + return nil +} + // GroupsCover struct. type GroupsCover struct { Enabled BaseBoolInt `json:"enabled"` // Information whether cover is enabled @@ -634,6 +667,53 @@ func (g *GroupsSectionsList) UnmarshalJSON(data []byte) error { return nil } +// DecodeMsgpack need for decode dynamic array (Example: [1, "Фотографии"]) to struct. +func (g *GroupsSectionsList) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + var alias []interface{} + + err = msgpack.Unmarshal(data, &alias) + if err != nil { + return err + } + + if len(alias) != 2 { + return &json.UnmarshalTypeError{ + Value: string(data), + Type: reflect.TypeOf((*GroupsSectionsList)(nil)), + } + } + + id, ok := alias[0].(int8) + if !ok { + return &json.UnmarshalTypeError{ + Value: string(data), + Type: reflect.TypeOf((*GroupsSectionsList)(nil)), + Struct: "GroupsSectionsList", + Field: "ID", + } + } + + name, ok := alias[1].(string) + if !ok { + return &json.UnmarshalTypeError{ + Value: string(data), + Type: reflect.TypeOf((*GroupsSectionsList)(nil)), + Struct: "GroupsSectionsList", + Field: "Name", + } + } + + g.ID = int(id) + g.Name = name + + return nil +} + // GroupsActionType for action_button in groups. type GroupsActionType string diff --git a/object/groups_test.go b/object/groups_test.go index 9dd1e78e..5658cde4 100644 --- a/object/groups_test.go +++ b/object/groups_test.go @@ -1,12 +1,58 @@ package object_test import ( + "encoding/json" "testing" "github.com/SevereCloud/vksdk/v2/object" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) +func TestGroupsCountersGroup_UnmarshalJSON(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.GroupsCountersGroup) { + t.Helper() + + var counters object.GroupsCountersGroup + + err := json.Unmarshal(data, &counters) + assert.NoError(t, err) + assert.Equal(t, want, counters) + } + + f([]byte("[]"), object.GroupsCountersGroup{}) + f([]byte(`{"docs":1}`), object.GroupsCountersGroup{Docs: 1}) + + var counters object.GroupsCountersGroup + err := json.Unmarshal([]byte("0"), &counters) + assert.Error(t, err) +} + +func TestGroupsCountersGroup_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.GroupsCountersGroup, wantErr string) { + t.Helper() + + var counters object.GroupsCountersGroup + + err := msgpack.Unmarshal(data, &counters) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, want, counters) + } + + f([]byte{msgpcode.FixedArrayLow}, object.GroupsCountersGroup{}, "") + f([]byte{0x81, 0xA4, 'd', 'o', 'c', 's', 0x01}, object.GroupsCountersGroup{Docs: 1}, "") + f([]byte("\xc2"), object.GroupsCountersGroup{}, "msgpack: unexpected code=c2 decoding map length") + f(nil, object.GroupsCountersGroup{}, "EOF") +} + func TestGroupsGroup_ToMention(t *testing.T) { t.Parallel() @@ -87,3 +133,55 @@ func TestGroupsSectionsList_UnmarshalJSON(t *testing.T) { true, ) } + +func TestGroupsSectionsList_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.GroupsSectionsList, wanErr bool) { + t.Helper() + + var got object.GroupsSectionsList + + err := msgpack.Unmarshal(data, &got) + + if wanErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, want, got) + } + } + + f( + []byte{0x92, 0x7B, 0xA4, 0x74, 0x65, 0x73, 0x74}, + object.GroupsSectionsList{ID: 123, Name: "test"}, + false, + ) + + // Errors: + f( + []byte{0x7B}, + object.GroupsSectionsList{}, + true, + ) + f( + []byte{0x93, 0x7B, 0xA3, 0x31, 0x32, 0x33, 0xA3, 0x31, 0x32, 0x33}, + object.GroupsSectionsList{}, + true, + ) + f( + []byte{0x92, 0xA3, 0x31, 0x32, 0x33, 0xA3, 0x31, 0x32, 0x33}, + object.GroupsSectionsList{}, + true, + ) + f( + []byte{0x92, 0x7B, 0x7B}, + object.GroupsSectionsList{}, + true, + ) + f( + nil, + object.GroupsSectionsList{}, + true, + ) +} diff --git a/object/market.go b/object/market.go index a1d75213..7fb5f6af 100644 --- a/object/market.go +++ b/object/market.go @@ -4,6 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) // Information whether the MarketMarketItem is available. @@ -100,6 +103,36 @@ func (market *MarketMarketItem) UnmarshalJSON(data []byte) error { return nil } +// DecodeMsgpack MarketMarketItem. +// +// BUG(VK): https://github.com/SevereCloud/vksdk/issues/147 +func (market *MarketMarketItem) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if bytes.Equal(data, []byte{msgpcode.False}) { + return nil + } + + type renamedMarketMarketItem MarketMarketItem + + var r renamedMarketMarketItem + + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + err = d.Decode(&r) + if err != nil { + return err + } + + *market = MarketMarketItem(r) + + return nil +} + // MarketMarketItemProperty struct. type MarketMarketItemProperty struct { VariantID int `json:"variant_id"` @@ -151,6 +184,36 @@ func (m *MarketPrice) UnmarshalJSON(data []byte) error { return nil } +// DecodeMsgpack MarketPrice. +// +// BUG(VK): unavailable product, in fave.get return []. +func (m *MarketPrice) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) { + return nil + } + + type renamedMarketPrice MarketPrice + + var r renamedMarketPrice + + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + err = d.Decode(&r) + if err != nil { + return err + } + + *m = MarketPrice(r) + + return nil +} + // MarketSection struct. type MarketSection struct { ID int `json:"id"` // Section ID diff --git a/object/market_test.go b/object/market_test.go index 2512ac9a..d7c7942f 100644 --- a/object/market_test.go +++ b/object/market_test.go @@ -6,6 +6,8 @@ import ( "github.com/SevereCloud/vksdk/v2/object" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) func TestMarketMarketItem_ToAttachment(t *testing.T) { @@ -21,7 +23,7 @@ func TestMarketMarketItem_ToAttachment(t *testing.T) { f(object.MarketMarketItem{ID: 20, OwnerID: -10}, "market-10_20") } -func TestMarketMarketItem__UnmarshalJSON(t *testing.T) { +func TestMarketMarketItem_UnmarshalJSON(t *testing.T) { t.Parallel() f := func(data []byte, wantMarket object.MarketMarketItem) { @@ -40,7 +42,28 @@ func TestMarketMarketItem__UnmarshalJSON(t *testing.T) { assert.Error(t, err) } -func TestMarketPrice__UnmarshalJSON(t *testing.T) { +func TestMarketMarketItem_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, wantMarket object.MarketMarketItem, wantErr string) { + var market object.MarketMarketItem + + err := msgpack.Unmarshal(data, &market) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, wantMarket, market) + } + + f([]byte{msgpcode.False}, object.MarketMarketItem{}, "") + f([]byte{0x81, 0xA2, 0x69, 0x64, 0x01}, object.MarketMarketItem{ID: 1}, "") + + f([]byte("\xc3"), object.MarketMarketItem{}, "msgpack: unexpected code=c3 decoding map length") + f(nil, object.MarketMarketItem{}, "EOF") +} + +func TestMarketPrice_UnmarshalJSON(t *testing.T) { t.Parallel() f := func(data []byte, wantMarket object.MarketPrice) { @@ -59,6 +82,29 @@ func TestMarketPrice__UnmarshalJSON(t *testing.T) { assert.Error(t, err) } +func TestMarketPrice_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, wantMarket object.MarketPrice, wantErr string) { + var market object.MarketPrice + + err := msgpack.Unmarshal(data, &market) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, wantMarket, market) + } + + f([]byte{msgpcode.FixedArrayLow}, object.MarketPrice{}, "") + f([]byte{ + 0x81, 0xA4, 0x74, 0x65, 0x78, 0x74, 0xA1, 0x61, + }, object.MarketPrice{Text: "a"}, "") + + f([]byte("\xc2"), object.MarketPrice{}, "msgpack: unexpected code=c2 decoding map length") + f(nil, object.MarketPrice{}, "EOF") +} + func TestMarketMarketAlbum_ToAttachment(t *testing.T) { t.Parallel() diff --git a/object/messages.go b/object/messages.go index 3356c1a8..2bf69096 100644 --- a/object/messages.go +++ b/object/messages.go @@ -379,7 +379,8 @@ type MessagesTemplateElementCarousel struct { Title string `json:"title,omitempty"` Action MessagesTemplateElementCarouselAction `json:"action,omitempty"` Description string `json:"description,omitempty"` - Photo *PhotosPhoto `json:"photo,omitempty"` + Photo *PhotosPhoto `json:"photo,omitempty"` // Only read + PhotoID string `json:"photo_id,omitempty"` // Only for send Buttons []MessagesKeyboardButton `json:"buttons,omitempty"` } diff --git a/object/notifications.go b/object/notifications.go index 119c3f9f..781a1de7 100644 --- a/object/notifications.go +++ b/object/notifications.go @@ -1,7 +1,5 @@ package object // import "github.com/SevereCloud/vksdk/v2/object" -import "encoding/json" - // NotificationsFeedback struct. type NotificationsFeedback struct { Attachments []WallWallpostAttachment `json:"attachments"` @@ -16,8 +14,8 @@ type NotificationsFeedback struct { // NotificationsNotification struct. type NotificationsNotification struct { Date int `json:"date"` // Date when the event has been occurred - Feedback json.RawMessage `json:"feedback"` - Parent json.RawMessage `json:"parent"` + Feedback RawMessage `json:"feedback"` + Parent RawMessage `json:"parent"` Reply NotificationsReply `json:"reply"` Type string `json:"type"` // Notification type } diff --git a/object/object.go b/object/object.go index 47e47b23..e03e7f3b 100644 --- a/object/object.go +++ b/object/object.go @@ -9,6 +9,8 @@ import ( "bytes" "encoding/json" "reflect" + + "github.com/vmihailenco/msgpack/v5" ) // Attachment interface. @@ -42,6 +44,44 @@ func (b *BaseBoolInt) UnmarshalJSON(data []byte) (err error) { return } +// DecodeMsgpack func. +func (b *BaseBoolInt) DecodeMsgpack(dec *msgpack.Decoder) (err error) { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + var ( + valueInt int + valueBool bool + ) + + switch { + case msgpack.Unmarshal(data, &valueBool) == nil: + *b = BaseBoolInt(valueBool) + case msgpack.Unmarshal(data, &valueInt) == nil: + if valueInt == 1 { + *b = true + break + } + + if valueInt == 0 { + *b = false + break + } + + fallthrough + default: + // return msgpack error + err = &json.UnmarshalTypeError{ + Value: string(data), + Type: reflect.TypeOf((*BaseBoolInt)(nil)), + } + } + + return err +} + // BaseCountry struct. type BaseCountry struct { ID int `json:"id"` @@ -151,6 +191,33 @@ func (obj *BaseImage) UnmarshalJSON(data []byte) (err error) { return err } +// DecodeMsgpack is required to support images with `src` field. +func (obj *BaseImage) DecodeMsgpack(dec *msgpack.Decoder) (err error) { + type renamedBaseImage struct { + Height float64 `msgpack:"height"` + URL string `msgpack:"url"` + Src string `msgpack:"src"` + Width float64 `msgpack:"width"` + Type string `msgpack:"type"` + } + + var renamedObj renamedBaseImage + + err = dec.Decode(&renamedObj) + + obj.Height = renamedObj.Height + obj.Width = renamedObj.Width + obj.Type = renamedObj.Type + + if renamedObj.Src == "" { + obj.URL = renamedObj.URL + } else { + obj.URL = renamedObj.Src + } + + return err +} + // BaseLikes struct. type BaseLikes struct { UserLikes BaseBoolInt `json:"user_likes"` // Information whether current user likes diff --git a/object/object_test.go b/object/object_test.go index 85d69438..97a602a0 100644 --- a/object/object_test.go +++ b/object/object_test.go @@ -1,11 +1,14 @@ package object_test import ( + "bytes" "reflect" "testing" "github.com/SevereCloud/vksdk/v2/object" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) func TestBaseBoolInt_UnmarshalJSON(t *testing.T) { @@ -17,7 +20,7 @@ func TestBaseBoolInt_UnmarshalJSON(t *testing.T) { var b object.BaseBoolInt err := b.UnmarshalJSON(data) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } @@ -31,6 +34,82 @@ func TestBaseBoolInt_UnmarshalJSON(t *testing.T) { f([]byte("null"), false, "json: cannot unmarshal null into Go value of type *object.BaseBoolInt") } +func TestBaseBoolInt_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.BaseBoolInt, wantErr string) { + t.Helper() + + var b object.BaseBoolInt + + dec := msgpack.NewDecoder(bytes.NewReader(data)) + + err := b.DecodeMsgpack(dec) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, want, b) + } + + ff := func(c string) string { + return "json: cannot unmarshal " + c + " into Go value of type *object.BaseBoolInt" + } + + // fixint + f([]byte("\x00"), false, "") + f([]byte("\x01"), true, "") + + // uint 8 + f([]byte("\xcc\x00"), false, "") + f([]byte("\xcc\x01"), true, "") + f([]byte("\xcc\x02"), false, ff("\xcc\x02")) + + // uint 16 + f([]byte("\xcd\x00\x00"), false, "") + f([]byte("\xcd\x00\x01"), true, "") + f([]byte("\xcd\x00\x02"), false, ff("\xcd\x00\x02")) + + // uint 32 + f([]byte("\xce\x00\x00\x00\x00"), false, "") + f([]byte("\xce\x00\x00\x00\x01"), true, "") + f([]byte("\xce\x00\x00\x00\x02"), false, ff("\xce\x00\x00\x00\x02")) + + // uint 64 + f([]byte("\xcf\x00\x00\x00\x00\x00\x00\x00\x00"), false, "") + f([]byte("\xcf\x00\x00\x00\x00\x00\x00\x00\x01"), true, "") + f([]byte("\xcf\x00\x00\x00\x00\x00\x00\x00\x02"), false, ff("\xcf\x00\x00\x00\x00\x00\x00\x00\x02")) + + // int 8 + f([]byte("\xd0\x00"), false, "") + f([]byte("\xd0\x01"), true, "") + f([]byte("\xd0\x02"), false, ff("\xd0\x02")) + + // int 16 + f([]byte("\xd1\x00\x00"), false, "") + f([]byte("\xd1\x00\x01"), true, "") + f([]byte("\xd1\x00\x02"), false, ff("\xd1\x00\x02")) + + // int 32 + f([]byte("\xd2\x00\x00\x00\x00"), false, "") + f([]byte("\xd2\x00\x00\x00\x01"), true, "") + f([]byte("\xd2\x00\x00\x00\x02"), false, ff("\xd2\x00\x00\x00\x02")) + + // int 64 + f([]byte("\xd3\x00\x00\x00\x00\x00\x00\x00\x00"), false, "") + f([]byte("\xd3\x00\x00\x00\x00\x00\x00\x00\x01"), true, "") + f([]byte("\xd3\x00\x00\x00\x00\x00\x00\x00\x02"), false, ff("\xd3\x00\x00\x00\x00\x00\x00\x00\x02")) + + // bool test + f([]byte{msgpcode.True}, true, "") + f([]byte{msgpcode.False}, false, "") + + // nil test + f([]byte{msgpcode.FixedArrayLow}, false, ff("\x90")) + + f(nil, false, "EOF") +} + func TestBaseImage_UnmarshalJSON(t *testing.T) { t.Parallel() @@ -40,7 +119,7 @@ func TestBaseImage_UnmarshalJSON(t *testing.T) { var b object.BaseImage err := b.UnmarshalJSON(data) - if err != nil { + if err != nil || wantErr != "" { assert.EqualError(t, err, wantErr) } @@ -76,7 +155,66 @@ func TestBaseImage_UnmarshalJSON(t *testing.T) { }, "", ) - f([]byte("null"), object.BaseImage{}, "json: cannot unmarshal ? into Go value of type BaseImage") + f([]byte("0"), object.BaseImage{}, "json: cannot unmarshal number into Go value of type object.renamedBaseImage") +} + +func TestBaseImage_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.BaseImage, wantErr string) { + t.Helper() + + var b object.BaseImage + + dec := msgpack.NewDecoder(bytes.NewReader(data)) + + err := b.DecodeMsgpack(dec) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, want, b) + } + + f([]byte("\x80"), object.BaseImage{}, "") + f([]byte{ + 0x84, 0xA3, 0x75, 0x72, 0x6C, 0xD9, 0x38, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3A, 0x2F, 0x2F, 0x70, 0x70, 0x2E, 0x76, 0x6B, 0x2E, 0x6D, 0x65, 0x2F, + 0x63, 0x36, 0x33, 0x33, 0x38, 0x32, 0x35, 0x2F, 0x76, 0x36, 0x33, 0x33, + 0x38, 0x32, 0x35, 0x30, 0x33, 0x34, 0x2F, 0x37, 0x33, 0x36, 0x39, 0x2F, + 0x77, 0x62, 0x73, 0x41, 0x73, 0x72, 0x6F, 0x6F, 0x71, 0x66, 0x41, 0x2E, + 0x6A, 0x70, 0x67, 0xA5, 0x77, 0x69, 0x64, 0x74, 0x68, 0xCC, 0x82, 0xA6, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x57, 0xA4, 0x74, 0x79, 0x70, 0x65, + 0xA1, 0x6D, + }, + object.BaseImage{ + URL: "https://pp.vk.me/c633825/v633825034/7369/wbsAsrooqfA.jpg", + Width: 130, + Height: 87, + Type: "m", + }, + "", + ) + f([]byte{ + 0x84, 0xA3, 0x73, 0x72, 0x63, 0xD9, 0x38, 0x68, 0x74, 0x74, 0x70, 0x73, + 0x3A, 0x2F, 0x2F, 0x70, 0x70, 0x2E, 0x76, 0x6B, 0x2E, 0x6D, 0x65, 0x2F, + 0x63, 0x36, 0x33, 0x33, 0x38, 0x32, 0x35, 0x2F, 0x76, 0x36, 0x33, 0x33, + 0x38, 0x32, 0x35, 0x30, 0x33, 0x34, 0x2F, 0x37, 0x33, 0x36, 0x39, 0x2F, + 0x77, 0x62, 0x73, 0x41, 0x73, 0x72, 0x6F, 0x6F, 0x71, 0x66, 0x41, 0x2E, + 0x6A, 0x70, 0x67, 0xA5, 0x77, 0x69, 0x64, 0x74, 0x68, 0xCC, 0x82, 0xA6, + 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x57, 0xA4, 0x74, 0x79, 0x70, 0x65, + 0xA1, 0x6D, + }, + object.BaseImage{ + URL: "https://pp.vk.me/c633825/v633825034/7369/wbsAsrooqfA.jpg", + Width: 130, + Height: 87, + Type: "m", + }, + "", + ) + f([]byte("xc0"), object.BaseImage{}, "msgpack: unexpected code=78 decoding map length") + f(nil, object.BaseImage{}, "EOF") } func TestBaseSticker_MaxSize(t *testing.T) { diff --git a/object/raw.go b/object/raw.go new file mode 100644 index 00000000..9492f67d --- /dev/null +++ b/object/raw.go @@ -0,0 +1,39 @@ +package object // import "github.com/SevereCloud/vksdk/v2/object" + +import "github.com/vmihailenco/msgpack/v5" + +// RawMessage is a raw encoded JSON or MessagePack value. +type RawMessage []byte + +// MarshalJSON returns m as the JSON encoding of m. +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *RawMessage) UnmarshalJSON(data []byte) error { + *m = append((*m)[0:0], data...) + return nil +} + +// EncodeMsgpack write m as the MessagePack encoding of m. +func (m RawMessage) EncodeMsgpack(enc *msgpack.Encoder) error { + _, err := enc.Writer().Write(m) + return err +} + +// DecodeMsgpack sets *m to a copy of data. +func (m *RawMessage) DecodeMsgpack(dec *msgpack.Decoder) error { + msg, err := dec.DecodeRaw() + if err != nil { + return err + } + + *m = RawMessage(msg) + + return nil +} diff --git a/object/users.go b/object/users.go index 24a08d63..7bf93b40 100644 --- a/object/users.go +++ b/object/users.go @@ -4,6 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) // User relationship status. @@ -258,6 +261,36 @@ func (personal *UsersPersonal) UnmarshalJSON(data []byte) error { return nil } +// DecodeMsgpack UsersPersonal. +// +// BUG(VK): UsersPersonal return []. +func (personal *UsersPersonal) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) { + return nil + } + + type renamedUsersPersonal UsersPersonal + + var r renamedUsersPersonal + + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + err = d.Decode(&r) + if err != nil { + return err + } + + *personal = UsersPersonal(r) + + return nil +} + // UsersRelative struct. type UsersRelative struct { BirthDate string `json:"birth_date"` // Date of child birthday (format dd.mm.yyyy) diff --git a/object/users_test.go b/object/users_test.go index 1395c8f3..ad8c9558 100644 --- a/object/users_test.go +++ b/object/users_test.go @@ -6,6 +6,8 @@ import ( "github.com/SevereCloud/vksdk/v2/object" "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" ) func TestUsersUser_ToMention(t *testing.T) { @@ -38,10 +40,12 @@ func TestUsersUserMin_ToMention(t *testing.T) { ) } -func TestUsersPersonal__UnmarshalJSON(t *testing.T) { +func TestUsersPersonal_UnmarshalJSON(t *testing.T) { t.Parallel() f := func(data []byte, wantpersonal object.UsersPersonal) { + t.Helper() + var personal object.UsersPersonal err := json.Unmarshal(data, &personal) @@ -56,3 +60,27 @@ func TestUsersPersonal__UnmarshalJSON(t *testing.T) { err := json.Unmarshal([]byte("0"), &personal) assert.Error(t, err) } + +func TestUsersPersonal_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, wantpersonal object.UsersPersonal, wantErr string) { + t.Helper() + + var personal object.UsersPersonal + + err := msgpack.Unmarshal(data, &personal) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, wantpersonal, personal) + } + + f([]byte{msgpcode.FixedArrayLow}, object.UsersPersonal{}, "") + f([]byte{ + 0x81, 0xA7, 0x61, 0x6C, 0x63, 0x6F, 0x68, 0x6F, 0x6C, 0x01, + }, object.UsersPersonal{Alcohol: 1}, "") + f([]byte("\xc2"), object.UsersPersonal{}, "msgpack: unexpected code=c2 decoding map length") + f(nil, object.UsersPersonal{}, "EOF") +} diff --git a/object/utils.go b/object/utils.go index 3bbd775d..408cf7f9 100644 --- a/object/utils.go +++ b/object/utils.go @@ -1,5 +1,13 @@ package object // import "github.com/SevereCloud/vksdk/v2/object" +import ( + "bytes" + "encoding/json" + + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + // UtilsDomainResolvedType object type. const ( UtilsDomainResolvedTypeUser = "user" @@ -15,6 +23,58 @@ type UtilsDomainResolved struct { Type string `json:"type"` } +// UnmarshalJSON UtilsDomainResolved. +// +// BUG(VK): UtilsDomainResolved return []. +func (link *UtilsDomainResolved) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("[]")) { + return nil + } + + type renamedUtilsDomainResolved UtilsDomainResolved + + var r renamedUtilsDomainResolved + + err := json.Unmarshal(data, &r) + if err != nil { + return err + } + + *link = UtilsDomainResolved(r) + + return nil +} + +// DecodeMsgpack UtilsDomainResolved. +// +// BUG(VK): UtilsDomainResolved return []. +func (link *UtilsDomainResolved) DecodeMsgpack(dec *msgpack.Decoder) error { + data, err := dec.DecodeRaw() + if err != nil { + return err + } + + if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) { + return nil + } + + type renamedUtilsDomainResolved UtilsDomainResolved + + var r renamedUtilsDomainResolved + + d := msgpack.NewDecoder(bytes.NewReader(data)) + d.SetCustomStructTag("json") + + err = d.Decode(&r) + if err != nil { + return err + } + + *link = UtilsDomainResolved(r) + + return nil +} + // UtilsLastShortenedLink struct. type UtilsLastShortenedLink struct { AccessKey string `json:"access_key"` // Access key for private stats diff --git a/object/utils_test.go b/object/utils_test.go new file mode 100644 index 00000000..1a11a6e1 --- /dev/null +++ b/object/utils_test.go @@ -0,0 +1,56 @@ +package object_test + +import ( + "encoding/json" + "testing" + + "github.com/SevereCloud/vksdk/v2/object" + "github.com/stretchr/testify/assert" + "github.com/vmihailenco/msgpack/v5" + "github.com/vmihailenco/msgpack/v5/msgpcode" +) + +func TestUtilsDomainResolved_UnmarshalJSON(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.UtilsDomainResolved) { + t.Helper() + + var resolved object.UtilsDomainResolved + + err := json.Unmarshal(data, &resolved) + assert.NoError(t, err) + assert.Equal(t, want, resolved) + } + + f([]byte("[]"), object.UtilsDomainResolved{}) + f([]byte(`{"object_id":1}`), object.UtilsDomainResolved{ObjectID: 1}) + + var resolved object.UtilsDomainResolved + err := json.Unmarshal([]byte("0"), &resolved) + assert.Error(t, err) +} + +func TestUtilsDomainResolved_DecodeMsgpack(t *testing.T) { + t.Parallel() + + f := func(data []byte, want object.UtilsDomainResolved, wantErr string) { + t.Helper() + + var resolved object.UtilsDomainResolved + + err := msgpack.Unmarshal(data, &resolved) + if err != nil || wantErr != "" { + assert.EqualError(t, err, wantErr) + } + + assert.Equal(t, want, resolved) + } + + f([]byte{msgpcode.FixedArrayLow}, object.UtilsDomainResolved{}, "") + f([]byte{ + 0x81, 0xA9, 'o', 'b', 'j', 'e', 'c', 't', '_', 'i', 'd', 0x01, + }, object.UtilsDomainResolved{ObjectID: 1}, "") + f([]byte("\xc2"), object.UtilsDomainResolved{}, "msgpack: unexpected code=c2 decoding map length") + f(nil, object.UtilsDomainResolved{}, "EOF") +} diff --git a/vkid/auth.go b/vkid/auth.go new file mode 100644 index 00000000..15f7b4b5 --- /dev/null +++ b/vkid/auth.go @@ -0,0 +1,37 @@ +// Package vkid ... +package vkid + +// SilentAuthPayload struct. +type SilentAuthPayload struct { + Auth int `json:"auth"` + Token string `json:"token"` + LoadExternalUsers bool `json:"loadExternalUsers"` + TTL int `json:"ttl"` + Type string `json:"type"` + User SilentUser `json:"user"` + UUID string `json:"uuid"` + + OAuthProvider string `json:"oauthProvider,omitempty"` + ExternalUser ExternalUser `json:"external_user,omitempty"` + IsRegistration bool `json:"is_registration,omitempty"` +} + +// SilentUser struct. +type SilentUser struct { + ID int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Avatar string `json:"avatar"` + Phone string `json:"phone"` +} + +// ExternalUser struct. +type ExternalUser struct { + ID string `json:"id,omitempty"` + Avatar string `json:"avatar,omitempty"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Phone string `json:"phone"` + BorderColor string `json:"borderColor,omitempty"` + Payload interface{} `json:"payload,omitempty"` +}