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], # 0.9.3: the `<digits>@lid` privacy id the counterparty above was # RESOLVED FROM, present only for a privacy-keyed chat. The phone in # from/to is always a real phone — this says where it came from, so a # host can audit a thread's origin. Never a value to thread on. sender_lid: %w[senderLid sender_lid] }.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).
-
#resolve_lid(lid:, metadata: {}) ⇒ Object
Resolves ONE WhatsApp privacy id (
@lid) to the phone number behind it, returning the bare digits or nil when nothing is recoverable. - #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.
49 50 51 52 53 54 55 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 49 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
45 46 47 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 45 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
89 90 91 92 93 94 95 96 97 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 89 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.
160 161 162 163 164 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 160 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.
184 185 186 187 188 189 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 184 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.
103 104 105 106 107 108 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 103 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.
117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 117 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
83 84 85 86 87 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 83 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).
171 172 173 174 175 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 171 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.
213 214 215 216 217 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 213 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.
141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 141 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 |
#resolve_lid(lid:, metadata: {}) ⇒ Object
Resolves ONE WhatsApp privacy id (@lid) to the phone number behind it,
returning the bare digits or nil when nothing is recoverable.
Hosts need this to repair conversations they keyed on an unresolved privacy id — rows written when the service still fabricated a "phone" out of the LID's own digits (fixed in 0.9.3). Live capture resolves on its own; this is the backward-looking half.
nil is a real answer ("no phone behind this id"), not an error: the service replies 200 with pn: null, and only a transport/gate failure raises.
202 203 204 205 206 207 208 209 210 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 202 def resolve_lid(lid:, metadata: {}) user_id = user_id_from() digits = lid.to_s.split("@").first.to_s.gsub(/\D/, "") return nil if digits.empty? response = request(:get, "/contacts/lid/#{user_id}?lid=#{digits}") resolved = response["pn"].to_s.split("@").first.to_s.gsub(/\D/, "") resolved.empty? ? nil : resolved end |
#send_message(payload:, session: {}) ⇒ Object
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/whatsapp_notifier/web_adapter.rb', line 57 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 |