bzapper 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c424228e334385c5d1e747d6951f6c76df26255765678d7748430642cfcb4c1
4
- data.tar.gz: 3b27b1c1a0bffffa53937fe6d0f19e8486afa18ecdbe8dd08044160103413ae5
3
+ metadata.gz: e7438b5b975605d626f5922c69496d7adef7b4e25d792018fee18755b79d4258
4
+ data.tar.gz: 21d526017685778f6c373f177c74815d2de996414e7653c82ed3cda77edd7430
5
5
  SHA512:
6
- metadata.gz: c071f315d70aefbaf8eb98933d36c42c2f45d2fee7b3cbdd66db0d7a141b6e978e7d65f7337d6ef2b5cdb744569fd47efe3c91cba05b8525eabf9f9234908025
7
- data.tar.gz: b7e9fed41f47242ba1c0fd659c5dc0255ae3ac48a6db969be4ed38806c047ebb72b4f63800c5224f1a389d2b9e32efd374e73f9341399c0326f0c68c088c35b0
6
+ metadata.gz: 2606d09adbe38226e21ff480188aa23dc909c04c35fc5541c3508f27dad4b5e627f1bf7614ba34b25e95f7f5c02b0148ce853009df6e07912ace6cabeb19fe12
7
+ data.tar.gz: db997c1ca043ac5b3172cbd1fe9af114eb2cfc2723f3eff54e59c2581390f84dc4ba5bcc2fcc6cac81dfbfb0e544be0627b63cc9ee3b8a4495a077e0147f178e
data/README.md CHANGED
@@ -10,13 +10,13 @@ Zero dependências de runtime (só biblioteca padrão: `net/http`, `json`, `open
10
10
  ## Instalação
11
11
 
12
12
  ```bash
13
- gem install bzapper -v 0.7.0
13
+ gem install bzapper -v 0.8.0
14
14
  ```
15
15
 
16
16
  Ou no `Gemfile` — **fixe a versão exata** (cada release declara se muda a superfície pública):
17
17
 
18
18
  ```ruby
19
- gem "bzapper", "0.7.0"
19
+ gem "bzapper", "0.8.0"
20
20
  ```
21
21
 
22
22
  ## Hello world
@@ -53,6 +53,27 @@ client = Bzapper::Client.new(
53
53
  Construir o cliente não faz nenhuma chamada de rede. Um cliente pode ser compartilhado entre
54
54
  threads.
55
55
 
56
+ ### Trocar a chave sem derrubar a integração (rotação)
57
+
58
+ `rotate_my_key` (admin) cria uma chave **nova** herdando papel, escopos, projeto e nome da
59
+ antiga, e mantém a antiga funcionando por um período de graça — dá tempo de fazer o deploy sem
60
+ janela de erro. A chave crua aparece **uma única vez**, no retorno. Depois do prazo a antiga
61
+ responde `401 key_expired`; `revoke_in_seconds: 0` revoga na hora (padrão 86400 s, máximo 30
62
+ dias). Chave de parceiro (bZapper Connect) roda pelo `PartnerClient#rotate_partner_connection_key`.
63
+
64
+ ```ruby
65
+ rotated = client.accounts.rotate_my_key(key_id, revoke_in_seconds: 3600) # 1 h de graça
66
+
67
+ rotated["api_key"] # a chave NOVA, crua — guarde agora, não volta
68
+ rotated["key"]["id"] # metadados da nova
69
+ rotated["previous_key"]["expires_at"] # quando a antiga para de funcionar (nil se revogada já)
70
+ rotated["old_key_expires_at"] # o mesmo instante, no topo do retorno
71
+ rotated["previous_key"]["rotated_to"] # id da chave que substituiu a antiga
72
+ ```
73
+
74
+ Uma chave já revogada ou já vencida responde `409` (`key_already_revoked` /
75
+ `key_already_expired`).
76
+
56
77
  ## Como os métodos funcionam
57
78
 
58
79
  Os métodos ficam em **recursos**, um por área da API, e se chamam como o `operationId` da
@@ -66,13 +87,13 @@ spec OpenAPI em snake_case (`sendText` → `send_text`):
66
87
  | `client.groups` | `list_groups`, `create_group`, `get_group`, `update_group`, `update_group_participants`, `group_invite_link`, `join_group`, `preview_group_invite`, `leave_group`, pedidos de entrada |
67
88
  | `client.conversations` | `list_conversations`, `conversation_history` |
68
89
  | `client.advanced` | editar/apagar/encaminhar mensagem, perfil, privacidade, arquivar/fixar/ler/silenciar chat, etiquetas, bloqueio, chamadas |
69
- | `client.contacts` | CRM (`list_contacts`, `create_contact`, `update_contact`…), tags, grupos de contatos, opt-in/out, supressões, `contacts_check` |
90
+ | `client.contacts` | CRM (`list_contacts`, `create_contact`, `update_contact`…), `import_contacts` (lote), `export_contacts` (CSV), tags, grupos de contatos, opt-in/out, supressões, `contacts_check` |
70
91
  | `client.campaigns` | `create_campaign`, `estimate_campaign`, destinatários, `dry_run_campaign`, `start/pause/resume/cancel_campaign` |
71
92
  | `client.pools` | pools de números |
72
93
  | `client.webhooks` | `create_webhook`, `update_webhook`, `test_webhook`, `list_webhook_deliveries`… |
73
94
  | `client.advisories` | avisos "atualize sua integração" |
74
95
  | `client.usage` / `client.billing` | consumo; plano, assinatura, add-ons, faturas, preços |
75
- | `client.accounts` | perfil, chaves de API, marca, projetos, usuários |
96
+ | `client.accounts` | perfil, chaves de API (`rotate_my_key`), marca, projetos, usuários |
76
97
  | `client.connect` | apps parceiros conectados à sua conta |
77
98
  | `client.system` | `get_health` |
78
99
 
@@ -88,6 +109,8 @@ Convenções (iguais em todos os métodos):
88
109
  vêm como `{"data" => [...], ...}` (a SDK não desembrulha, para não perder paginação e
89
110
  metadados). Campos novos da API aparecem sem quebrar nada. `204` → `nil`.
90
111
  * Parâmetro de caminho vazio, `"."` ou `".."` → `ArgumentError` antes de qualquer requisição.
112
+ * Uma exceção ao retorno: `client.contacts.export_contacts` devolve **`String`** (o CSV cru),
113
+ porque a rota responde `text/csv` e não JSON.
91
114
 
92
115
  ## Mensagens
93
116
 
@@ -162,6 +185,48 @@ client.contacts.contacts_check(instance_id: "…", phones: ["+5511999999999"]) #
162
185
 
163
186
  O vínculo contato ↔ projeto/número é mantido **automaticamente** pela API.
164
187
 
188
+ ### Importar em lote
189
+
190
+ Até **1000** contatos por chamada, upsert por telefone: o novo entra como
191
+ `pending_validation` (precisa de opt-in antes de campanha), o que já existe só recebe os campos
192
+ informados — valor em branco não apaga o que está lá. Linha ruim vai para `errors` e **não**
193
+ derruba o resto; contato suprimido/opt-out/bloqueado aparece em `skipped_rows` e nunca
194
+ ressuscita. Tags e grupos são criados na hora. `dry_run: true` valida tudo e não grava nada.
195
+
196
+ ```ruby
197
+ result = client.contacts.import_contacts(
198
+ contacts: [
199
+ { phone: "+5511999999999", name: "Ana", email: "ana@example.com", tags: %w[vip] },
200
+ { phone: "+5511888888888", name: "Bruno", document: "12345678900", document_type: "cpf",
201
+ address: { city: "São Paulo", state: "SP", country: "BR" }, groups: %w[clientes] }
202
+ ],
203
+ dry_run: true # ensaio: nada é gravado
204
+ )
205
+ result["created"] # => 2
206
+ result["errors"].each { |row| warn "linha #{row['index']} (#{row['phone']}): #{row['reason']}" }
207
+ ```
208
+
209
+ ### Exportar em CSV
210
+
211
+ `export_contacts` aceita os **mesmos filtros** do `list_contacts` (menos `offset`; use `limit`
212
+ para limitar as linhas) e devolve o **texto do CSV** — uma `String` UTF-8, não JSON. Colunas:
213
+ `phone,name,email,status,source,tags,groups,created_at,last_activity_at`, com tags e grupos
214
+ unidos por `;` e instantes em RFC 3339 UTC.
215
+
216
+ ```ruby
217
+ require "csv"
218
+
219
+ csv = client.contacts.export_contacts(status: "active", tags: %w[vip], has_email: true,
220
+ created_after: Time.utc(2026, 1, 1), limit: 50_000)
221
+
222
+ File.write("contatos.csv", csv) # gravar como veio
223
+ CSV.parse(csv, headers: true) { |row| puts row["phone"] } # ou percorrer linha a linha
224
+ ```
225
+
226
+ Vírgulas e aspas dentro dos campos vêm escapadas pelo servidor — não monte o CSV de novo,
227
+ entregue o texto ao parser. Para bases muito grandes, exporte em fatias com os filtros
228
+ (`created_after`/`created_before`, `limit`) em vez de puxar tudo de uma vez.
229
+
165
230
  ## Campanhas
166
231
 
167
232
  ```ruby
data/lib/bzapper/codec.rb CHANGED
@@ -99,9 +99,15 @@ module Bzapper
99
99
  end
100
100
  end
101
101
 
102
+ # Corpo cru → texto UTF-8 (bytes inválidos trocados). É o que as rotas de texto
103
+ # (`text/csv`, ex.: `exportContacts`) devolvem, sem passar por JSON.
104
+ def utf8(raw)
105
+ raw.to_s.dup.force_encoding(::Encoding::UTF_8).scrub
106
+ end
107
+
102
108
  # Texto UTF-8 (bytes inválidos trocados) → `[json_ou_nil, texto, é_json?]`.
103
109
  def decode_json(raw)
104
- text = raw.to_s.dup.force_encoding(::Encoding::UTF_8).scrub
110
+ text = utf8(raw)
105
111
  return [nil, text, false] if text.strip.empty?
106
112
 
107
113
  [JSON.parse(text), text, true]
@@ -34,7 +34,7 @@ module Bzapper
34
34
 
35
35
  # Revoke a tenant API key. Admin only. `DELETE /keys/{id}`
36
36
  #
37
- # @param id [String] Instance ID (UUID).
37
+ # @param id [String] API key ID (UUID).
38
38
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
39
39
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
40
40
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -47,6 +47,33 @@ module Bzapper
47
47
  timeout: timeout)
48
48
  end
49
49
 
50
+ # Rotate a tenant API key. Admin only. `POST /keys/{id}/rotate`
51
+ #
52
+ # Creates a NEW key inheriting the old one's role, scopes, project and name, and keeps the OLD
53
+ # key working for a grace period so a running integration does not break mid-deploy. The raw key
54
+ # is shown ONCE. After the deadline the old key answers `401 key_expired`. `revoke_in_seconds:
55
+ # 0` revokes it immediately (default 86400, max 30 days). Partner keys (bZapper Connect) rotate
56
+ # through `/partner/connections/{id}/rotate-key`.
57
+ #
58
+ # @param id [String] API key ID (UUID).
59
+ # @param revoke_in_seconds [Integer, nil] (corpo) Grace period for the OLD key. 0 revokes it
60
+ # immediately.
61
+ # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
62
+ # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
63
+ # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
64
+ # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
65
+ # @raise [ArgumentError] parâmetro de caminho vazio, "." ou "..".
66
+ def rotate_my_key(id, revoke_in_seconds: UNSET, idempotency_key: nil, timeout: nil)
67
+ payload = compact(
68
+ "revoke_in_seconds" => revoke_in_seconds
69
+ )
70
+ request("POST",
71
+ "/keys/#{segment(id, "id")}/rotate",
72
+ body: payload,
73
+ idempotency_key: idempotency_key,
74
+ timeout: timeout)
75
+ end
76
+
50
77
  # Authenticated identity (+ profile when it's a user session). `GET /me`
51
78
  #
52
79
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -198,7 +225,7 @@ module Bzapper
198
225
  #
199
226
  # `api_mode` is immutable and is ignored here.
200
227
  #
201
- # @param id [String] Resource ID (UUID).
228
+ # @param id [String] Project ID (UUID).
202
229
  # @param name [String] (corpo)
203
230
  # @param logo_url [String, nil] (corpo)
204
231
  # @param color [String, nil] (corpo)
@@ -223,7 +250,7 @@ module Bzapper
223
250
 
224
251
  # Delete a project (admin). `DELETE /projects/{id}`
225
252
  #
226
- # @param id [String] Resource ID (UUID).
253
+ # @param id [String] Project ID (UUID).
227
254
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
228
255
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
229
256
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -238,7 +265,7 @@ module Bzapper
238
265
 
239
266
  # Identity of the numbers of a specific project. `GET /projects/{id}/brand`
240
267
  #
241
- # @param id [String] Resource ID (UUID).
268
+ # @param id [String] Project ID (UUID).
242
269
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
243
270
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
244
271
  # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
@@ -249,7 +276,7 @@ module Bzapper
249
276
 
250
277
  # Save the identity of a specific project's numbers (admin). `PUT /projects/{id}/brand`
251
278
  #
252
- # @param id [String] Resource ID (UUID).
279
+ # @param id [String] Project ID (UUID).
253
280
  # @param about [String, nil] (corpo) "About"/status — applied to all numbers.
254
281
  # @param display_name [String, nil] (corpo) Business name (kit).
255
282
  # @param logo_url [String, nil] (corpo) Logo URL (kit).
@@ -286,7 +313,7 @@ module Bzapper
286
313
  # Upload the project logo (multipart, PNG/JPEG/WebP up to 5 MB) — admin. `POST
287
314
  # /projects/{id}/logo`
288
315
  #
289
- # @param id [String] Resource ID (UUID).
316
+ # @param id [String] Project ID (UUID).
290
317
  # @param file [String, IO, Pathname] conteúdo do arquivo (bytes), um IO ou o caminho no disco.
291
318
  # @param filename [String, nil] nome do arquivo (padrão: o do caminho, senão "file").
292
319
  # @param content_type [String, nil] tipo MIME (padrão: application/octet-stream).
@@ -337,7 +364,7 @@ module Bzapper
337
364
  #
338
365
  # Demoting the last administrator of the account is refused (409 last_admin).
339
366
  #
340
- # @param id [String] Resource ID (UUID).
367
+ # @param id [String] Account user ID (UUID).
341
368
  # @param role [String] (corpo) agent = member (no billing)
342
369
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
343
370
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -359,7 +386,7 @@ module Bzapper
359
386
  #
360
387
  # You cannot remove yourself (409 self_remove) nor the last administrator (409 last_admin).
361
388
  #
362
- # @param id [String] Resource ID (UUID).
389
+ # @param id [String] Account user ID (UUID).
363
390
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
364
391
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
365
392
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -8,7 +8,7 @@ module Bzapper
8
8
  class Advanced < Base
9
9
  # Edit the text of a sent message. `PATCH /messages/{id}`
10
10
  #
11
- # @param id [String] Instance ID (UUID).
11
+ # @param id [String] bZapper message ID (UUID) — the `id` returned when the message was queued.
12
12
  # @param text [String] (corpo)
13
13
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
14
14
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -28,7 +28,7 @@ module Bzapper
28
28
 
29
29
  # Revoke a message (delete for everyone). `DELETE /messages/{id}`
30
30
  #
31
- # @param id [String] Instance ID (UUID).
31
+ # @param id [String] bZapper message ID (UUID) — the `id` returned when the message was queued.
32
32
  # @param for_everyone [Boolean, nil] (query)
33
33
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
34
34
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -121,7 +121,7 @@ module Bzapper
121
121
 
122
122
  # Archive/unarchive a chat. `POST /chats/{jid}/archive`
123
123
  #
124
- # @param jid [String] Group JID (…@g.us).
124
+ # @param jid [String] Chat JID — contact (…@s.whatsapp.net / …@lid) or group (…@g.us).
125
125
  # @param instance_id [String] (corpo)
126
126
  # @param on [Boolean] (corpo)
127
127
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
@@ -143,7 +143,7 @@ module Bzapper
143
143
 
144
144
  # Pin/unpin a chat. `POST /chats/{jid}/pin`
145
145
  #
146
- # @param jid [String] Group JID (…@g.us).
146
+ # @param jid [String] Chat JID — contact (…@s.whatsapp.net / …@lid) or group (…@g.us).
147
147
  # @param instance_id [String] (corpo)
148
148
  # @param on [Boolean] (corpo)
149
149
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
@@ -165,7 +165,7 @@ module Bzapper
165
165
 
166
166
  # Mark a chat read/unread. `POST /chats/{jid}/read`
167
167
  #
168
- # @param jid [String] Group JID (…@g.us).
168
+ # @param jid [String] Chat JID — contact (…@s.whatsapp.net / …@lid) or group (…@g.us).
169
169
  # @param instance_id [String] (corpo)
170
170
  # @param on [Boolean] (corpo)
171
171
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
@@ -187,7 +187,7 @@ module Bzapper
187
187
 
188
188
  # Mute/unmute a chat. `POST /chats/{jid}/mute`
189
189
  #
190
- # @param jid [String] Group JID (…@g.us).
190
+ # @param jid [String] Chat JID — contact (…@s.whatsapp.net / …@lid) or group (…@g.us).
191
191
  # @param instance_id [String] (corpo)
192
192
  # @param on [Boolean] (corpo)
193
193
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
@@ -209,7 +209,7 @@ module Bzapper
209
209
 
210
210
  # Apply/remove a label on a chat (experimental). `POST /chats/{jid}/labels`
211
211
  #
212
- # @param jid [String] Group JID (…@g.us).
212
+ # @param jid [String] Chat JID — contact (…@s.whatsapp.net / …@lid) or group (…@g.us).
213
213
  # @param instance_id [String] (corpo)
214
214
  # @param label_id [String] (corpo)
215
215
  # @param apply [Boolean, nil] (corpo) true = apply, false = remove.
@@ -289,7 +289,7 @@ module Bzapper
289
289
 
290
290
  # Block a contact. `POST /contacts/{jid}/block`
291
291
  #
292
- # @param jid [String] Group JID (…@g.us).
292
+ # @param jid [String] Contact JID (…@s.whatsapp.net) to block/unblock.
293
293
  # @param instance_id [String] (corpo)
294
294
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
295
295
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -309,7 +309,7 @@ module Bzapper
309
309
 
310
310
  # Unblock a contact. `POST /contacts/{jid}/unblock`
311
311
  #
312
- # @param jid [String] Group JID (…@g.us).
312
+ # @param jid [String] Contact JID (…@s.whatsapp.net) to block/unblock.
313
313
  # @param instance_id [String] (corpo)
314
314
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
315
315
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -22,7 +22,7 @@ module Bzapper
22
22
  #
23
23
  # Marks the advisory as handled; it stops being returned by `GET /advisories`.
24
24
  #
25
- # @param id [String] parâmetro de caminho
25
+ # @param id [String] Advisory ID.
26
26
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
27
27
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
28
28
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -27,6 +27,12 @@ module Bzapper
27
27
  idempotency_key: idempotency_key, timeout: timeout)
28
28
  end
29
29
 
30
+ # Rotas que respondem TEXTO (`text/csv`, ex.: `exportContacts`): devolve o corpo cru,
31
+ # sem JSON. Mesmos cabeçalhos, novas tentativas e erros do {#request}.
32
+ def request_text(method, path, query: nil, timeout: nil, accept: "text/csv")
33
+ @transport.request(method, path, query: query, timeout: timeout, accept: accept, as_text: true)
34
+ end
35
+
30
36
  def segment(value, name)
31
37
  Codec.path_segment(value, name)
32
38
  end
@@ -107,7 +107,7 @@ module Bzapper
107
107
 
108
108
  # Get a campaign with stats and variations. `GET /campaigns/{id}`
109
109
  #
110
- # @param id [String] parâmetro de caminho
110
+ # @param id [String] Campaign ID (UUID).
111
111
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
112
112
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
113
113
  # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
@@ -121,7 +121,7 @@ module Bzapper
121
121
  # Updates name, pacing, schedule and (if sent) replaces the variations. Only allowed while
122
122
  # draft/scheduled — 409 once started.
123
123
  #
124
- # @param id [String] parâmetro de caminho
124
+ # @param id [String] Campaign ID (UUID).
125
125
  # @param name [String, nil] (corpo)
126
126
  # @param pacing_profile [String, nil] (corpo)
127
127
  # @param start_at [Time, String, nil] (corpo) Future start = scheduled campaign.
@@ -148,7 +148,7 @@ module Bzapper
148
148
 
149
149
  # List recipients with per-contact delivery. `GET /campaigns/{id}/recipients`
150
150
  #
151
- # @param id [String] parâmetro de caminho
151
+ # @param id [String] Campaign ID (UUID).
152
152
  # @param limit [Integer, nil] (query)
153
153
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
154
154
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -163,7 +163,7 @@ module Bzapper
163
163
 
164
164
  # Add (or replace) recipients. `POST /campaigns/{id}/recipients`
165
165
  #
166
- # @param id [String] parâmetro de caminho
166
+ # @param id [String] Campaign ID (UUID).
167
167
  # @param recipients [Array<Hash>, nil] (corpo)
168
168
  # @param contacts [Hash, nil] (corpo) Map of phone → payload, e.g. {"+5551999198087": {"name":
169
169
  # "Vinicius"}}.
@@ -196,7 +196,7 @@ module Bzapper
196
196
 
197
197
  # Start (or schedule) a campaign. `POST /campaigns/{id}/start`
198
198
  #
199
- # @param id [String] parâmetro de caminho
199
+ # @param id [String] Campaign ID (UUID).
200
200
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
201
201
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
202
202
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -211,7 +211,7 @@ module Bzapper
211
211
 
212
212
  # Pause a campaign. `POST /campaigns/{id}/pause`
213
213
  #
214
- # @param id [String] parâmetro de caminho
214
+ # @param id [String] Campaign ID (UUID).
215
215
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
216
216
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
217
217
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -226,7 +226,7 @@ module Bzapper
226
226
 
227
227
  # Resume a campaign. `POST /campaigns/{id}/resume`
228
228
  #
229
- # @param id [String] parâmetro de caminho
229
+ # @param id [String] Campaign ID (UUID).
230
230
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
231
231
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
232
232
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -241,7 +241,7 @@ module Bzapper
241
241
 
242
242
  # Cancel a campaign. `POST /campaigns/{id}/cancel`
243
243
  #
244
- # @param id [String] parâmetro de caminho
244
+ # @param id [String] Campaign ID (UUID).
245
245
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
246
246
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
247
247
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -256,7 +256,7 @@ module Bzapper
256
256
 
257
257
  # Simulate a campaign without sending. `POST /campaigns/{id}/dry-run`
258
258
  #
259
- # @param id [String] parâmetro de caminho
259
+ # @param id [String] Campaign ID (UUID).
260
260
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
261
261
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
262
262
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -18,7 +18,7 @@ module Bzapper
18
18
  # Disconnect a partner app (admin). The partner's key stops working immediately. `DELETE
19
19
  # /me/connections/{id}`
20
20
  #
21
- # @param id [String] Instance ID (UUID).
21
+ # @param id [String] Connected partner app (connection) ID (UUID).
22
22
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
23
23
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
24
24
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -105,6 +105,33 @@ module Bzapper
105
105
  timeout: timeout)
106
106
  end
107
107
 
108
+ # Import contacts in bulk. `POST /contacts/import`
109
+ #
110
+ # Upserts up to 1000 contacts by phone in one call. A new contact is created with `source:
111
+ # import` and `status: pending_validation` (it needs opt-in before a campaign). An existing one
112
+ # has only its informed fields updated — a blank value never erases what is there. A
113
+ # suppressed/opted-out/blocked contact is reported in `skipped_rows` and never resurrected. Tags
114
+ # and groups are created on demand. A bad row is reported in `errors` and does NOT fail the rest
115
+ # of the call. `dry_run` validates everything and writes nothing.
116
+ #
117
+ # @param contacts [Array<Hash>] (corpo)
118
+ # @param dry_run [Boolean, nil] (corpo) Validates and reports without writing anything.
119
+ # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
120
+ # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
121
+ # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
122
+ # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
123
+ def import_contacts(contacts:, dry_run: UNSET, idempotency_key: nil, timeout: nil)
124
+ payload = compact(
125
+ "contacts" => contacts,
126
+ "dry_run" => dry_run
127
+ )
128
+ request("POST",
129
+ "/contacts/import",
130
+ body: payload,
131
+ idempotency_key: idempotency_key,
132
+ timeout: timeout)
133
+ end
134
+
108
135
  # Get a contact. `GET /contacts/{id}`
109
136
  #
110
137
  # @param id [String] Contact ID (UUID).
@@ -337,7 +364,7 @@ module Bzapper
337
364
  #
338
365
  # Removes the tag from the dictionary and unlinks it from every contact.
339
366
  #
340
- # @param id [String] Contact ID (UUID).
367
+ # @param id [String] Tag ID (UUID).
341
368
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
342
369
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
343
370
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -389,7 +416,7 @@ module Bzapper
389
416
  #
390
417
  # Removes the contact group from the dictionary and unlinks it from every contact.
391
418
  #
392
- # @param id [String] Contact ID (UUID).
419
+ # @param id [String] Contact group ID (UUID).
393
420
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
394
421
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
395
422
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ESCRITO À MÃO (não sai do script/generate.rb): `exportContacts` responde `text/csv`, e não
4
+ # JSON — está fora dos casos gerados (BRIEF §6, "CSV") e tem teste próprio
5
+ # (test/unit_test.rb, CsvExportTest). O resto do `client.contacts` é gerado
6
+ # (lib/bzapper/resources/contacts.rb).
7
+
8
+ module Bzapper
9
+ module Resources
10
+ class Contacts
11
+ # Export contacts as CSV. `GET /contacts/export`
12
+ #
13
+ # Devolve a base de contatos em **CSV** (`text/csv`, `Content-Disposition: attachment`) —
14
+ # não é JSON. Colunas:
15
+ # `phone,name,email,status,source,tags,groups,created_at,last_activity_at`; tags e grupos
16
+ # vêm unidos por `;` e os instantes em RFC 3339 UTC. Os filtros são os MESMOS do
17
+ # {#list_contacts} (só não há `offset`: use `limit` para limitar as linhas).
18
+ #
19
+ # O retorno é o texto do CSV, como veio do servidor (UTF-8) — passe-o a `CSV.parse` ou
20
+ # grave em disco. Para não carregar tudo na memória, exporte em fatias com os filtros
21
+ # (`created_after`/`created_before`, `limit`).
22
+ #
23
+ # @example Gravar em disco
24
+ # File.write("contatos.csv", client.contacts.export_contacts(status: "active"))
25
+ #
26
+ # @example Ler linha a linha
27
+ # require "csv"
28
+ # CSV.parse(client.contacts.export_contacts(tags: %w[vip]), headers: true) do |row|
29
+ # puts row["phone"]
30
+ # end
31
+ #
32
+ # @param search [String, nil] (query) Filter by name, phone, email or document.
33
+ # @param tags [Array<String>, nil] (query) Tag keys (repeat the param or a CSV list).
34
+ # @param tags_match [String, nil] (query) Match ANY (default) or ALL of `tags`.
35
+ # @param groups [Array<String>, nil] (query) Contact-group keys.
36
+ # @param project_id [String, nil] (query) Scope by project (alternative to the X-Project-Id
37
+ # header).
38
+ # @param instance_id [String, nil] (query) Filter by a number (instance) the contact
39
+ # interacted with.
40
+ # @param status [String, nil] (query)
41
+ # @param city [String, nil] (query)
42
+ # @param state [String, nil] (query)
43
+ # @param country [String, nil] (query)
44
+ # @param zip [String, nil] (query)
45
+ # @param document [String, nil] (query)
46
+ # @param has_email [Boolean, nil] (query) Only contacts that have (true) or lack (false) an
47
+ # email.
48
+ # @param last_activity_after [Time, String, nil] (query) last_message_at ≥ this instant
49
+ # (RFC3339).
50
+ # @param last_activity_before [Time, String, nil] (query) last_message_at ≤ this instant
51
+ # (RFC3339).
52
+ # @param created_after [Time, String, nil] (query) created_at ≥ this instant (RFC3339).
53
+ # @param created_before [Time, String, nil] (query) created_at ≤ this instant (RFC3339).
54
+ # @param sort [String, nil] (query)
55
+ # @param limit [Integer, nil] (query) Cap of exported rows.
56
+ # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
57
+ # @return [String] o CSV cru (UTF-8), com o cabeçalho na primeira linha.
58
+ # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
59
+ def export_contacts(search: nil, tags: nil, tags_match: nil, groups: nil, project_id: nil,
60
+ instance_id: nil, status: nil, city: nil, state: nil, country: nil,
61
+ zip: nil, document: nil, has_email: nil, last_activity_after: nil,
62
+ last_activity_before: nil, created_after: nil, created_before: nil,
63
+ sort: nil, limit: nil, timeout: nil)
64
+ query = {
65
+ "search" => search,
66
+ "tags" => tags,
67
+ "tags_match" => tags_match,
68
+ "groups" => groups,
69
+ "project_id" => project_id,
70
+ "instance_id" => instance_id,
71
+ "status" => status,
72
+ "city" => city,
73
+ "state" => state,
74
+ "country" => country,
75
+ "zip" => zip,
76
+ "document" => document,
77
+ "has_email" => has_email,
78
+ "last_activity_after" => last_activity_after,
79
+ "last_activity_before" => last_activity_before,
80
+ "created_after" => created_after,
81
+ "created_before" => created_before,
82
+ "sort" => sort,
83
+ "limit" => limit
84
+ }
85
+ request_text("GET", "/contacts/export", query: query, timeout: timeout)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -89,7 +89,7 @@ module Bzapper
89
89
 
90
90
  # Get one connection (status, account, numbers). `GET /partner/connections/{id}`
91
91
  #
92
- # @param id [String] Instance ID (UUID).
92
+ # @param id [String] Partner connection ID (UUID).
93
93
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
94
94
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
95
95
  # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
@@ -101,7 +101,7 @@ module Bzapper
101
101
  # End a connection (revokes the key; does NOT cancel the customer's plan). `DELETE
102
102
  # /partner/connections/{id}`
103
103
  #
104
- # @param id [String] Instance ID (UUID).
104
+ # @param id [String] Partner connection ID (UUID).
105
105
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
106
106
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
107
107
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -117,7 +117,7 @@ module Bzapper
117
117
  # Issue a new API key for a completed connection (the previous key stops working). `POST
118
118
  # /partner/connections/{id}/rotate-key`
119
119
  #
120
- # @param id [String] Instance ID (UUID).
120
+ # @param id [String] Partner connection ID (UUID).
121
121
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
122
122
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
123
123
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -36,7 +36,7 @@ module Bzapper
36
36
 
37
37
  # Get a pool (with members). `GET /pools/{id}`
38
38
  #
39
- # @param id [String] Instance ID (UUID).
39
+ # @param id [String] Pool ID (UUID).
40
40
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
41
41
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
42
42
  # @raise [Bzapper::Error] resposta fora de 2xx ou falha de rede.
@@ -47,7 +47,7 @@ module Bzapper
47
47
 
48
48
  # Add a number to the pool. `POST /pools/{id}/numbers`
49
49
  #
50
- # @param id [String] Instance ID (UUID).
50
+ # @param id [String] Pool ID (UUID).
51
51
  # @param instance_id [String] (corpo)
52
52
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
53
53
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -21,7 +21,7 @@ module Bzapper
21
21
 
22
22
  # Cancel a pending scheduled send. `DELETE /messages/scheduled/{id}`
23
23
  #
24
- # @param id [String] parâmetro de caminho
24
+ # @param id [String] Scheduled message ID (UUID).
25
25
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
26
26
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
27
27
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -47,7 +47,7 @@ module Bzapper
47
47
  # = true. With `secret: "regenerate"` a new secret is generated and returned ONCE in the
48
48
  # response; any other non-empty `secret` replaces it.
49
49
  #
50
- # @param id [String] Resource ID (UUID).
50
+ # @param id [String] Webhook ID (UUID).
51
51
  # @param url [String] (corpo)
52
52
  # @param secret [String, nil] (corpo) Empty = keep. "regenerate" = rotate (returned once).
53
53
  # @param event_types [Array<String>, nil] (corpo) Empty = all events.
@@ -76,7 +76,7 @@ module Bzapper
76
76
 
77
77
  # Remove a webhook. `DELETE /webhooks/{id}`
78
78
  #
79
- # @param id [String] Instance ID (UUID).
79
+ # @param id [String] Webhook ID (UUID).
80
80
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
81
81
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
82
82
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -91,7 +91,7 @@ module Bzapper
91
91
 
92
92
  # Send a signed sample event to this webhook and report the result. `POST /webhooks/{id}/test`
93
93
  #
94
- # @param id [String] Resource ID (UUID).
94
+ # @param id [String] Webhook ID (UUID).
95
95
  # @param event_type [String, nil] (corpo) Event type of the sample (default a generic one).
96
96
  # @param idempotency_key [String, nil] chave de idempotência (senão a SDK gera uma).
97
97
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
@@ -111,7 +111,7 @@ module Bzapper
111
111
 
112
112
  # Latest deliveries of a webhook. `GET /webhooks/{id}/deliveries`
113
113
  #
114
- # @param id [String] Resource ID (UUID).
114
+ # @param id [String] Webhook ID (UUID).
115
115
  # @param limit [Integer, nil] (query) 1–200 (default 50).
116
116
  # @param timeout [Numeric, nil] segundos por tentativa (padrão: o do cliente).
117
117
  # @return [Hash, Array, nil] o JSON da resposta, inteiro (nil em 204).
@@ -96,8 +96,12 @@ module Bzapper
96
96
  # Executa a chamada e devolve o JSON decodificado da resposta (`nil` sem corpo).
97
97
  #
98
98
  # `body: nil` significa SEM corpo JSON; `multipart:` é um {Upload}.
99
+ #
100
+ # `accept:` troca o `Accept` (padrão `application/json`) e `as_text: true` devolve o corpo
101
+ # CRU em texto, sem passar por JSON — é o caminho das rotas `text/csv` (`exportContacts`).
99
102
  # @raise [Bzapper::Error]
100
- def request(method, path, query: nil, body: nil, multipart: nil, idempotency_key: nil, timeout: nil)
103
+ def request(method, path, query: nil, body: nil, multipart: nil, idempotency_key: nil, timeout: nil,
104
+ accept: nil, as_text: false)
101
105
  method = method.to_s.upcase
102
106
  per_try = timeout.nil? ? @timeout : timeout
103
107
  unless per_try.is_a?(Numeric) && per_try.positive?
@@ -108,7 +112,7 @@ module Bzapper
108
112
  request_id = SecureRandom.uuid.delete("-")
109
113
  headers = {
110
114
  "Authorization" => "Bearer #{@api_key}",
111
- "Accept" => "application/json",
115
+ "Accept" => accept || "application/json",
112
116
  "X-Bzapper-Client" => CLIENT_ID,
113
117
  "User-Agent" => CLIENT_ID,
114
118
  # Mesmo id em todas as tentativas desta chamada: é como o suporte correlaciona.
@@ -146,7 +150,10 @@ module Bzapper
146
150
  )
147
151
  end
148
152
 
149
- return decode(status, resp_headers, raw, request_id) if status.between?(200, 299)
153
+ if status.between?(200, 299)
154
+ # Texto: o corpo volta como veio (o `INVALID_RESPONSE` do §4 não vale aqui).
155
+ return as_text ? Codec.utf8(raw) : decode(status, resp_headers, raw, request_id)
156
+ end
150
157
 
151
158
  if RETRY_STATUSES.include?(status) && attempt < @max_retries
152
159
  @sleeper.call(retry_delay(attempt, resp_headers["retry-after"]))
@@ -3,5 +3,5 @@
3
3
  module Bzapper
4
4
  # Versão da gem. O `scripts/release-sdks.sh` do monorepo bumpa a linha abaixo por regex
5
5
  # (ancorada no início da linha) e o `bzapper.gemspec` lê daqui — não mude o formato dela.
6
- VERSION = "0.7.0"
6
+ VERSION = "0.8.0"
7
7
  end
data/lib/bzapper.rb CHANGED
@@ -26,5 +26,7 @@ require_relative "bzapper/codec"
26
26
  require_relative "bzapper/upload"
27
27
  require_relative "bzapper/transport"
28
28
  require_relative "bzapper/resources"
29
+ # Escrito à mão: `exportContacts` responde CSV, fora dos métodos gerados.
30
+ require_relative "bzapper/resources/contacts_export"
29
31
  require_relative "bzapper/client"
30
32
  require_relative "bzapper/webhook"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bzapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Berni Software
@@ -31,6 +31,7 @@ files:
31
31
  - lib/bzapper/resources/campaigns.rb
32
32
  - lib/bzapper/resources/connect.rb
33
33
  - lib/bzapper/resources/contacts.rb
34
+ - lib/bzapper/resources/contacts_export.rb
34
35
  - lib/bzapper/resources/conversations.rb
35
36
  - lib/bzapper/resources/groups.rb
36
37
  - lib/bzapper/resources/instances.rb