matterbridge/vendor/github.com/SevereCloud/vksdk/v2/api/api.go

437 lines
10 KiB
Go
Raw Normal View History

/*
Package api implements VK API.
See more https://dev.vk.com/ru/api/api-requests
*/
package api // import "github.com/SevereCloud/vksdk/v2/api"
import (
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"mime"
"net/http"
"net/url"
"reflect"
"sync"
"sync/atomic"
"time"
"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.
const (
Version = vksdk.API
MethodURL = "https://api.vk.com/method/"
)
// VKontakte API methods (except for methods from secure and ads sections)
// with user access key or service access key can be accessed
// no more than 3 times per second. The community access key is limited
// to 20 requests per second.
//
// Maximum amount of calls to the secure section methods depends
// on the app's users amount. If an app has less than
// 10 000 users, 5 requests per second,
// up to 100 000 8 requests,
// up to 1 000 000 20 requests,
// 1 000 000+ 35 requests.
//
// The ads section methods are subject to their own limitations,
// you can read them on this page - https://dev.vk.com/ru/method/ads
//
// If one of this limits is exceeded, the server will return following error:
// "Too many requests per second". (errors.TooMany).
//
// If your app's logic implies many requests in a row, check the execute method.
// It allows for up to 25 requests for different methods in a single request.
//
// In addition to restrictions on the frequency of calls, there are also
// quantitative restrictions on calling the same type of methods.
//
// After exceeding the quantitative limit, access to a particular method may
// require entering a captcha (see https://dev.vk.com/ru/api/captcha-error),
// and may also be temporarily restricted (in this case, the server does
// not return a response to the call of a particular method, but handles
// any other requests without problems).
//
// If this error occurs, the following parameters are also passed in
// the error message:
//
// CaptchaSID - identifier captcha.
//
// CaptchaImg - a link to the image that you want to show the user
// to enter text from that image.
//
// In this case, you should ask the user to enter text from
// the CaptchaImg image and repeat the request by adding parameters to it:
//
// captcha_sid - the obtained identifier;
//
// captcha_key - text entered by the user.
//
// More info: https://dev.vk.com/ru/api/api-requests
const (
LimitUserToken = 3
LimitGroupToken = 20
)
// VK struct.
type VK struct {
accessTokens []string
lastToken uint32
MethodURL string
Version string
Client *http.Client
Limit int
UserAgent string
Handler func(method string, params ...Params) (Response, error)
msgpack bool
zstd bool
mux sync.Mutex
lastTime time.Time
rps int
}
// Response struct.
type Response struct {
Response object.RawMessage `json:"response"`
Error Error `json:"error"`
ExecuteErrors ExecuteErrors `json:"execute_errors"`
}
// NewVK returns a new VK.
//
// The VKSDK will use the http.DefaultClient.
// This means that if the http.DefaultClient is modified by other components
// of your application the modifications will be picked up by the SDK as well.
//
// In some cases this might be intended, but it is a better practice
// to create a custom HTTP Client to share explicitly through
// your application. You can configure the VKSDK to use the custom
// HTTP Client by setting the VK.Client value.
//
// This set limit 20 requests per second for one token.
func NewVK(tokens ...string) *VK {
var vk VK
vk.accessTokens = tokens
vk.Version = Version
vk.Handler = vk.DefaultHandler
vk.MethodURL = MethodURL
vk.Client = http.DefaultClient
vk.Limit = LimitGroupToken
vk.UserAgent = internal.UserAgent
return &vk
}
// getToken return next token (simple round-robin).
func (vk *VK) getToken() string {
i := atomic.AddUint32(&vk.lastToken, 1)
return vk.accessTokens[(int(i)-1)%len(vk.accessTokens)]
}
// Params type.
type Params map[string]interface{}
// Lang - determines the language for the data to be displayed on. For
// example country and city names. If you use a non-cyrillic language,
// cyrillic symbols will be transliterated automatically.
// Numeric format from account.getInfo is supported as well.
//
// p.Lang(object.LangRU)
//
// See all language code in module object.
func (p Params) Lang(v int) Params {
p["lang"] = v
return p
}
// TestMode allows to send requests from a native app without switching it on
// for all users.
func (p Params) TestMode(v bool) Params {
p["test_mode"] = v
return p
}
// CaptchaSID received ID.
//
// See https://dev.vk.com/ru/api/captcha-error
func (p Params) CaptchaSID(v string) Params {
p["captcha_sid"] = v
return p
}
// CaptchaKey text input.
//
// See https://dev.vk.com/ru/api/captcha-error
func (p Params) CaptchaKey(v string) Params {
p["captcha_key"] = v
return p
}
// Confirm parameter.
//
// See https://dev.vk.com/ru/api/confirmation-required-error
func (p Params) Confirm(v bool) Params {
p["confirm"] = v
return p
}
// WithContext parameter.
func (p Params) WithContext(ctx context.Context) Params {
p[":context"] = ctx
return p
}
func buildQuery(sliceParams ...Params) (context.Context, url.Values) {
query := url.Values{}
ctx := context.Background()
for _, params := range sliceParams {
for key, value := range params {
switch key {
case "access_token":
continue
case ":context":
ctx = value.(context.Context)
default:
query.Set(key, FmtValue(value, 0))
}
}
}
return ctx, query
}
// DefaultHandler provides access to VK API methods.
func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, error) {
u := vk.MethodURL + method
ctx, query := buildQuery(sliceParams...)
attempt := 0
for {
var response Response
attempt++
// Rate limiting
if vk.Limit > 0 {
vk.mux.Lock()
sleepTime := time.Second - time.Since(vk.lastTime)
if sleepTime < 0 {
vk.lastTime = time.Now()
vk.rps = 0
} else if vk.rps == vk.Limit*len(vk.accessTokens) {
time.Sleep(sleepTime)
vk.lastTime = time.Now()
vk.rps = 0
}
vk.rps++
vk.mux.Unlock()
}
rawBody := bytes.NewBufferString(query.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, rawBody)
if err != nil {
return response, err
}
acceptEncoding := "gzip"
if vk.zstd {
acceptEncoding = "zstd"
}
token := sliceParams[len(sliceParams)-1]["access_token"].(string)
req.Header.Set("Authorization", "Bearer "+token)
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
}
switch resp.Header.Get("Content-Encoding") {
case "zstd":
zstdReader, _ := zstd.NewReader(resp.Body)
defer zstdReader.Close()
reader = zstdReader
case "gzip":
gzipReader, _ := gzip.NewReader(resp.Body)
defer gzipReader.Close()
reader = gzipReader
default:
reader = resp.Body
}
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, &InvalidContentType{mediatype}
}
_ = resp.Body.Close()
switch response.Error.Code {
case ErrNoType:
return response, nil
case ErrTooMany:
if attempt < vk.Limit {
continue
}
return response, &response.Error
}
return response, &response.Error
}
}
// Request provides access to VK API methods.
func (vk *VK) Request(method string, sliceParams ...Params) ([]byte, error) {
token := vk.getToken()
reqParams := Params{
"access_token": token,
"v": vk.Version,
}
sliceParams = append(sliceParams, reqParams)
if vk.msgpack {
method += ".msgpack"
}
resp, err := vk.Handler(method, sliceParams...)
return resp.Response, err
}
// RequestUnmarshal provides access to VK API methods.
func (vk *VK) RequestUnmarshal(method string, obj interface{}, sliceParams ...Params) error {
rawResponse, err := vk.Request(method, sliceParams...)
if err != nil {
return err
}
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 {
switch f := value; value.Kind() {
case reflect.Invalid:
return ""
case reflect.Bool:
return fmtBool(f.Bool())
case reflect.Array, reflect.Slice:
s := ""
for i := 0; i < f.Len(); i++ {
if i > 0 {
s += ","
}
s += FmtValue(f.Index(i).Interface(), depth)
}
return s
case reflect.Ptr:
// pointer to array or slice or struct? ok at top level
// but not embedded (avoid loops)
if depth == 0 && f.Pointer() != 0 {
switch a := f.Elem(); a.Kind() {
case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map:
return FmtValue(a.Interface(), depth+1)
}
}
}
return fmt.Sprint(value)
}
// FmtValue return vk format string.
func FmtValue(value interface{}, depth int) string {
if value == nil {
return ""
}
switch f := value.(type) {
case bool:
return fmtBool(f)
case object.Attachment:
return f.ToAttachment()
case object.JSONObject:
return f.ToJSON()
case reflect.Value:
return fmtReflectValue(f, depth)
}
return fmtReflectValue(reflect.ValueOf(value), depth)
}