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".
tocarries 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
84 85 86 87 88 89 90 91 92 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 84 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.
155 156 157 158 159 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 155 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.
179 180 181 182 183 184 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 179 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.
98 99 100 101 102 103 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 98 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.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 112 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 ServiceError.new("service request failed (#{res.code}): #{res.body}", status: res.code) 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
78 79 80 81 82 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 78 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).
166 167 168 169 170 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 166 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.
187 188 189 190 191 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 187 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.
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 136 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 73 74 75 76 |
# 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, # A code the service supplied wins over anything the provider could # infer from the message text. Absent on every service ≤ 0.8.x, which # is why the provider still classifies as a fallback. error_code: ErrorCode.normalize(response["errorCode"] || response["error_code"]), error_message: response["error"] } end |