mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-06 08:02:06 +00:00
512 lines
15 KiB
Go
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
|
|
}
|