Files
warmbly/internal/app/pipedrive/api.go
T

512 lines
15 KiB
Go

package pipedrive
import (
"context"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
)
// Records as API v2 returns them (notes, users, filters, activity types and
// webhooks are v1 only).
type Me struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
CompanyDomain string `json:"company_domain"`
}
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
ActiveFlag bool `json:"active_flag"`
IsDeleted bool `json:"is_deleted"`
}
type Pipeline struct {
ID int64 `json:"id"`
Name string `json:"name"`
OrderNr int `json:"order_nr"`
IsDeleted bool `json:"is_deleted"`
}
type Stage struct {
ID int64 `json:"id"`
Name string `json:"name"`
OrderNr int `json:"order_nr"`
PipelineID int64 `json:"pipeline_id"`
DealProbability *float64 `json:"deal_probability"`
IsDeleted bool `json:"is_deleted"`
}
type Deal struct {
ID int64 `json:"id"`
Title string `json:"title"`
OwnerID *int64 `json:"owner_id"`
PersonID *int64 `json:"person_id"`
OrgID *int64 `json:"org_id"`
PipelineID int64 `json:"pipeline_id"`
StageID int64 `json:"stage_id"`
Value *float64 `json:"value"`
Currency string `json:"currency"`
Status string `json:"status"`
LostReason *string `json:"lost_reason"`
ExpectedCloseDate *string `json:"expected_close_date"`
CloseTime *string `json:"close_time"`
WonTime *string `json:"won_time"`
LostTime *string `json:"lost_time"`
AddTime string `json:"add_time"`
UpdateTime string `json:"update_time"`
IsDeleted bool `json:"is_deleted"`
}
// ContactPoint is one of a person's emails or phones.
type ContactPoint struct {
Value string `json:"value"`
Primary bool `json:"primary"`
Label string `json:"label,omitempty"`
}
type Person struct {
ID int64 `json:"id"`
Name string `json:"name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
OwnerID *int64 `json:"owner_id"`
OrgID *int64 `json:"org_id"`
Emails []ContactPoint `json:"emails"`
Phones []ContactPoint `json:"phones"`
LabelIDs []int64 `json:"label_ids"`
CustomFields map[string]any `json:"custom_fields"`
MarketingStatus string `json:"marketing_status"`
AddTime string `json:"add_time"`
UpdateTime string `json:"update_time"`
IsDeleted bool `json:"is_deleted"`
}
// Email is the person's primary address, else the first.
func (p *Person) Email() string {
for _, e := range p.Emails {
if e.Primary && strings.TrimSpace(e.Value) != "" {
return strings.ToLower(strings.TrimSpace(e.Value))
}
}
for _, e := range p.Emails {
if strings.TrimSpace(e.Value) != "" {
return strings.ToLower(strings.TrimSpace(e.Value))
}
}
return ""
}
// Phone is the person's primary phone, else the first.
func (p *Person) Phone() string {
for _, e := range p.Phones {
if e.Primary && e.Value != "" {
return e.Value
}
}
if len(p.Phones) > 0 {
return p.Phones[0].Value
}
return ""
}
type Organization struct {
ID int64 `json:"id"`
Name string `json:"name"`
Website string `json:"website"`
IsDeleted bool `json:"is_deleted"`
}
type Participant struct {
PersonID int64 `json:"person_id"`
Primary bool `json:"primary"`
}
type Activity struct {
ID int64 `json:"id"`
Subject string `json:"subject"`
Type string `json:"type"`
OwnerID *int64 `json:"owner_id"`
DealID *int64 `json:"deal_id"`
PersonID *int64 `json:"person_id"`
OrgID *int64 `json:"org_id"`
DueDate string `json:"due_date"`
DueTime string `json:"due_time"`
Done bool `json:"done"`
MarkedAsDoneTime string `json:"marked_as_done_time"`
Note string `json:"note"`
Participants []Participant `json:"participants"`
AddTime string `json:"add_time"`
UpdateTime string `json:"update_time"`
IsDeleted bool `json:"is_deleted"`
}
// Person is the activity's primary participant.
func (a *Activity) Person() int64 {
for _, p := range a.Participants {
if p.Primary {
return p.PersonID
}
}
if a.PersonID != nil {
return *a.PersonID
}
if len(a.Participants) > 0 {
return a.Participants[0].PersonID
}
return 0
}
type ActivityType struct {
ID int64 `json:"id"`
Name string `json:"name"`
KeyString string `json:"key_string"`
Color string `json:"color"`
OrderNr int `json:"order_nr"`
ActiveFlag bool `json:"active_flag"`
}
type Note struct {
ID int64 `json:"id"`
Content string `json:"content"`
DealID *int64 `json:"deal_id"`
PersonID *int64 `json:"person_id"`
OrgID *int64 `json:"org_id"`
UserID int64 `json:"user_id"`
AddTime string `json:"add_time"`
UpdateTime string `json:"update_time"`
ActiveFlag bool `json:"active_flag"`
}
type FieldOption struct {
ID int64 `json:"id"`
Label string `json:"label"`
Color string `json:"color"`
}
type Field struct {
FieldName string `json:"field_name"`
FieldCode string `json:"field_code"`
FieldType string `json:"field_type"`
Options []FieldOption `json:"options"`
IsCustomField bool `json:"is_custom_field"`
}
type Filter struct {
ID int64 `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
ActiveFlag bool `json:"active_flag"`
UpdateTime string `json:"update_time"`
}
type Webhook struct {
ID int64 `json:"id"`
SubscriptionURL string `json:"subscription_url"`
EventAction string `json:"event_action"`
EventObject string `json:"event_object"`
IsActive any `json:"is_active"`
}
func id(v int64) string { return strconv.FormatInt(v, 10) }
func idPath(prefix string, v int64) string { return prefix + "/" + id(v) }
// ---------- account ----------
func (c *Client) Me(ctx context.Context) (*Me, error) {
var out Me
_, err := c.call(ctx, http.MethodGet, "/v1/users/me", nil, nil, &out)
return &out, err
}
func (c *Client) Users(ctx context.Context) ([]User, error) {
var out []User
_, err := c.call(ctx, http.MethodGet, "/v1/users", nil, nil, &out)
return out, err
}
// ---------- pipelines ----------
func (c *Client) Pipelines(ctx context.Context) ([]Pipeline, error) {
return listV2[Pipeline](ctx, c, "/api/v2/pipelines", nil, 0)
}
func (c *Client) Stages(ctx context.Context) ([]Stage, error) {
return listV2[Stage](ctx, c, "/api/v2/stages", nil, 0)
}
// ---------- deals ----------
func (c *Client) Deals(ctx context.Context, q url.Values, max int) ([]Deal, error) {
return listV2[Deal](ctx, c, "/api/v2/deals", q, max)
}
func (c *Client) GetDeal(ctx context.Context, dealID int64) (*Deal, error) {
var out Deal
_, err := c.call(ctx, http.MethodGet, idPath("/api/v2/deals", dealID), nil, nil, &out)
return &out, err
}
func (c *Client) CreateDeal(ctx context.Context, body map[string]any) (*Deal, error) {
var out Deal
_, err := c.call(ctx, http.MethodPost, "/api/v2/deals", nil, body, &out)
return &out, err
}
func (c *Client) UpdateDeal(ctx context.Context, dealID int64, body map[string]any) (*Deal, error) {
var out Deal
_, err := c.call(ctx, http.MethodPatch, idPath("/api/v2/deals", dealID), nil, body, &out)
return &out, err
}
func (c *Client) DeleteDeal(ctx context.Context, dealID int64) error {
_, err := c.call(ctx, http.MethodDelete, idPath("/api/v2/deals", dealID), nil, nil, nil)
return err
}
// ---------- persons ----------
func (c *Client) Persons(ctx context.Context, q url.Values, max int) ([]Person, error) {
return listV2[Person](ctx, c, "/api/v2/persons", c.withMarketing(q), max)
}
// noMarketing remembers companies that refused the marketing_status field (no
// Campaigns add-on), so later reads stop asking for it.
var noMarketing sync.Map
func (c *Client) marketingOff() bool {
_, ok := noMarketing.Load(c.company)
return ok
}
func (c *Client) setMarketingOff() { noMarketing.Store(c.company, true) }
// withMarketing asks for the marketing status where the company has one.
func (c *Client) withMarketing(q url.Values) url.Values {
if q == nil {
q = url.Values{}
}
if !c.marketingOff() {
q.Set("include_fields", "marketing_status")
}
return q
}
// GetPerson reads one person, with its marketing status where the company
// has one.
func (c *Client) GetPerson(ctx context.Context, personID int64) (*Person, error) {
var out Person
_, err := c.call(ctx, http.MethodGet, idPath("/api/v2/persons", personID), c.withMarketing(nil), nil, &out)
return &out, err
}
// PersonsByIDs reads up to 100 persons at once.
func (c *Client) PersonsByIDs(ctx context.Context, ids []int64) ([]Person, error) {
var out []Person
for start := 0; start < len(ids); start += 100 {
end := min(start+100, len(ids))
parts := make([]string, 0, end-start)
for _, v := range ids[start:end] {
parts = append(parts, id(v))
}
page, err := c.Persons(ctx, url.Values{"ids": {strings.Join(parts, ",")}}, 0)
if err != nil {
return out, err
}
out = append(out, page...)
}
return out, nil
}
// FindPersonByEmail returns the id of the person with this address, or 0.
func (c *Client) FindPersonByEmail(ctx context.Context, email string) (int64, error) {
var out struct {
Items []struct {
Item struct {
ID int64 `json:"id"`
} `json:"item"`
} `json:"items"`
}
q := url.Values{"term": {email}, "fields": {"email"}, "exact_match": {"true"}, "limit": {"1"}}
if _, err := c.call(ctx, http.MethodGet, "/api/v2/persons/search", q, nil, &out); err != nil {
return 0, err
}
if len(out.Items) == 0 {
return 0, nil
}
return out.Items[0].Item.ID, nil
}
func (c *Client) CreatePerson(ctx context.Context, body map[string]any) (*Person, error) {
var out Person
_, err := c.call(ctx, http.MethodPost, "/api/v2/persons", nil, body, &out)
return &out, err
}
func (c *Client) UpdatePerson(ctx context.Context, personID int64, body map[string]any) (*Person, error) {
var out Person
_, err := c.call(ctx, http.MethodPatch, idPath("/api/v2/persons", personID), nil, body, &out)
return &out, err
}
// ---------- organizations ----------
func (c *Client) GetOrganization(ctx context.Context, orgID int64) (*Organization, error) {
var out Organization
_, err := c.call(ctx, http.MethodGet, idPath("/api/v2/organizations", orgID), nil, nil, &out)
return &out, err
}
// FindOrganization returns the organization named exactly name, or nil.
func (c *Client) FindOrganization(ctx context.Context, name string) (*Organization, error) {
if len([]rune(strings.TrimSpace(name))) < 2 {
return nil, nil
}
var out struct {
Items []struct {
Item Organization `json:"item"`
} `json:"items"`
}
q := url.Values{"term": {name}, "fields": {"name"}, "exact_match": {"true"}, "limit": {"1"}}
if _, err := c.call(ctx, http.MethodGet, "/api/v2/organizations/search", q, nil, &out); err != nil {
return nil, err
}
if len(out.Items) == 0 {
return nil, nil
}
o := out.Items[0].Item
return &o, nil
}
func (c *Client) CreateOrganization(ctx context.Context, body map[string]any) (*Organization, error) {
var out Organization
_, err := c.call(ctx, http.MethodPost, "/api/v2/organizations", nil, body, &out)
return &out, err
}
// ---------- activities ----------
func (c *Client) Activities(ctx context.Context, q url.Values, max int) ([]Activity, error) {
return listV2[Activity](ctx, c, "/api/v2/activities", q, max)
}
func (c *Client) GetActivity(ctx context.Context, activityID int64) (*Activity, error) {
var out Activity
_, err := c.call(ctx, http.MethodGet, idPath("/api/v2/activities", activityID), nil, nil, &out)
return &out, err
}
func (c *Client) CreateActivity(ctx context.Context, body map[string]any) (*Activity, error) {
var out Activity
_, err := c.call(ctx, http.MethodPost, "/api/v2/activities", nil, body, &out)
return &out, err
}
func (c *Client) UpdateActivity(ctx context.Context, activityID int64, body map[string]any) (*Activity, error) {
var out Activity
_, err := c.call(ctx, http.MethodPatch, idPath("/api/v2/activities", activityID), nil, body, &out)
return &out, err
}
func (c *Client) DeleteActivity(ctx context.Context, activityID int64) error {
_, err := c.call(ctx, http.MethodDelete, idPath("/api/v2/activities", activityID), nil, nil, nil)
return err
}
func (c *Client) ActivityTypes(ctx context.Context) ([]ActivityType, error) {
var out []ActivityType
_, err := c.call(ctx, http.MethodGet, "/v1/activityTypes", nil, nil, &out)
return out, err
}
// ---------- notes ----------
func (c *Client) Notes(ctx context.Context, q url.Values, max int) ([]Note, error) {
return listV1[Note](ctx, c, "/v1/notes", q, max)
}
func (c *Client) GetNote(ctx context.Context, noteID int64) (*Note, error) {
var out Note
_, err := c.call(ctx, http.MethodGet, idPath("/v1/notes", noteID), nil, nil, &out)
return &out, err
}
func (c *Client) CreateNote(ctx context.Context, body map[string]any) (*Note, error) {
var out Note
_, err := c.call(ctx, http.MethodPost, "/v1/notes", nil, body, &out)
return &out, err
}
func (c *Client) UpdateNote(ctx context.Context, noteID int64, content string) error {
_, err := c.call(ctx, http.MethodPut, idPath("/v1/notes", noteID), nil, map[string]any{"content": content}, nil)
return err
}
func (c *Client) DeleteNote(ctx context.Context, noteID int64) error {
_, err := c.call(ctx, http.MethodDelete, idPath("/v1/notes", noteID), nil, nil, nil)
return err
}
// ---------- fields ----------
func (c *Client) PersonFields(ctx context.Context) ([]Field, error) {
return listV2[Field](ctx, c, "/api/v2/personFields", nil, 0)
}
func (c *Client) CreatePersonField(ctx context.Context, name, fieldType string, options []string) (*Field, error) {
body := map[string]any{"field_name": name, "field_type": fieldType}
if len(options) > 0 {
opts := make([]map[string]string, 0, len(options))
for _, o := range options {
opts = append(opts, map[string]string{"label": o})
}
body["options"] = opts
}
var out Field
_, err := c.call(ctx, http.MethodPost, "/api/v2/personFields", nil, body, &out)
return &out, err
}
// ---------- filters ----------
// PersonFilters lists the company's saved people filters.
func (c *Client) PersonFilters(ctx context.Context) ([]Filter, error) {
var out []Filter
_, err := c.call(ctx, http.MethodGet, "/v1/filters", url.Values{"type": {"people"}}, nil, &out)
return out, err
}
// ---------- leads ----------
func (c *Client) CreateLead(ctx context.Context, body map[string]any) error {
_, err := c.call(ctx, http.MethodPost, "/v1/leads", nil, body, nil)
return err
}
// ---------- webhooks ----------
func (c *Client) Webhooks(ctx context.Context) ([]Webhook, error) {
var out []Webhook
_, err := c.call(ctx, http.MethodGet, "/v1/webhooks", nil, nil, &out)
return out, err
}
func (c *Client) CreateWebhook(ctx context.Context, body map[string]any) error {
_, err := c.call(ctx, http.MethodPost, "/v1/webhooks", nil, body, nil)
return err
}
func (c *Client) DeleteWebhook(ctx context.Context, webhookID int64) error {
_, err := c.call(ctx, http.MethodDelete, idPath("/v1/webhooks", webhookID), nil, nil, nil)
return err
}