/* Package longpoll implements Bots Long Poll API. See more https://dev.vk.com/ru/api/bots-long-poll/getting-started */ package longpoll // import "github.com/SevereCloud/vksdk/v2/longpoll-bot" import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "github.com/SevereCloud/vksdk/v2" "github.com/SevereCloud/vksdk/v2/api" "github.com/SevereCloud/vksdk/v2/events" "github.com/SevereCloud/vksdk/v2/internal" ) // Response struct. type Response struct { Ts string `json:"ts"` Updates []events.GroupEvent `json:"updates"` Failed int `json:"failed"` } // LongPoll struct. type LongPoll struct { GroupID int Server string Key string Ts string Wait int VK *api.VK Client *http.Client cancel context.CancelFunc funcFullResponseList []func(Response) events.FuncList } // NewLongPoll returns a new LongPoll. // // The LongPoll 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. func NewLongPoll(vk *api.VK, groupID int) (*LongPoll, error) { lp := &LongPoll{ VK: vk, GroupID: groupID, Wait: 25, Client: http.DefaultClient, } lp.FuncList = *events.NewFuncList() err := lp.updateServer(true) return lp, err } // NewLongPollCommunity returns a new LongPoll for community token. // // The LongPoll 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. func NewLongPollCommunity(vk *api.VK) (*LongPoll, error) { resp, err := vk.GroupsGetByID(nil) if err != nil { return nil, err } lp := &LongPoll{ VK: vk, GroupID: resp[0].ID, Wait: 25, Client: http.DefaultClient, } lp.FuncList = *events.NewFuncList() err = lp.updateServer(true) return lp, err } func (lp *LongPoll) updateServer(updateTs bool) error { params := api.Params{ "group_id": lp.GroupID, } serverSetting, err := lp.VK.GroupsGetLongPollServer(params) if err != nil { return err } lp.Key = serverSetting.Key lp.Server = serverSetting.Server if updateTs { lp.Ts = serverSetting.Ts } return nil } func (lp *LongPoll) check(ctx context.Context) (response Response, err error) { u := fmt.Sprintf("%s?act=a_check&key=%s&ts=%s&wait=%d", lp.Server, lp.Key, lp.Ts, lp.Wait) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return response, err } resp, err := lp.Client.Do(req) if err != nil { return response, err } defer resp.Body.Close() response, err = parseResponse(resp.Body) if err != nil { return response, err } err = lp.checkResponse(response) return response, err } func parseResponse(reader io.Reader) (response Response, err error) { decoder := json.NewDecoder(reader) for decoder.More() { token, err := decoder.Token() if err != nil { if errors.Is(err, io.EOF) { break } return response, err } t, ok := token.(string) if !ok { continue } switch t { case "failed": raw, err := decoder.Token() if err != nil { return response, err } response.Failed = int(raw.(float64)) case "updates": var updates []events.GroupEvent err = decoder.Decode(&updates) if err != nil { return response, err } response.Updates = updates case "ts": // can be a number in the response with "failed" field: {"ts":8,"failed":1} // or string, e.g. {"ts":"8","updates":[]} rawTs, err := decoder.Token() if err != nil { return response, err } if ts, isNumber := rawTs.(float64); isNumber { response.Ts = strconv.Itoa(int(ts)) } else { response.Ts = rawTs.(string) } } } return response, err } func (lp *LongPoll) checkResponse(response Response) (err error) { switch response.Failed { case 0: lp.Ts = response.Ts case 1: lp.Ts = response.Ts case 2: err = lp.updateServer(false) case 3: err = lp.updateServer(true) default: err = &Failed{response.Failed} } return } func (lp *LongPoll) autoSetting(ctx context.Context) error { params := api.Params{ "group_id": lp.GroupID, "enabled": true, "api_version": vksdk.API, }.WithContext(ctx) for _, event := range lp.ListEvents() { params[string(event)] = true } // Updating LongPoll settings _, err := lp.VK.GroupsSetLongPollSettings(params) return err } // Run handler. func (lp *LongPoll) Run() error { return lp.RunWithContext(context.Background()) } // RunWithContext handler. func (lp *LongPoll) RunWithContext(ctx context.Context) error { return lp.run(ctx) } func (lp *LongPoll) run(ctx context.Context) error { ctx, lp.cancel = context.WithCancel(ctx) if err := lp.autoSetting(ctx); err != nil { return err } for { select { case _, ok := <-ctx.Done(): if !ok { return nil } default: resp, err := lp.check(ctx) if err != nil { return err } ctx = context.WithValue(ctx, internal.LongPollTsKey, resp.Ts) for _, event := range resp.Updates { err = lp.Handler(ctx, event) if err != nil { return err } } for _, f := range lp.funcFullResponseList { f(resp) } } } } // Shutdown gracefully shuts down the longpoll without interrupting any active connections. func (lp *LongPoll) Shutdown() { if lp.cancel != nil { lp.cancel() } } // FullResponse handler. func (lp *LongPoll) FullResponse(f func(Response)) { lp.funcFullResponseList = append(lp.funcFullResponseList, f) }