Class: WhatsAppNotifier::WebAdapter
- Inherits:
-
Object
- Object
- WhatsAppNotifier::WebAdapter
- Defined in:
- lib/whatsapp_notifier/web_adapter.rb
Constant Summary collapse
- DEFAULT_OPEN_TIMEOUT =
5- DEFAULT_READ_TIMEOUT =
30- MEDIA_OPEN_TIMEOUT =
Media bytes can be tens of MB over a slow link — give the binary fetch a longer read window than the JSON control plane.
5- MEDIA_READ_TIMEOUT =
60- HTTP_CLASSES =
{ post: Net::HTTP::Post, get: Net::HTTP::Get, delete: Net::HTTP::Delete }.freeze
- INBOUND_OPTIONAL_KEYS =
Optional inbound keys introduced by the 0.7.0 service (media verdict + sender display name) and the 0.8.0 service (two-way capture). Mapped ONLY when the wire payload carries them, so hosts can key-gate on presence: a missing has_media means “0.6.0 service, no media support” (while has_media: false means “text message”), and a missing from_me means “customer message or pre-0.8.0 service”. ‘to` carries the counterparty chat id on operator-sent (from_me) messages — the id the host threads the conversation on.
{ has_media: %w[hasMedia has_media], media_status: %w[mediaStatus media_status], media_error: %w[mediaError media_error], media_mime: %w[mediaMime media_mime], media_filename: %w[mediaFilename media_filename], media_size: %w[mediaSize media_size], sender_name: %w[senderName sender_name], to: %w[to], from_me: %w[fromMe from_me] }.freeze
- HISTORY_LIMIT_DEFAULT =
Mirrors the service-side clamp (history.ts) so a host-passed limit can never balloon one request into a session-stalling bulk fetch.
50- HISTORY_LIMIT_RANGE =
(1..200).freeze
Class Method Summary collapse
Instance Method Summary collapse
- #connection_status(metadata: {}) ⇒ Object
-
#delete_media(message_id:, metadata: {}) ⇒ Object
Removes the service’s copy after the host has attached the bytes.
-
#fetch_history(chat_id:, limit: 50, metadata: {}) ⇒ Object
Replays one chat’s history through the service’s live-capture normalizer and returns it synchronously (no queue, no webhook) — oldest-first, mapped exactly like fetch_inbound messages, including from_me/to on the operator’s side of the conversation.
-
#fetch_inbound(metadata: {}) ⇒ Object
Drains the service’s pending inbound queue for this user.
-
#fetch_media(message_id:, metadata: {}) ⇒ Object
Fetches the raw bytes of a downloaded inbound media file.
- #fetch_qr_code(metadata: {}) ⇒ Object
-
#initialize(base_url: self.class.default_base_url, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT) ⇒ WebAdapter
constructor
A new instance of WebAdapter.
-
#list_chats(metadata: {}) ⇒ Object
Lists the paired number’s 1:1 chats for history-sync discovery.
-
#logout(metadata: {}) ⇒ Object
Logs the user out of WhatsApp and clears their saved session on the service.
-
#refetch_media(message_id:, chat_id:, metadata: {}) ⇒ Object
On-demand re-download (WhatsApp tap-to-download).
- #send_message(payload:, session: {}) ⇒ Object
Constructor Details
#initialize(base_url: self.class.default_base_url, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT) ⇒ WebAdapter
Returns a new instance of WebAdapter.
44 45 46 47 48 49 50 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 44 def initialize(base_url: self.class.default_base_url, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT) @base_url = base_url @open_timeout = open_timeout @read_timeout = read_timeout end |
Class Method Details
.default_base_url ⇒ Object
40 41 42 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 40 def self.default_base_url ENV["WHATSAPP_NOTIFIER_SERVICE_URL"] || ENV["WHATSAPP_SERVICE_URL"] || "http://127.0.0.1:3001" end |
Instance Method Details
#connection_status(metadata: {}) ⇒ Object
80 81 82 83 84 85 86 87 88 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 80 def connection_status(metadata: {}) user_id = user_id_from() response = request(:get, "/status/#{user_id}") { state: response["state"], authenticated: response["authenticated"], has_qr: response["hasQR"] } end |
#delete_media(message_id:, metadata: {}) ⇒ Object
Removes the service’s copy after the host has attached the bytes. Idempotent on the service side: deleting absent media still succeeds. A 0.6.0 service mid-rollout has no /media routes and answers 404 —degrade to { success: false } instead of raising, mirroring fetch_media’s nil-on-404.
151 152 153 154 155 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 151 def delete_media(message_id:, metadata: {}) user_id = user_id_from() response = request(:delete, "/media/#{user_id}/#{path_id()}", allow_404: true) { success: response.fetch("success", false) } end |
#fetch_history(chat_id:, limit: 50, metadata: {}) ⇒ Object
Replays one chat’s history through the service’s live-capture normalizer and returns it synchronously (no queue, no webhook) —oldest-first, mapped exactly like fetch_inbound messages, including from_me/to on the operator’s side of the conversation. History media arrives marked unavailable by design (media_error “history”): the service never bulk-downloads old media; live capture handles bytes going forward.
175 176 177 178 179 180 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 175 def fetch_history(chat_id:, limit: 50, metadata: {}) user_id = user_id_from() body = { chatId: chat_id, limit: clamp_history_limit(limit) } response = request(:post, "/history/#{user_id}", body: body) Array(response["messages"]).map { |m| (m) } end |
#fetch_inbound(metadata: {}) ⇒ Object
Drains the service’s pending inbound queue for this user. The service returns the messages once, then clears them (at-least-once handoff —callers must dedupe on message_id). Accepts either a bare array or a { “messages” => […] } envelope so the wire format can evolve.
94 95 96 97 98 99 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 94 def fetch_inbound(metadata: {}) user_id = user_id_from() response = request(:get, "/inbound/#{user_id}") raw = response.is_a?(Hash) ? response["messages"] : response Array(raw).map { |m| (m) } end |
#fetch_media(message_id:, metadata: {}) ⇒ Object
Fetches the raw bytes of a downloaded inbound media file. Returns { body:, mime:, filename:, size: } or nil when the service has no copy (never downloaded, swept by TTL, or already deleted).
Deliberately NOT routed through #request: that path JSON-parses the response body (and host apps are known to patch it further), which would corrupt binary payloads.
108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 108 def fetch_media(message_id:, metadata: {}) user_id = user_id_from() res = binary_get("/media/#{user_id}/#{path_id()}") return nil if res.code.to_s == "404" raise "service request failed (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess) body = res.body.to_s { body: body, mime: res["Content-Type"], filename: filename_from(res["Content-Disposition"]), size: body.bytesize } end |
#fetch_qr_code(metadata: {}) ⇒ Object
74 75 76 77 78 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 74 def fetch_qr_code(metadata: {}) user_id = user_id_from() response = request(:get, "/qr/#{user_id}") response["qr"] end |
#list_chats(metadata: {}) ⇒ Object
Lists the paired number’s 1:1 chats for history-sync discovery. Returns
- { id:, name:, last_message_at: }
-
newest-first; the service caps the
list at its newest 500 and excludes groups/status/privacy chats. The route is token-gated like /media and raises the standard error on any non-2xx (401 when the user never paired or isn’t ready).
162 163 164 165 166 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 162 def list_chats(metadata: {}) user_id = user_id_from() response = request(:get, "/chats/#{user_id}") Array(response["chats"]).map { |chat| map_chat_summary(chat) } end |
#logout(metadata: {}) ⇒ Object
Logs the user out of WhatsApp and clears their saved session on the service.
183 184 185 186 187 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 183 def logout(metadata: {}) user_id = user_id_from() response = request(:post, "/logout/#{user_id}") { success: response.fetch("success", false) } end |
#refetch_media(message_id:, chat_id:, metadata: {}) ⇒ Object
On-demand re-download (WhatsApp tap-to-download). The host calls this when an operator opens a media bubble whose bytes the service no longer holds (rolled off by the per-user cap or expired by TTL): the service re-pulls THAT one message’s media and stores it, after which the host fetches it with the usual fetch_media GET. Returns { mime:, filename:, size:, status: } on success, or nil when the media is gone upstream (404) — same nil-on-404 contract as fetch_media, so a host that gets nil can grey the bubble out. A 0.7.0 service mid-rollout has no /refetch route and also answers 404 →nil, indistinguishable from gone, which is the safe degrade.
132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 132 def refetch_media(message_id:, chat_id:, metadata: {}) user_id = user_id_from() body = { messageId: , chatId: chat_id } response = request(:post, "/media/#{user_id}/refetch", body: body, allow_404: true) return nil unless response["success"] { mime: response["mediaMime"] || response["media_mime"], filename: response["mediaFilename"] || response["media_filename"], size: response["mediaSize"] || response["media_size"], status: response["mediaStatus"] || response["media_status"] } end |
#send_message(payload:, session: {}) ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 52 def (payload:, session: {}) user_id = user_id_from(payload[:metadata] || {}) body = { to: payload[:to], message: payload[:body], mediaUrl: payload.dig(:metadata, :media_url) }.compact response = request(:post, "/send/#{user_id}", body: body) { success: response.fetch("success"), # Prefer the service-issued WhatsApp message id (0.8.0): it is the key # the host dedupes the send's own fromMe echo on, so a real id must # win over the locally fabricated one. The fallback keeps 0.7.0 # services (no messageId in the response) working unchanged. message_id: response["messageId"] || response["message_id"] || payload[:idempotency_key] || "local-#{Time.now.to_i}", session: session, error_message: response["error"] } end |