blueticks 2.0.0 → 2.1.1

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: 62b2a3ae7e59ee684634e2e2e26883fa47db18da62aa74eda711efd088d4e868
4
- data.tar.gz: 392f498378faa47fa4be6f73fbefad15a5642ab16718ec2fdd9787a8aa5951a4
3
+ metadata.gz: 80ac68082d0098a0ceae9ace729c7d019bbeca7f4714b5f8f11c4846af84ac04
4
+ data.tar.gz: f8d39230912328f38e8e9dda9034cc5d0d9192b47f7ed7857063d2a173fd32ec
5
5
  SHA512:
6
- metadata.gz: e3a873bb77d76820cf1946483f11eabf2c53169c3930f1578fbbaa86679193e6428071aed414d4d9c41f5f99beb3dfd7bac411e9db84ebcad3e66b364e5f9def
7
- data.tar.gz: 681cf759086355b74cdba8fec69cf515b62705b7a8a0039916c25a267dc34928d2f24c2a62dc948e3c04ea909c45f618cb0803677cc3efe0a56a2568447576d4
6
+ metadata.gz: fc539e01c948edc214d396f4bb8aafb09eacc7a450a370c5e4675a23021c3705b829b467332c19a1e2c5b4c3ab53d88af64930e122e205b980f1a3354ca4cfb0
7
+ data.tar.gz: ebe45c16ef9311b23600ca225dcaffb1924e64f1df5681966383053a8be2d50d05791abc199db390de7fc30404d87a386892b0086631ecf269187e1ddef21736
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to the Blueticks Ruby SDK are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 2.1.1 — 2026-11-07
8
+
9
+ ### Fixed
10
+ - Publish workflow now runs on a GitHub-hosted runner (`ubuntu-24.04`); the 2.1.0 publish failed on the self-hosted runner, so 2.1.1 is the first published release with `groups.get_picture` / `groups.remove_picture`.
11
+
12
+ ## 2.1.0 — 2026-11-07
13
+
14
+ ### Added
15
+
16
+ - Group picture: `groups.get_picture` (get the current picture URL) and `groups.remove_picture` (remove the picture; Gateway/Baileys engine only — Chrome-extension-only accounts get `501 unsupported_command`). `groups.set_picture` already existed.
17
+
7
18
  ## 1.2.0 — 2026-05-22
8
19
 
9
20
  OpenAPI parity pass. The SDK now matches `backend/openapi.json`
@@ -4,14 +4,18 @@ require_relative "errors"
4
4
  require_relative "transport"
5
5
  require_relative "resources/account"
6
6
  require_relative "resources/audiences"
7
+ require_relative "resources/boards"
7
8
  require_relative "resources/campaigns"
8
9
  require_relative "resources/chats"
9
10
  require_relative "resources/contacts"
10
11
  require_relative "resources/engines"
11
12
  require_relative "resources/groups"
13
+ require_relative "resources/identity"
14
+ require_relative "resources/latest_chats"
12
15
  require_relative "resources/messages"
13
16
  require_relative "resources/newsletters"
14
17
  require_relative "resources/ping"
18
+ require_relative "resources/realtime"
15
19
  require_relative "resources/scheduled_messages"
16
20
  require_relative "resources/suno"
17
21
  require_relative "resources/utils"
@@ -100,6 +104,22 @@ module Blueticks
100
104
  @messages ||= Resources::MessagesResource.new(self)
101
105
  end
102
106
 
107
+ def boards
108
+ @boards ||= Resources::BoardsResource.new(self)
109
+ end
110
+
111
+ def identity
112
+ @identity ||= Resources::IdentityResource.new(self)
113
+ end
114
+
115
+ def latest_chats
116
+ @latest_chats ||= Resources::LatestChatsResource.new(self)
117
+ end
118
+
119
+ def realtime
120
+ @realtime ||= Resources::RealtimeResource.new(self)
121
+ end
122
+
103
123
  def suno
104
124
  @suno ||= Resources::SunoResource.new(self)
105
125
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_resource"
4
+ require_relative "../types/boards"
5
+ require_relative "../types/paginated"
6
+
7
+ module Blueticks
8
+ module Resources
9
+ class BoardsResource < BaseResource
10
+ # List boards, newest first. Offset-paginated (Board rows).
11
+ #
12
+ # Requires the `boards:read` scope.
13
+ def list(order: nil, skip: nil, limit: nil)
14
+ params = {}
15
+ params["order"] = order unless order.nil?
16
+ params["skip"] = skip unless skip.nil?
17
+ params["limit"] = limit unless limit.nil?
18
+ env = client.request("GET", "/v1/boards", params: params.empty? ? nil : params)
19
+ Types::PaginatedPage.from_hash(env, item_type: Types::Board)
20
+ end
21
+
22
+ # Create or find a board.
23
+ #
24
+ # Returns the existing board for the given `domain`/`entity_kind` when one
25
+ # exists, otherwise creates it. Requires the `boards:write` scope.
26
+ def create(domain: nil, entity_kind: nil, name: nil, description: nil,
27
+ agent_id: nil, memory_retention_days: nil)
28
+ body = {}
29
+ body["domain"] = domain unless domain.nil?
30
+ body["entity_kind"] = entity_kind unless entity_kind.nil?
31
+ body["name"] = name unless name.nil?
32
+ body["description"] = description unless description.nil?
33
+ body["agent_id"] = agent_id unless agent_id.nil?
34
+ body["memory_retention_days"] = memory_retention_days unless memory_retention_days.nil?
35
+ env = client.request("POST", "/v1/boards", body: body)
36
+ Types::BoardDetail.from_hash(env && env["data"])
37
+ end
38
+
39
+ # Get a board (with its columns) by id.
40
+ #
41
+ # Requires the `boards:read` scope.
42
+ def retrieve(board_id)
43
+ env = client.request("GET", "/v1/boards/#{board_id}")
44
+ Types::BoardDetail.from_hash(env && env["data"])
45
+ end
46
+
47
+ # List a board's items (tasks). Offset-paginated (BoardItem rows).
48
+ #
49
+ # Pass `active_only: true` to return only items updated within the
50
+ # retention window whose status is non-terminal. Filter by `status` label
51
+ # or `updated_since`. Requires the `boards:read` scope.
52
+ def list_items(board_id, status: nil, order: nil, active_only: nil,
53
+ updated_since: nil, skip: nil, limit: nil)
54
+ params = {}
55
+ params["status"] = status unless status.nil?
56
+ params["order"] = order unless order.nil?
57
+ params["active_only"] = active_only unless active_only.nil?
58
+ params["updated_since"] = updated_since unless updated_since.nil?
59
+ params["skip"] = skip unless skip.nil?
60
+ params["limit"] = limit unless limit.nil?
61
+ env = client.request("GET", "/v1/boards/#{board_id}/items",
62
+ params: params.empty? ? nil : params)
63
+ Types::PaginatedPage.from_hash(env, item_type: Types::BoardItem)
64
+ end
65
+
66
+ # Create a board item.
67
+ #
68
+ # Requires the `boards:write` scope.
69
+ def create_item(board_id, text:, status: nil, priority: nil, due_date: nil, cells: nil,
70
+ source: nil, confidence: nil, allow_duplicate: nil)
71
+ body = { "text" => text }
72
+ body["status"] = status unless status.nil?
73
+ body["priority"] = priority unless priority.nil?
74
+ body["due_date"] = due_date unless due_date.nil?
75
+ body["cells"] = cells unless cells.nil?
76
+ body["source"] = source unless source.nil?
77
+ body["confidence"] = confidence unless confidence.nil?
78
+ body["allow_duplicate"] = allow_duplicate unless allow_duplicate.nil?
79
+ env = client.request("POST", "/v1/boards/#{board_id}/items", body: body)
80
+ Types::BoardItem.from_hash(env && env["data"])
81
+ end
82
+
83
+ # Update a board item. Pass any subset of the editable fields.
84
+ #
85
+ # Requires the `boards:write` scope.
86
+ def update_item(board_id, item_id, text: nil, status: nil, priority: nil, due_date: nil,
87
+ cells: nil, source: nil, confidence: nil, deleted: nil, note: nil)
88
+ body = {}
89
+ body["text"] = text unless text.nil?
90
+ body["status"] = status unless status.nil?
91
+ body["priority"] = priority unless priority.nil?
92
+ body["due_date"] = due_date unless due_date.nil?
93
+ body["cells"] = cells unless cells.nil?
94
+ body["source"] = source unless source.nil?
95
+ body["confidence"] = confidence unless confidence.nil?
96
+ body["deleted"] = deleted unless deleted.nil?
97
+ body["note"] = note unless note.nil?
98
+ env = client.request("PATCH", "/v1/boards/#{board_id}/items/#{item_id}", body: body)
99
+ Types::BoardItem.from_hash(env && env["data"])
100
+ end
101
+
102
+ # Bulk upsert board items.
103
+ #
104
+ # `items` is an Array of item Hashes (same keys as `create_item`, string
105
+ # keys on the wire). Requires the `boards:write` scope.
106
+ def bulk_upsert_items(board_id, items:, mode: nil)
107
+ body = { "items" => items }
108
+ body["mode"] = mode unless mode.nil?
109
+ env = client.request("POST", "/v1/boards/#{board_id}/items/bulk", body: body)
110
+ Types::BulkItemsResult.from_hash(env && env["data"])
111
+ end
112
+
113
+ # Find existing board items similar to the given candidates.
114
+ #
115
+ # `candidates` is an Array of `{ "text" => "..." }` Hashes. Requires the
116
+ # `boards:read` scope.
117
+ def find_similar_items(board_id, candidates:)
118
+ env = client.request("POST", "/v1/boards/#{board_id}/items/find-similar",
119
+ body: { "candidates" => candidates })
120
+ Types::FindSimilarResult.from_hash(env && env["data"])
121
+ end
122
+ end
123
+ end
124
+ end
@@ -9,9 +9,12 @@ module Blueticks
9
9
  module Resources
10
10
  class ChatsResource < BaseResource
11
11
  # List/search chats, newest first. Offset-paginated.
12
+ #
13
+ # `kinds` is an Array of chat kinds to keep (`%w[contact group newsletter]`);
14
+ # `since` keeps only chats whose last message is at/after that ISO 8601 instant.
12
15
  def list(search_token: nil, filter: nil, include_last_message: nil,
13
16
  include_extended_info: nil, include_without_name: nil,
14
- include_archive: nil, skip: nil, limit: nil)
17
+ include_archive: nil, skip: nil, limit: nil, kinds: nil, since: nil)
15
18
  params = {}
16
19
  params["searchToken"] = search_token unless search_token.nil?
17
20
  params["filter"] = filter unless filter.nil?
@@ -21,6 +24,8 @@ module Blueticks
21
24
  params["includeArchive"] = include_archive unless include_archive.nil?
22
25
  params["skip"] = skip unless skip.nil?
23
26
  params["limit"] = limit unless limit.nil?
27
+ params["kinds"] = kinds unless kinds.nil? || kinds.empty?
28
+ params["since"] = since unless since.nil?
24
29
  env = client.request("GET", "/v1/chats", params: params.empty? ? nil : params)
25
30
  Types::PaginatedPage.from_hash(env, item_type: Types::Chat)
26
31
  end
@@ -88,6 +88,21 @@ module Blueticks
88
88
  Types::Group.from_hash(env && env["data"])
89
89
  end
90
90
 
91
+ # Get the current group picture as a short-lived WhatsApp CDN `url`, or
92
+ # `url: nil` when the group has none. Requires `groups:read`.
93
+ def get_picture(group_id)
94
+ env = client.request("GET", "/v1/groups/#{group_id}/picture")
95
+ Types::GroupPicture.from_hash(env && env["data"])
96
+ end
97
+
98
+ # Remove the group picture. Gateway (Baileys) engine only: a
99
+ # Chrome-extension-only account gets 501 `unsupported_command`. Requires
100
+ # `groups:write`.
101
+ def remove_picture(group_id)
102
+ env = client.request("DELETE", "/v1/groups/#{group_id}/picture")
103
+ Types::GroupPictureRemoved.from_hash(env && env["data"])
104
+ end
105
+
91
106
  # Leave the group as the authenticated identity. Idempotent (204).
92
107
  def leave(group_id)
93
108
  client.request("DELETE", "/v1/groups/#{group_id}/members/me")
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_resource"
4
+ require_relative "../types/identity"
5
+
6
+ module Blueticks
7
+ module Resources
8
+ class IdentityResource < BaseResource
9
+ # Resolve an identity.
10
+ #
11
+ # Maps a WhatsApp JID (phone, `@lid` or group) to its canonical form and
12
+ # aliases. Requires the `contacts:read` scope.
13
+ def retrieve(id)
14
+ env = client.request("GET", "/v1/identity/#{id}")
15
+ Types::Identity.from_hash(env && env["data"])
16
+ end
17
+
18
+ # Resolve identities (batch).
19
+ #
20
+ # Resolves many JIDs in one call. Requires the `contacts:read` scope.
21
+ def resolve_batch(ids:)
22
+ env = client.request("POST", "/v1/identity/resolve", body: { "ids" => ids })
23
+ Types::IdentityBatch.from_hash(env && env["data"])
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_resource"
4
+ require_relative "../types/latest_chats"
5
+
6
+ module Blueticks
7
+ module Resources
8
+ class LatestChatsResource < BaseResource
9
+ # Get latest chats & messages.
10
+ #
11
+ # Returns the most recent chats, each with its latest messages, in one
12
+ # call. Requires the `chats:read` scope.
13
+ def retrieve(number_of_chats: nil, number_of_messages: nil)
14
+ params = {}
15
+ params["numberOfChats"] = number_of_chats unless number_of_chats.nil?
16
+ params["numberOfMessages"] = number_of_messages unless number_of_messages.nil?
17
+ env = client.request("GET", "/v1/latest-chats", params: params.empty? ? nil : params)
18
+ Types::LatestChats.from_hash(env && env["data"])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -10,10 +10,13 @@ module Blueticks
10
10
  class MessagesResource < BaseResource
11
11
  # Query messages from the WhatsApp store, newest first. Offset-paginated
12
12
  # (PublicMessage rows). `message_types` is an Array of wire message types
13
- # (e.g. `%w[chat image]`).
13
+ # (e.g. `%w[chat image]`). `query_any` is an Array of keywords matched with OR
14
+ # semantics; `mime`, `filename_contains`, `has_media` and `sender` narrow the
15
+ # result further.
14
16
  def list(chat_id: nil, search_token: nil, order: nil, since: nil, until_: nil,
15
17
  message_types: nil, load_from_phone_if_needed: nil,
16
- include_media_content: nil, skip: nil, limit: nil)
18
+ include_media_content: nil, skip: nil, limit: nil,
19
+ query_any: nil, mime: nil, filename_contains: nil, has_media: nil, sender: nil)
17
20
  params = {}
18
21
  params["chatId"] = chat_id unless chat_id.nil?
19
22
  params["searchToken"] = search_token unless search_token.nil?
@@ -25,6 +28,11 @@ module Blueticks
25
28
  params["includeMediaContent"] = include_media_content unless include_media_content.nil?
26
29
  params["skip"] = skip unless skip.nil?
27
30
  params["limit"] = limit unless limit.nil?
31
+ params["queryAny"] = query_any unless query_any.nil? || query_any.empty?
32
+ params["mime"] = mime unless mime.nil?
33
+ params["filenameContains"] = filename_contains unless filename_contains.nil?
34
+ params["hasMedia"] = has_media unless has_media.nil?
35
+ params["sender"] = sender unless sender.nil?
28
36
  env = client.request("GET", "/v1/messages", params: params.empty? ? nil : params)
29
37
  Types::PaginatedPage.from_hash(env, item_type: Types::PublicMessage)
30
38
  end
@@ -54,8 +62,15 @@ module Blueticks
54
62
  end
55
63
 
56
64
  # Ask the engine to pull older history from the connected phone.
57
- def load_older(chat_id)
58
- env = client.request("POST", "/v1/messages/load_older/#{chat_id}")
65
+ #
66
+ # `pages` (1-10, default 1) is how many older pages to pull in one call;
67
+ # `until_date` (ISO 8601 string or epoch seconds/ms) stops paging once the
68
+ # oldest loaded message is at/before that date.
69
+ def load_older(chat_id, pages: nil, until_date: nil)
70
+ body = {}
71
+ body["pages"] = pages unless pages.nil?
72
+ body["until_date"] = until_date unless until_date.nil?
73
+ env = client.request("POST", "/v1/messages/load_older/#{chat_id}", body: body.empty? ? nil : body)
59
74
  Types::LoadOlderResult.from_hash(env && env["data"])
60
75
  end
61
76
 
@@ -105,7 +120,8 @@ module Blueticks
105
120
  def create(chat_id, type:, text: nil, media_url: nil, media_base64: nil,
106
121
  media_kind: nil, media_filename: nil, poll_question: nil,
107
122
  poll_options: nil, poll_allow_multiple: nil, reply_to: nil,
108
- secret: nil, with_typing: nil, typing_seconds: nil, idempotency_key: nil)
123
+ secret: nil, with_typing: nil, typing_seconds: nil, idempotency_key: nil,
124
+ mentions: nil)
109
125
  body = { "type" => type }
110
126
  body["text"] = text unless text.nil?
111
127
  body["mediaUrl"] = media_url unless media_url.nil?
@@ -119,6 +135,7 @@ module Blueticks
119
135
  body["secret"] = secret unless secret.nil?
120
136
  body["withTyping"] = with_typing unless with_typing.nil?
121
137
  body["typingSeconds"] = typing_seconds unless typing_seconds.nil?
138
+ body["mentions"] = mentions unless mentions.nil?
122
139
  env = client.request("POST", "/v1/messages/#{chat_id}", body: body, idempotency_key: idempotency_key)
123
140
  Types::Message.from_hash(env && env["data"])
124
141
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_resource"
4
+ require_relative "../types/realtime"
5
+
6
+ module Blueticks
7
+ module Resources
8
+ class RealtimeResource < BaseResource
9
+ # Get MQTT credentials for receiving messages in real time.
10
+ #
11
+ # Returns short-lived broker credentials and the topic to subscribe to.
12
+ # Requires the `chats:read` scope.
13
+ def create_token
14
+ env = client.request("POST", "/v1/realtime/token")
15
+ Types::RealtimeToken.from_hash(env && env["data"])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -15,6 +15,9 @@ module Blueticks
15
15
  # Omit `send_at` to dispatch as soon as possible, or pass an ISO-8601
16
16
  # timestamp to schedule it.
17
17
  #
18
+ # `mentions` is an Array of participant JIDs to @mention in a group;
19
+ # `recipient_name` is an optional display name for the scheduled-message list.
20
+ #
18
21
  # - `type: "text"` — requires `text`.
19
22
  # - `type: "media"` — requires `media_url` or `media_base64`; optional
20
23
  # `media_kind`, `media_filename`.
@@ -23,7 +26,8 @@ module Blueticks
23
26
  def create(chat_id, type:, text: nil, media_url: nil, media_base64: nil,
24
27
  media_kind: nil, media_filename: nil, poll_question: nil,
25
28
  poll_options: nil, poll_allow_multiple: nil, reply_to: nil,
26
- secret: nil, send_at: nil, idempotency_key: nil)
29
+ secret: nil, send_at: nil, idempotency_key: nil,
30
+ mentions: nil, recipient_name: nil)
27
31
  body = { "type" => type }
28
32
  body["text"] = text unless text.nil?
29
33
  body["mediaUrl"] = media_url unless media_url.nil?
@@ -36,6 +40,8 @@ module Blueticks
36
40
  body["replyTo"] = reply_to unless reply_to.nil?
37
41
  body["secret"] = secret unless secret.nil?
38
42
  body["sendAt"] = send_at unless send_at.nil?
43
+ body["mentions"] = mentions unless mentions.nil?
44
+ body["recipientName"] = recipient_name unless recipient_name.nil?
39
45
  env = client.request("POST", "/v1/scheduled-messages/#{chat_id}", body: body,
40
46
  idempotency_key: idempotency_key)
41
47
  Types::ScheduledMessage.from_hash(env && env["data"])
@@ -67,7 +73,7 @@ module Blueticks
67
73
  def update(message_id, type: nil, text: nil, media_url: nil, media_base64: nil,
68
74
  media_kind: nil, media_filename: nil, poll_question: nil,
69
75
  poll_options: nil, poll_allow_multiple: nil, reply_to: nil,
70
- secret: nil, send_at: nil)
76
+ secret: nil, send_at: nil, mentions: nil)
71
77
  body = {}
72
78
  body["type"] = type unless type.nil?
73
79
  body["text"] = text unless text.nil?
@@ -81,6 +87,7 @@ module Blueticks
81
87
  body["replyTo"] = reply_to unless reply_to.nil?
82
88
  body["secret"] = secret unless secret.nil?
83
89
  body["sendAt"] = send_at unless send_at.nil?
90
+ body["mentions"] = mentions unless mentions.nil?
84
91
  env = client.request("PATCH", "/v1/scheduled-messages/#{message_id}", body: body)
85
92
  Types::ScheduledMessage.from_hash(env && env["data"])
86
93
  end
@@ -12,15 +12,16 @@ module Blueticks
12
12
  Types::SunoAccount.from_hash(env && env["data"])
13
13
  end
14
14
 
15
- # Submit a song generation job. Requires `lyrics` and `style`; the many
15
+ # Submit a song generation job. Requires `style`; `lyrics` is required unless `instrumental` is true. The many
16
16
  # optional knobs mirror the Suno request (vocal_gender, weirdness,
17
17
  # style_influence, audio_influence, instrumental, model, title, upload_id,
18
18
  # captcha_token, negative_style).
19
- def create_song(lyrics:, style:, negative_style: nil, vocal_gender: nil,
19
+ def create_song(style:, lyrics: nil, negative_style: nil, vocal_gender: nil,
20
20
  weirdness: nil, style_influence: nil, audio_influence: nil,
21
21
  instrumental: nil, model: nil, title: nil, upload_id: nil,
22
22
  captcha_token: nil)
23
- body = { "lyrics" => lyrics, "style" => style }
23
+ body = { "style" => style }
24
+ body["lyrics"] = lyrics unless lyrics.nil?
24
25
  body["negativeStyle"] = negative_style unless negative_style.nil?
25
26
  body["vocalGender"] = vocal_gender unless vocal_gender.nil?
26
27
  body["weirdness"] = weirdness unless weirdness.nil?
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Blueticks
6
+ module Types
7
+ # A status/priority label with its colour (OptionRef).
8
+ class BoardOption < Base
9
+ field :label, :string
10
+ field :bgcolor, :string
11
+ end
12
+
13
+ # A board column definition (BoardColumn). `type` is the column kind
14
+ # (e.g. "status", "text"); `options` lists the selectable labels.
15
+ class BoardColumn < Base
16
+ field :column_id, :string
17
+ field :title, :string
18
+ field :type, :string
19
+ field :options, [:array, BoardOption]
20
+ end
21
+
22
+ # A board summary (BoardRow) as returned by boards.list.
23
+ class Board < Base
24
+ field :board_id, :string
25
+ field :name, :string
26
+ field :description, :string, optional: true
27
+ field :domain, :string, optional: true
28
+ field :agent_managed, :boolean
29
+ field :memory_retention_days, :integer, optional: true
30
+ field :item_count, :integer, optional: true
31
+ field :web_url, :string
32
+ field :created_at, :string
33
+ field :updated_at, :string
34
+ end
35
+
36
+ # A board with its columns (BoardDetail) as returned by boards.create and
37
+ # boards.retrieve.
38
+ class BoardDetail < Base
39
+ field :board_id, :string
40
+ field :name, :string
41
+ field :description, :string, optional: true
42
+ field :domain, :string, optional: true
43
+ field :agent_managed, :boolean
44
+ field :memory_retention_days, :integer, optional: true
45
+ field :item_count, :integer, optional: true
46
+ field :web_url, :string
47
+ field :created_at, :string
48
+ field :updated_at, :string
49
+ field :columns, [:array, BoardColumn]
50
+ end
51
+
52
+ # Short board reference (BoardRef) embedded in a bulk-upsert result.
53
+ class BoardRef < Base
54
+ field :board_id, :string
55
+ field :name, :string
56
+ field :description, :string, optional: true
57
+ field :domain, :string, optional: true
58
+ field :web_url, :string
59
+ end
60
+
61
+ # The chat message an item was extracted from (ItemSource).
62
+ class ItemSource < Base
63
+ field :chat_id, :string, optional: true
64
+ field :chat_name, :string, optional: true
65
+ field :message_key, :string, optional: true
66
+ field :excerpt, :string, optional: true
67
+ field :extracted_at, :integer, optional: true
68
+ end
69
+
70
+ # A board item / task (BoardItem). `cells` holds custom column values keyed
71
+ # by column title (string or number).
72
+ class BoardItem < Base
73
+ field :item_id, :string
74
+ field :text, :string
75
+ field :status, BoardOption, optional: true
76
+ field :priority, BoardOption, optional: true
77
+ field :due_date, :string, optional: true
78
+ field :cells, :hash, optional: true
79
+ field :source, ItemSource, optional: true
80
+ field :confidence, :number, optional: true
81
+ field :last_seen_at, :integer, optional: true
82
+ field :web_url, :string
83
+ field :created_at, :string
84
+ field :updated_at, :string
85
+ end
86
+
87
+ # Result of a bulk upsert (BulkItemsResponse).
88
+ class BulkItemsResult < Base
89
+ field :created, [:array, BoardItem]
90
+ field :updated, [:array, BoardItem]
91
+ field :unchanged, :integer
92
+ field :board, BoardRef
93
+ end
94
+
95
+ # One candidate-to-existing-item match (FindSimilarResponse.matches[]).
96
+ class SimilarMatch < Base
97
+ field :candidate_index, :integer
98
+ field :item_id, :string
99
+ field :text, :string
100
+ field :score, :number
101
+ end
102
+
103
+ # Result of boards.find_similar_items (FindSimilarResponse). `misses` holds
104
+ # the indexes of candidates with no match.
105
+ class FindSimilarResult < Base
106
+ field :matches, [:array, SimilarMatch]
107
+ field :misses, %i[array integer]
108
+ end
109
+ end
110
+ end
@@ -9,12 +9,12 @@ module Blueticks
9
9
  # A chat row (Chat) returned by chats.list / chats.retrieve.
10
10
  class Chat < Base
11
11
  field :chat_id, :string, wire_name: "chatId"
12
- field :name, :string, nullable: true
12
+ field :name, :string, nullable: true, optional: true
13
13
  field :chat_type, [:enum, CHAT_TYPES], wire_name: "chatType"
14
14
  field :pinned, :boolean, optional: true
15
15
  field :archived, :boolean, optional: true
16
- field :last_message_at, :string, nullable: true, wire_name: "lastMessageAt"
17
- field :unread_count, :integer, nullable: true, wire_name: "unreadCount"
16
+ field :last_message_at, :string, nullable: true, optional: true, wire_name: "lastMessageAt"
17
+ field :unread_count, :integer, nullable: true, optional: true, wire_name: "unreadCount"
18
18
  field :marked_unread, :boolean, wire_name: "markedUnread"
19
19
  field :last_message_text, :string, nullable: true, optional: true, wire_name: "lastMessageText"
20
20
  field :last_message_from_me, :boolean, nullable: true, optional: true, wire_name: "lastMessageFromMe"
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Blueticks
6
+ module Types
7
+ # How far back a chat is loaded (LoadCoverage). `oldest_loaded_iso` is the
8
+ # deterministic answer to "have I reached far enough back?".
9
+ class Coverage < Base
10
+ field :oldest_loaded_timestamp, :number, optional: true, wire_name: "oldestLoadedTimestamp"
11
+ field :oldest_loaded_iso, :string, optional: true, wire_name: "oldestLoadedIso"
12
+ end
13
+ end
14
+ end
@@ -27,6 +27,17 @@ module Blueticks
27
27
  field :participants, [:array, GroupParticipant], nullable: true
28
28
  end
29
29
 
30
+ # Current group picture (GroupPicture). `url` is a short-lived WhatsApp CDN
31
+ # URL, or nil when the group has no picture.
32
+ class GroupPicture < Base
33
+ field :url, :string, nullable: true
34
+ end
35
+
36
+ # Result of removing a group picture (GroupPictureRemoved).
37
+ class GroupPictureRemoved < Base
38
+ field :ok, :boolean
39
+ end
40
+
30
41
  # A group row in the groups.list / contacts.common_groups page
31
42
  # (GroupListItem) — a slimmer projection without announce/participants.
32
43
  class GroupListItem < Base
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Blueticks
6
+ module Types
7
+ IDENTITY_TYPES = %w[user group unknown].freeze
8
+ IDENTITY_SOURCES = %w[engine cache unknown].freeze
9
+
10
+ # A resolved WhatsApp identity (Identity). `canonical`, `phone` and `lid`
11
+ # are absent/null when the identity could not be resolved.
12
+ class Identity < Base
13
+ field :input, :string
14
+ field :resolved, :boolean
15
+ field :canonical, :string, optional: true, nullable: true
16
+ field :phone, :string, optional: true, nullable: true
17
+ field :lid, :string, optional: true, nullable: true
18
+ field :aliases, %i[array string]
19
+ field :type, [:enum, IDENTITY_TYPES]
20
+ field :source, [:enum, IDENTITY_SOURCES]
21
+ end
22
+
23
+ # Batch identity-resolution result (IdentityBatchResponse).
24
+ class IdentityBatch < Base
25
+ field :data, [:array, Identity]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "chats"
5
+ require_relative "messages"
6
+
7
+ module Blueticks
8
+ module Types
9
+ # A chat row together with its latest messages (LatestChatWithMessages).
10
+ class LatestChat < Base
11
+ field :chat_id, :string, wire_name: "chatId"
12
+ field :name, :string, optional: true, nullable: true
13
+ field :chat_type, [:enum, CHAT_TYPES], wire_name: "chatType"
14
+ field :pinned, :boolean, optional: true
15
+ field :archived, :boolean, optional: true
16
+ field :last_message_at, :string, optional: true, nullable: true, wire_name: "lastMessageAt"
17
+ field :unread_count, :integer, optional: true, nullable: true, wire_name: "unreadCount"
18
+ field :marked_unread, :boolean, wire_name: "markedUnread"
19
+ field :last_message_text, :string, nullable: true, optional: true, wire_name: "lastMessageText"
20
+ field :last_message_from_me, :boolean, nullable: true, optional: true, wire_name: "lastMessageFromMe"
21
+ field :messages, [:array, PublicMessage]
22
+ end
23
+
24
+ # Latest chats and their messages (LatestChatsResponse) from GET /v1/latest-chats.
25
+ class LatestChats < Base
26
+ field :chats, [:array, LatestChat]
27
+ field :total_chats, :integer, wire_name: "totalChats"
28
+ field :number_of_chats, :integer, wire_name: "numberOfChats"
29
+ field :number_of_messages, :integer, wire_name: "numberOfMessages"
30
+ end
31
+ end
32
+ end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "coverage"
4
5
 
5
6
  module Blueticks
6
7
  module Types
7
8
  MESSAGE_KINDS = %w[text media poll].freeze
8
9
  MESSAGE_MEDIA_KINDS = %w[image video audio document sticker voice gif].freeze
9
10
  MEDIA_UNAVAILABLE_REASONS = %w[expired fetching awaiting_sender error no_media].freeze
11
+ MESSAGE_SOURCES = %w[vault backup].freeze
10
12
  MESSAGE_STATUSES = %w[
11
13
  pending before-wa-send bt-sent sending sent sent_pending_ack confirmed
12
14
  delivered received read played cancelled error failed expired
@@ -65,6 +67,8 @@ module Blueticks
65
67
  field :filename, :string, optional: true
66
68
  field :link_preview, PublicLinkPreview, optional: true, wire_name: "linkPreview"
67
69
  field :quoted_message, QuotedMessage, optional: true, wire_name: "quotedMessage"
70
+ field :source, [:enum, MESSAGE_SOURCES], optional: true
71
+ field :secret, :string, optional: true
68
72
  end
69
73
 
70
74
  # Media payload for a message (Media) returned by messages.get_media.
@@ -97,13 +101,17 @@ module Blueticks
97
101
  field :text, :string, nullable: true
98
102
  end
99
103
 
100
- # Result of a load-older request (LoadOlderResult).
104
+ # Result of a load-older request (LoadOlderResult). `pages_executed` is how
105
+ # many older pages this call pulled; `coverage` says how far back the chat is
106
+ # now loaded.
101
107
  class LoadOlderResult < Base
102
108
  field :total_messages, :integer, nullable: true, wire_name: "totalMessages"
103
109
  field :added, :integer, nullable: true
104
110
  field :can_load_more, :boolean, wire_name: "canLoadMore"
105
111
  field :history_unavailable, :boolean, wire_name: "historyUnavailable"
106
112
  field :error, :string, nullable: true
113
+ field :pages_executed, :integer, wire_name: "pagesExecuted"
114
+ field :coverage, Coverage
107
115
  end
108
116
 
109
117
  # A sent/queued outbound message record — the shape returned by
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "base"
4
+ require_relative "coverage"
4
5
  require_relative "../errors"
5
6
 
6
7
  module Blueticks
@@ -11,21 +12,23 @@ module Blueticks
11
12
  # Iterate `data` for the current page; advance by passing `skip: skip + limit`
12
13
  # (or any offset) back on the next list call. `total` is the full match count
13
14
  # across all pages. `has_more` is populated only by endpoints that echo a
14
- # `hasMore` flag (e.g. messages.list); nil elsewhere.
15
+ # `hasMore` flag (e.g. messages.list); nil elsewhere. `coverage` (Types::Coverage) is
16
+ # likewise populated only by messages.list.
15
17
  class PaginatedPage
16
- attr_reader :data, :limit, :skip, :total, :has_more
18
+ attr_reader :data, :limit, :skip, :total, :has_more, :coverage
17
19
 
18
- def initialize(data:, limit:, skip:, total:, has_more: nil)
20
+ def initialize(data:, limit:, skip:, total:, has_more: nil, coverage: nil)
19
21
  @data = data
20
22
  @limit = limit
21
23
  @skip = skip
22
24
  @total = total
23
25
  @has_more = has_more
26
+ @coverage = coverage
24
27
  end
25
28
 
26
29
  # Build a PaginatedPage from a raw list envelope, decoding each item via
27
30
  # item_type.from_hash. Reads `data`/`limit`/`skip`/`total` (and optional
28
- # `hasMore`) off the envelope; ignores the `success` flag.
31
+ # `hasMore` / `coverage`) off the envelope; ignores the `success` flag.
29
32
  def self.from_hash(raw, item_type:)
30
33
  unless raw.is_a?(Hash)
31
34
  raise Errors::ValidationError.new(message: "Expected Hash for PaginatedPage, got #{raw.class}")
@@ -43,11 +46,13 @@ module Blueticks
43
46
  raise Errors::ValidationError.new(message: "Field 'hasMore' must be boolean or absent in PaginatedPage")
44
47
  end
45
48
 
49
+ coverage = raw["coverage"].nil? ? nil : Coverage.from_hash(raw["coverage"], context: "PaginatedPage.coverage")
50
+
46
51
  decoded = raw["data"].each_with_index.map do |item, i|
47
52
  item_type.from_hash(item, context: "PaginatedPage.data[#{i}]")
48
53
  end
49
54
 
50
- new(data: decoded, limit: limit, skip: skip, total: total, has_more: has_more)
55
+ new(data: decoded, limit: limit, skip: skip, total: total, has_more: has_more, coverage: coverage)
51
56
  end
52
57
 
53
58
  def self.require_integer(raw, key)
@@ -62,7 +67,7 @@ module Blueticks
62
67
  def to_h
63
68
  {
64
69
  data: data.map { |d| d.respond_to?(:to_h) ? d.to_h : d },
65
- limit: limit, skip: skip, total: total, has_more: has_more
70
+ limit: limit, skip: skip, total: total, has_more: has_more, coverage: coverage&.to_h
66
71
  }
67
72
  end
68
73
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Blueticks
6
+ module Types
7
+ # MQTT topics the realtime credentials are authorised for.
8
+ class RealtimeTopics < Base
9
+ field :messages, :string
10
+ end
11
+
12
+ # Short-lived MQTT credentials for receiving messages in real time
13
+ # (RealtimeTokenResponse).
14
+ class RealtimeToken < Base
15
+ field :broker_url, :string, wire_name: "brokerUrl"
16
+ field :username, :string
17
+ field :password, :string
18
+ field :expires_at, :string, wire_name: "expiresAt"
19
+ field :expires_in_seconds, :integer, wire_name: "expiresInSeconds"
20
+ field :topics, RealtimeTopics
21
+ end
22
+ end
23
+ end
@@ -16,11 +16,11 @@ module Blueticks
16
16
  group_admin_changed_webhook group_name_changed_webhook group_description_changed_webhook
17
17
  group_message_pinned_webhook poll_vote_webhook reply_to_my_message_webhook
18
18
  message_deleted_revoked_webhook message_edited_webhook
19
+ new_message_received_from_me_webhook
19
20
  ].freeze
20
21
 
21
- # A webhook subscription (WebhookResponse). The signing secret is returned
22
- # only out-of-band at creation time in the dashboard; it is not part of the
23
- # API response body in 2.x.
22
+ # A webhook subscription (WebhookResponse). `signing_secret` is present only
23
+ # when the API includes it in the response body.
24
24
  class Webhook < Base
25
25
  field :id, :string
26
26
  field :url, :string
@@ -28,6 +28,7 @@ module Blueticks
28
28
  field :description, :string, nullable: true
29
29
  field :status, [:enum, WEBHOOK_STATUSES]
30
30
  field :created_at, :string, wire_name: "createdAt"
31
+ field :signing_secret, :string, optional: true, wire_name: "signingSecret"
31
32
  end
32
33
 
33
34
  # A delivered webhook event, as parsed by Blueticks::Webhooks.verify. Note
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Blueticks
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blueticks
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Blueticks
@@ -68,14 +68,18 @@ files:
68
68
  - lib/blueticks/errors.rb
69
69
  - lib/blueticks/resources/account.rb
70
70
  - lib/blueticks/resources/audiences.rb
71
+ - lib/blueticks/resources/boards.rb
71
72
  - lib/blueticks/resources/campaigns.rb
72
73
  - lib/blueticks/resources/chats.rb
73
74
  - lib/blueticks/resources/contacts.rb
74
75
  - lib/blueticks/resources/engines.rb
75
76
  - lib/blueticks/resources/groups.rb
77
+ - lib/blueticks/resources/identity.rb
78
+ - lib/blueticks/resources/latest_chats.rb
76
79
  - lib/blueticks/resources/messages.rb
77
80
  - lib/blueticks/resources/newsletters.rb
78
81
  - lib/blueticks/resources/ping.rb
82
+ - lib/blueticks/resources/realtime.rb
79
83
  - lib/blueticks/resources/scheduled_messages.rb
80
84
  - lib/blueticks/resources/suno.rb
81
85
  - lib/blueticks/resources/utils.rb
@@ -84,18 +88,23 @@ files:
84
88
  - lib/blueticks/types/account.rb
85
89
  - lib/blueticks/types/audiences.rb
86
90
  - lib/blueticks/types/base.rb
91
+ - lib/blueticks/types/boards.rb
87
92
  - lib/blueticks/types/campaigns.rb
88
93
  - lib/blueticks/types/chats.rb
89
94
  - lib/blueticks/types/contacts.rb
95
+ - lib/blueticks/types/coverage.rb
90
96
  - lib/blueticks/types/deleted_resource.rb
91
97
  - lib/blueticks/types/engines.rb
92
98
  - lib/blueticks/types/groups.rb
99
+ - lib/blueticks/types/identity.rb
100
+ - lib/blueticks/types/latest_chats.rb
93
101
  - lib/blueticks/types/messages.rb
94
102
  - lib/blueticks/types/newsletter.rb
95
103
  - lib/blueticks/types/ok_result.rb
96
104
  - lib/blueticks/types/page.rb
97
105
  - lib/blueticks/types/paginated.rb
98
106
  - lib/blueticks/types/ping.rb
107
+ - lib/blueticks/types/realtime.rb
99
108
  - lib/blueticks/types/scheduled_messages.rb
100
109
  - lib/blueticks/types/suno.rb
101
110
  - lib/blueticks/types/utils.rb