diff --git a/README.md b/README.md index 43909e4..1f42831 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@

Quick start • + Migrate from v2Why intercom-go?ExamplesAPI coverage • @@ -85,6 +86,14 @@ client options: client, err := intercom.NewClient("access-token", intercom.WithRegion(intercom.EU)) ``` +## Migrating from the legacy v2 client + +`intercom-go` is an independent, community-maintained SDK rather than a drop-in +replacement for `gopkg.in/intercom/intercom-go.v2`. It uses the current Intercom +API model and requires a `context.Context` on each service call. The +[migration guide](docs/migrating-from-intercom-go-v2.md) maps common workflows +and calls out the deliberate API differences. + ## Everyday API ### Retrieve and search contacts @@ -229,6 +238,9 @@ internal under [`internal/generated/intercom`](internal/generated/intercom). - [`examples/identify_admin`](examples/identify_admin) - [`examples/search_contacts`](examples/search_contacts) +- [`examples/list_conversations`](examples/list_conversations) +- [`examples/observe_rate_limits`](examples/observe_rate_limits) +- [`examples/verify_webhook`](examples/verify_webhook) ## Support and Security diff --git a/docs/assets/social-preview-background.png b/docs/assets/social-preview-background.png new file mode 100644 index 0000000..265c319 Binary files /dev/null and b/docs/assets/social-preview-background.png differ diff --git a/docs/assets/social-preview-overlay.svg b/docs/assets/social-preview-overlay.svg new file mode 100644 index 0000000..5eab831 --- /dev/null +++ b/docs/assets/social-preview-overlay.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + intercom-go + Modern Go SDK for Intercom API 2.15 + + Typed services · Safe retries · Verified webhooks + Unofficial and community-maintained + + diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png new file mode 100644 index 0000000..97a2b3e Binary files /dev/null and b/docs/assets/social-preview.png differ diff --git a/docs/launch-checklist.md b/docs/launch-checklist.md new file mode 100644 index 0000000..a69878d --- /dev/null +++ b/docs/launch-checklist.md @@ -0,0 +1,75 @@ +# Launch Checklist + +This checklist prepares the public `v0.2.2` release and retains the proposed +community announcements for explicit approval. Do not submit either draft +without approval of its final text, target, links, and account identity. + +## Repository settings + +- [x] Repository description: `Modern, production-ready Go SDK for Intercom API 2.15 (unofficial, community-maintained).` +- [x] Homepage: +- [x] Topics: `api-client`, `go`, `golang`, `intercom`, `intercom-api`, `openapi`, `sdk`. +- [ ] Upload [`docs/assets/social-preview.png`](assets/social-preview.png), a 1280×640 PNG under 1 MB, in GitHub's **Settings → General → Social preview**. + +## Release checks + +- [x] Run `make pre-push`. +- [ ] Merge the release-preparation pull request into `main`. +- [ ] Publish immutable tag and GitHub release `v0.2.2`. +- [ ] Verify `GOPROXY=proxy.golang.org go list -m github.com/uffejaeger/intercom-go@v0.2.2`. +- [ ] Verify shows `v0.2.2`. + +Installation command: + +```sh +go get github.com/uffejaeger/intercom-go@v0.2.2 +``` + +Release summary: + +> v0.2.2 adds a migration guide from the legacy v2 client and runnable examples +> for verified webhooks, conversation iteration, and response/rate-limit +> observability. It makes no public Go API changes. + +## Draft: Go Forum Releases + +**Title:** `intercom-go v0.2.2 — a community-maintained Go SDK for Intercom API 2.15` + +> I released `intercom-go` v0.2.2, an unofficial, community-maintained Go SDK +> for Intercom API 2.15. +> +> It keeps generated OpenAPI code internal while exposing idiomatic services, +> opt-in conservative retries, regional endpoints, request/rate-limit metadata, +> verified webhooks, and local HTTP test support. +> +> Install: `go get github.com/uffejaeger/intercom-go@v0.2.2` +> +> The release includes a migration guide for +> `gopkg.in/intercom/intercom-go.v2` and runnable webhook, conversation, and +> observability examples: +> +> I would especially value feedback from teams replacing the legacy client: +> which workflow or migration helper would make evaluation easier? + +## Draft: Intercom Community + +**Title:** `Community Go SDK for Intercom API 2.15 — looking for migration feedback` + +> I maintain `intercom-go`, an unofficial, community-maintained Go SDK for +> Intercom API 2.15. It is not affiliated with or endorsed by Intercom. +> +> The project offers typed services, optional conservative retries, regional +> endpoints, response/rate-limit metadata, verified webhooks, and local HTTP +> test support. The latest release also includes a migration guide for the +> legacy `gopkg.in/intercom/intercom-go.v2` client: +> +> +> If you are using Go with Intercom, which legacy-client workflow would you +> want documented or supported first? + +## Approval gate + +Before posting either draft, confirm the exact final text, destination URL, +account identity, and every linked page with the repository owner. Do not +cross-post or submit to newsletters, curated lists, Reddit, or Intercom +documentation without separate approval. diff --git a/docs/migrating-from-intercom-go-v2.md b/docs/migrating-from-intercom-go-v2.md new file mode 100644 index 0000000..1c922b7 --- /dev/null +++ b/docs/migrating-from-intercom-go-v2.md @@ -0,0 +1,116 @@ +# Migrating From `intercom-go.v2` + +This SDK is an independent, community-maintained alternative to +[`gopkg.in/intercom/intercom-go.v2`](https://github.com/intercom/intercom-go). +It is not a drop-in replacement: its public API is shaped around the current +Intercom API and requires a `context.Context` for every request. + +Start a migration on a branch, upgrade one workflow at a time, and retain the +legacy client until its replacement has been exercised against a test +workspace. Never run both clients with the same request path in production +unless the operation is safe to duplicate. + +## Install and construct a client + +Replace the legacy module: + +```sh +go get github.com/uffejaeger/intercom-go@v0.2.2 +``` + +```go +// Before +import intercom "gopkg.in/intercom/intercom-go.v2" + +legacy := intercom.NewClient("access-token", "") + +// After +import intercom "github.com/uffejaeger/intercom-go" + +client, err := intercom.NewClient("access-token") +if err != nil { + return err +} +``` + +For applications that already keep credentials in the environment, use +`intercom.NewClientFromEnv()`. Select a non-US Intercom region at construction +time with `intercom.WithRegion(intercom.EU)` or `intercom.WithRegion(intercom.AU)`. + +## Add a context and deadline + +Legacy calls do not take a context. Give each replacement request a deadline +that covers its expected work, including any retry waits: + +```go +ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) +defer cancel() + +contact, err := client.Contacts.Get(ctx, contactID) +``` + +## Map common contact workflows + +| Legacy v2 workflow | `intercom-go` workflow | +| --- | --- | +| `ic.Contacts.FindByID(id)` | `client.Contacts.Get(ctx, id)` | +| `ic.Contacts.FindByUserID(id)` | `client.Contacts.GetByExternalID(ctx, id)` | +| `ic.Contacts.ListByEmail(email, ...)` | `client.Contacts.Search(ctx, intercom.ContactSearch{Field: "email", Operator: intercom.ContactSearchEquals, Value: email})` | +| `ic.Contacts.List(...)` | `client.Contacts.List(ctx)` for the first page, or `SearchIter` for cursor-paginated search results | +| `ic.Contacts.Create(...)` | `client.Contacts.Create(ctx, intercom.ContactCreate{...})` | +| `ic.Contacts.Update(...)` | `client.Contacts.Update(ctx, contactID, intercom.ContactUpdate{...})` | + +The older SDK distinguishes users and contacts in places where the current +Intercom API uses contacts. Review each legacy user call against the current +API before moving it; do not assume a mechanical method rename is correct. + +## Replace client options and HTTP tracing + +Pass options when constructing the client instead of calling `ic.Option` later: + +```go +client, err := intercom.NewClient("access-token", + intercom.WithHTTPClient(http.DefaultClient), + intercom.WithRetry(intercom.RetryConfig{MaxAttempts: 3}), + intercom.WithResponseHook(func(info intercom.ResponseInfo) { + log.Printf("status=%d request_id=%s remaining=%s", + info.StatusCode, info.RequestID, info.RateLimitRemaining) + }), +) +``` + +`WithResponseHook` is the replacement for ad-hoc transport tracing when the +goal is observability. Keep hooks fast and never log access tokens, request +bodies, or customer data. + +## Paginate conversations safely + +Use an iterator rather than manually retaining a cursor: + +```go +iter := client.Conversations.ListIter(ctx, intercom.CursorPageOptions{PerPage: 25}) +for iter.Next() { + conversation := iter.Conversation() + // Process the item without logging customer data. + _ = conversation +} +if err := iter.Err(); err != nil { + return err +} +``` + +The iterator stops with `intercom.ErrPaginationStalled` if Intercom repeats a +cursor, preventing an accidental infinite loop. + +## Handle errors and webhooks + +Use `intercom.IsNotFound(err)` for missing resources and `errors.As` with +`*intercom.ErrorResponse` when an application needs response status, headers, +or Intercom's request ID. For webhooks, use `ParseAndVerifyWebhook` so the +signature is checked against the exact request bytes before parsing. + +See the runnable [webhook](../examples/verify_webhook), +[conversation](../examples/list_conversations), and +[observability](../examples/observe_rate_limits) examples. Consult the +[production guide](production.md) before enabling retries for an existing +write path. diff --git a/examples/list_conversations/main.go b/examples/list_conversations/main.go new file mode 100644 index 0000000..c8ea6ac --- /dev/null +++ b/examples/list_conversations/main.go @@ -0,0 +1,33 @@ +// list_conversations lists conversation pages lazily without retaining cursors. +package main + +import ( + "context" + "fmt" + "log" + "time" + + intercom "github.com/uffejaeger/intercom-go" +) + +func main() { + client, err := intercom.NewClientFromEnv() + if err != nil { + log.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + iter := client.Conversations.ListIter(ctx, intercom.CursorPageOptions{PerPage: 25}) + count := 0 + for iter.Next() { + // Process the conversation here. Avoid logging customer data by default. + count++ + } + if err := iter.Err(); err != nil { + log.Fatal(err) + } + + fmt.Printf("processed %d conversations\n", count) +} diff --git a/examples/observe_rate_limits/main.go b/examples/observe_rate_limits/main.go new file mode 100644 index 0000000..cb9ed1a --- /dev/null +++ b/examples/observe_rate_limits/main.go @@ -0,0 +1,32 @@ +// observe_rate_limits records request metadata without logging credentials or payloads. +package main + +import ( + "context" + "log" + + intercom "github.com/uffejaeger/intercom-go" +) + +func main() { + client, err := intercom.NewClientFromEnv( + intercom.WithResponseHook(func(info intercom.ResponseInfo) { + log.Printf("attempt=%d/%d status=%d request_id=%s remaining=%s reset=%s duration=%s", + info.Attempt, + info.MaxAttempts, + info.StatusCode, + info.RequestID, + info.RateLimitRemaining, + info.RateLimitReset, + info.Duration, + ) + }), + ) + if err != nil { + log.Fatal(err) + } + + if _, err := client.Admins.Me(context.Background()); err != nil { + log.Fatal(err) + } +} diff --git a/examples/verify_webhook/main.go b/examples/verify_webhook/main.go new file mode 100644 index 0000000..28709b9 --- /dev/null +++ b/examples/verify_webhook/main.go @@ -0,0 +1,43 @@ +// verify_webhook runs a minimal HTTP endpoint that verifies Intercom webhooks +// before processing their payloads. +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + + intercom "github.com/uffejaeger/intercom-go" +) + +func main() { + clientSecret := os.Getenv("INTERCOM_CLIENT_SECRET") + if clientSecret == "" { + log.Fatal("INTERCOM_CLIENT_SECRET is required") + } + + http.HandleFunc("/webhooks/intercom", func(w http.ResponseWriter, r *http.Request) { + event, err := intercom.ParseAndVerifyWebhook(r, clientSecret, 0) + if err != nil { + switch { + case errors.Is(err, intercom.ErrWebhookPayloadTooLarge): + http.Error(w, "payload too large", http.StatusRequestEntityTooLarge) + case errors.Is(err, intercom.ErrWebhookSignatureMissing), + errors.Is(err, intercom.ErrWebhookSignatureInvalid), + errors.Is(err, intercom.ErrWebhookSignatureUnsupported): + http.Error(w, "invalid webhook signature", http.StatusUnauthorized) + default: + http.Error(w, "invalid webhook", http.StatusBadRequest) + } + return + } + + // Decode event.Data.Item only for topics this endpoint handles. + log.Printf("verified webhook topic=%s id=%s", event.Topic, event.ID) + fmt.Fprintln(w, "ok") + }) + + log.Fatal(http.ListenAndServe(":8080", nil)) +}