135 lines
4.4 KiB
Go
135 lines
4.4 KiB
Go
// Package bitrix — минимальный клиент REST API Битрикс24 через входящий вебхук.
|
||
// Используется для выгрузки лидов и контактов в «Отчёт по каналам» и для лидов онлайн-записи,
|
||
// которые подтверждает оператор (см. leads.go).
|
||
package bitrix
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type Client struct {
|
||
WebhookURL string
|
||
HTTP *http.Client
|
||
}
|
||
|
||
func New(webhookURL string) *Client {
|
||
return &Client{
|
||
WebhookURL: strings.TrimRight(strings.TrimSpace(webhookURL), "/"),
|
||
HTTP: &http.Client{Timeout: 30 * time.Second},
|
||
}
|
||
}
|
||
|
||
func (c *Client) Configured() bool { return c != nil && c.WebhookURL != "" }
|
||
|
||
type phoneField struct {
|
||
Value string `json:"VALUE"`
|
||
}
|
||
|
||
type crmItem struct {
|
||
ID string `json:"ID"`
|
||
SourceID string `json:"SOURCE_ID"`
|
||
Phone []phoneField `json:"PHONE"`
|
||
}
|
||
|
||
type listResponse struct {
|
||
Result []crmItem `json:"result"`
|
||
Total int `json:"total"`
|
||
Error string `json:"error"`
|
||
Descr string `json:"error_description"`
|
||
}
|
||
|
||
// Record — лид или контакт CRM: нужные нам поля, без лишнего.
|
||
type Record struct {
|
||
ID string
|
||
SourceID string // у контактов всегда пусто — поле есть только у лидов
|
||
Phones []string
|
||
}
|
||
|
||
const pageSize = 50
|
||
|
||
// Progress — колбэк хода постраничной выгрузки: сколько записей забрано из скольких всего (по ответу API).
|
||
type Progress func(done, total int)
|
||
|
||
// list — одна выборка (лиды или контакты) целиком, со всеми страницами.
|
||
func (c *Client) list(ctx context.Context, method string, selectFields []string, filter map[string]any, onPage Progress) ([]Record, error) {
|
||
var out []Record
|
||
start := 0
|
||
for {
|
||
body := map[string]any{
|
||
"select": selectFields,
|
||
"order": map[string]string{"ID": "ASC"},
|
||
"start": start,
|
||
}
|
||
if len(filter) > 0 {
|
||
body["filter"] = filter
|
||
}
|
||
b, err := json.Marshal(body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
||
c.WebhookURL+"/"+method+".json", strings.NewReader(string(b)))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
req.Header.Set("Content-Type", "application/json")
|
||
resp, err := c.HTTP.Do(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("битрикс %s: %w", method, redactURL(err))
|
||
}
|
||
var lr listResponse
|
||
decErr := json.NewDecoder(resp.Body).Decode(&lr)
|
||
resp.Body.Close()
|
||
if decErr != nil {
|
||
return nil, fmt.Errorf("битрикс %s: не разобрать ответ: %w", method, decErr)
|
||
}
|
||
if lr.Error != "" {
|
||
return nil, fmt.Errorf("битрикс %s: %s (%s)", method, lr.Error, lr.Descr)
|
||
}
|
||
for _, item := range lr.Result {
|
||
r := Record{ID: item.ID, SourceID: item.SourceID}
|
||
for _, p := range item.Phone {
|
||
if v := strings.TrimSpace(p.Value); v != "" {
|
||
r.Phones = append(r.Phones, v)
|
||
}
|
||
}
|
||
out = append(out, r)
|
||
}
|
||
if onPage != nil {
|
||
onPage(len(out), lr.Total)
|
||
}
|
||
start += pageSize
|
||
if start >= lr.Total || len(lr.Result) == 0 {
|
||
break
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// dateFilter — ограничение по дате создания записи (DATE_CREATE), чтобы не тянуть всю историю CRM
|
||
// за годы. from/to нулевые — фильтр не применяется (выгружается всё, для старых мест вызова).
|
||
func dateFilter(from, to time.Time) map[string]any {
|
||
if from.IsZero() || to.IsZero() {
|
||
return nil
|
||
}
|
||
return map[string]any{
|
||
">=DATE_CREATE": from.Format(time.RFC3339),
|
||
"<DATE_CREATE": to.Format(time.RFC3339),
|
||
}
|
||
}
|
||
|
||
// Leads — лиды CRM за период [from, to) с их источником (SOURCE_ID) и номерами.
|
||
func (c *Client) Leads(ctx context.Context, from, to time.Time, onPage Progress) ([]Record, error) {
|
||
return c.list(ctx, "crm.lead.list", []string{"ID", "SOURCE_ID", "PHONE"}, dateFilter(from, to), onPage)
|
||
}
|
||
|
||
// Contacts — контакты CRM за период [from, to) с номерами. У контактов источника нет.
|
||
func (c *Client) Contacts(ctx context.Context, from, to time.Time, onPage Progress) ([]Record, error) {
|
||
return c.list(ctx, "crm.contact.list", []string{"ID", "PHONE"}, dateFilter(from, to), onPage)
|
||
}
|