whatsapp_notifier 0.6.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 +4 -4
- data/README.md +20 -3
- data/lib/generators/whatsapp_notifier/install_service_generator.rb +3 -0
- data/lib/whatsapp_notifier/client.rb +20 -0
- data/lib/whatsapp_notifier/providers/web_automation.rb +54 -0
- data/lib/whatsapp_notifier/services/web_automation/history.test.ts +695 -0
- data/lib/whatsapp_notifier/services/web_automation/history.ts +323 -0
- data/lib/whatsapp_notifier/services/web_automation/inbound.test.ts +357 -2
- data/lib/whatsapp_notifier/services/web_automation/inbound.ts +228 -18
- data/lib/whatsapp_notifier/services/web_automation/index.ts +123 -41
- data/lib/whatsapp_notifier/services/web_automation/media.test.ts +751 -0
- data/lib/whatsapp_notifier/services/web_automation/media.ts +548 -0
- data/lib/whatsapp_notifier/services/web_automation/send.test.ts +20 -0
- data/lib/whatsapp_notifier/services/web_automation/send.ts +17 -0
- data/lib/whatsapp_notifier/version.rb +1 -1
- data/lib/whatsapp_notifier/web_adapter.rb +199 -13
- data/lib/whatsapp_notifier.rb +20 -0
- data/spec/client_spec.rb +48 -0
- data/spec/generators/install_service_generator_spec.rb +12 -1
- data/spec/providers/web_automation_spec.rb +97 -0
- data/spec/web_adapter_spec.rb +407 -0
- data/spec/whatsapp_notifier_spec.rb +33 -0
- metadata +7 -1
|
@@ -6,6 +6,36 @@ module WhatsAppNotifier
|
|
|
6
6
|
class WebAdapter
|
|
7
7
|
DEFAULT_OPEN_TIMEOUT = 5
|
|
8
8
|
DEFAULT_READ_TIMEOUT = 30
|
|
9
|
+
# Media bytes can be tens of MB over a slow link — give the binary fetch a
|
|
10
|
+
# longer read window than the JSON control plane.
|
|
11
|
+
MEDIA_OPEN_TIMEOUT = 5
|
|
12
|
+
MEDIA_READ_TIMEOUT = 60
|
|
13
|
+
|
|
14
|
+
HTTP_CLASSES = {
|
|
15
|
+
post: Net::HTTP::Post,
|
|
16
|
+
get: Net::HTTP::Get,
|
|
17
|
+
delete: Net::HTTP::Delete
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
# Optional inbound keys introduced by the 0.7.0 service (media verdict +
|
|
21
|
+
# sender display name) and the 0.8.0 service (two-way capture). Mapped
|
|
22
|
+
# ONLY when the wire payload carries them, so hosts can key-gate on
|
|
23
|
+
# presence: a missing has_media means "0.6.0 service, no media support"
|
|
24
|
+
# (while has_media: false means "text message"), and a missing from_me
|
|
25
|
+
# means "customer message or pre-0.8.0 service". `to` carries the
|
|
26
|
+
# counterparty chat id on operator-sent (from_me) messages — the id the
|
|
27
|
+
# host threads the conversation on.
|
|
28
|
+
INBOUND_OPTIONAL_KEYS = {
|
|
29
|
+
has_media: %w[hasMedia has_media],
|
|
30
|
+
media_status: %w[mediaStatus media_status],
|
|
31
|
+
media_error: %w[mediaError media_error],
|
|
32
|
+
media_mime: %w[mediaMime media_mime],
|
|
33
|
+
media_filename: %w[mediaFilename media_filename],
|
|
34
|
+
media_size: %w[mediaSize media_size],
|
|
35
|
+
sender_name: %w[senderName sender_name],
|
|
36
|
+
to: %w[to],
|
|
37
|
+
from_me: %w[fromMe from_me]
|
|
38
|
+
}.freeze
|
|
9
39
|
|
|
10
40
|
def self.default_base_url
|
|
11
41
|
ENV["WHATSAPP_NOTIFIER_SERVICE_URL"] || ENV["WHATSAPP_SERVICE_URL"] || "http://127.0.0.1:3001"
|
|
@@ -30,7 +60,12 @@ module WhatsAppNotifier
|
|
|
30
60
|
response = request(:post, "/send/#{user_id}", body: body)
|
|
31
61
|
{
|
|
32
62
|
success: response.fetch("success"),
|
|
33
|
-
|
|
63
|
+
# Prefer the service-issued WhatsApp message id (0.8.0): it is the key
|
|
64
|
+
# the host dedupes the send's own fromMe echo on, so a real id must
|
|
65
|
+
# win over the locally fabricated one. The fallback keeps 0.7.0
|
|
66
|
+
# services (no messageId in the response) working unchanged.
|
|
67
|
+
message_id: response["messageId"] || response["message_id"] ||
|
|
68
|
+
payload[:idempotency_key] || "local-#{Time.now.to_i}",
|
|
34
69
|
session: session,
|
|
35
70
|
error_message: response["error"]
|
|
36
71
|
}
|
|
@@ -60,15 +95,88 @@ module WhatsAppNotifier
|
|
|
60
95
|
user_id = user_id_from(metadata)
|
|
61
96
|
response = request(:get, "/inbound/#{user_id}")
|
|
62
97
|
raw = response.is_a?(Hash) ? response["messages"] : response
|
|
63
|
-
Array(raw).map
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
Array(raw).map { |m| map_inbound_message(m) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Fetches the raw bytes of a downloaded inbound media file. Returns
|
|
102
|
+
# { body:, mime:, filename:, size: } or nil when the service has no copy
|
|
103
|
+
# (never downloaded, swept by TTL, or already deleted).
|
|
104
|
+
#
|
|
105
|
+
# Deliberately NOT routed through #request: that path JSON-parses the
|
|
106
|
+
# response body (and host apps are known to patch it further), which would
|
|
107
|
+
# corrupt binary payloads.
|
|
108
|
+
def fetch_media(message_id:, metadata: {})
|
|
109
|
+
user_id = user_id_from(metadata)
|
|
110
|
+
res = binary_get("/media/#{user_id}/#{path_id(message_id)}")
|
|
111
|
+
return nil if res.code.to_s == "404"
|
|
112
|
+
raise "service request failed (#{res.code}): #{res.body}" unless res.is_a?(Net::HTTPSuccess)
|
|
113
|
+
|
|
114
|
+
body = res.body.to_s
|
|
115
|
+
{
|
|
116
|
+
body: body,
|
|
117
|
+
mime: res["Content-Type"],
|
|
118
|
+
filename: filename_from(res["Content-Disposition"]),
|
|
119
|
+
size: body.bytesize
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# On-demand re-download (WhatsApp tap-to-download). The host calls this when
|
|
124
|
+
# an operator opens a media bubble whose bytes the service no longer holds
|
|
125
|
+
# (rolled off by the per-user cap or expired by TTL): the service re-pulls
|
|
126
|
+
# THAT one message's media and stores it, after which the host fetches it
|
|
127
|
+
# with the usual fetch_media GET. Returns { mime:, filename:, size:, status: }
|
|
128
|
+
# on success, or nil when the media is gone upstream (404) — same nil-on-404
|
|
129
|
+
# contract as fetch_media, so a host that gets nil can grey the bubble out.
|
|
130
|
+
# A 0.7.0 service mid-rollout has no /refetch route and also answers 404 →
|
|
131
|
+
# nil, indistinguishable from gone, which is the safe degrade.
|
|
132
|
+
def refetch_media(message_id:, chat_id:, metadata: {})
|
|
133
|
+
user_id = user_id_from(metadata)
|
|
134
|
+
body = { messageId: message_id, chatId: chat_id }
|
|
135
|
+
response = request(:post, "/media/#{user_id}/refetch", body: body, allow_404: true)
|
|
136
|
+
return nil unless response["success"]
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
mime: response["mediaMime"] || response["media_mime"],
|
|
140
|
+
filename: response["mediaFilename"] || response["media_filename"],
|
|
141
|
+
size: response["mediaSize"] || response["media_size"],
|
|
142
|
+
status: response["mediaStatus"] || response["media_status"]
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Removes the service's copy after the host has attached the bytes.
|
|
147
|
+
# Idempotent on the service side: deleting absent media still succeeds.
|
|
148
|
+
# A 0.6.0 service mid-rollout has no /media routes and answers 404 —
|
|
149
|
+
# degrade to { success: false } instead of raising, mirroring
|
|
150
|
+
# fetch_media's nil-on-404.
|
|
151
|
+
def delete_media(message_id:, metadata: {})
|
|
152
|
+
user_id = user_id_from(metadata)
|
|
153
|
+
response = request(:delete, "/media/#{user_id}/#{path_id(message_id)}", allow_404: true)
|
|
154
|
+
{ success: response.fetch("success", false) }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Lists the paired number's 1:1 chats for history-sync discovery. Returns
|
|
158
|
+
# [{ id:, name:, last_message_at: }] newest-first; the service caps the
|
|
159
|
+
# list at its newest 500 and excludes groups/status/privacy chats. The
|
|
160
|
+
# route is token-gated like /media and raises the standard error on any
|
|
161
|
+
# non-2xx (401 when the user never paired or isn't ready).
|
|
162
|
+
def list_chats(metadata: {})
|
|
163
|
+
user_id = user_id_from(metadata)
|
|
164
|
+
response = request(:get, "/chats/#{user_id}")
|
|
165
|
+
Array(response["chats"]).map { |chat| map_chat_summary(chat) }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Replays one chat's history through the service's live-capture
|
|
169
|
+
# normalizer and returns it synchronously (no queue, no webhook) —
|
|
170
|
+
# oldest-first, mapped exactly like fetch_inbound messages, including
|
|
171
|
+
# from_me/to on the operator's side of the conversation. History media
|
|
172
|
+
# arrives marked unavailable by design (media_error "history"): the
|
|
173
|
+
# service never bulk-downloads old media; live capture handles bytes
|
|
174
|
+
# going forward.
|
|
175
|
+
def fetch_history(chat_id:, limit: 50, metadata: {})
|
|
176
|
+
user_id = user_id_from(metadata)
|
|
177
|
+
body = { chatId: chat_id, limit: clamp_history_limit(limit) }
|
|
178
|
+
response = request(:post, "/history/#{user_id}", body: body)
|
|
179
|
+
Array(response["messages"]).map { |m| map_inbound_message(m) }
|
|
72
180
|
end
|
|
73
181
|
|
|
74
182
|
# Logs the user out of WhatsApp and clears their saved session on the service.
|
|
@@ -78,24 +186,102 @@ module WhatsAppNotifier
|
|
|
78
186
|
{ success: response.fetch("success", false) }
|
|
79
187
|
end
|
|
80
188
|
|
|
189
|
+
# Mirrors the service-side clamp (history.ts) so a host-passed limit can
|
|
190
|
+
# never balloon one request into a session-stalling bulk fetch.
|
|
191
|
+
HISTORY_LIMIT_DEFAULT = 50
|
|
192
|
+
HISTORY_LIMIT_RANGE = (1..200).freeze
|
|
193
|
+
|
|
81
194
|
private
|
|
82
195
|
|
|
83
196
|
def user_id_from(metadata)
|
|
84
197
|
(metadata[:user_id] || metadata["user_id"] || "default").to_s
|
|
85
198
|
end
|
|
86
199
|
|
|
87
|
-
def
|
|
200
|
+
def clamp_history_limit(limit)
|
|
201
|
+
Integer(limit).clamp(HISTORY_LIMIT_RANGE.min, HISTORY_LIMIT_RANGE.max)
|
|
202
|
+
rescue ArgumentError, TypeError
|
|
203
|
+
# Non-integer garbage falls back to the default — the service does the
|
|
204
|
+
# same, so both layers agree on the effective page size.
|
|
205
|
+
HISTORY_LIMIT_DEFAULT
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def map_chat_summary(chat)
|
|
209
|
+
{
|
|
210
|
+
id: chat["id"],
|
|
211
|
+
name: chat["name"],
|
|
212
|
+
last_message_at: chat.key?("lastMessageAt") ? chat["lastMessageAt"] : chat["last_message_at"]
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def map_inbound_message(message)
|
|
217
|
+
mapped = {
|
|
218
|
+
from: message["from"],
|
|
219
|
+
body: message["body"],
|
|
220
|
+
message_id: message["messageId"] || message["message_id"],
|
|
221
|
+
timestamp: message["timestamp"],
|
|
222
|
+
type: message["type"]
|
|
223
|
+
}
|
|
224
|
+
INBOUND_OPTIONAL_KEYS.each do |key, wire_keys|
|
|
225
|
+
wire = wire_keys.find { |candidate| message.key?(candidate) }
|
|
226
|
+
mapped[key] = message[wire] if wire
|
|
227
|
+
end
|
|
228
|
+
mapped
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Mirror the service-side sanitizeId charset so a hostile message_id can
|
|
232
|
+
# never smuggle path separators or a query string into the request URL.
|
|
233
|
+
def path_id(message_id)
|
|
234
|
+
message_id.to_s.gsub(/[^A-Za-z0-9@._-]/, "")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def filename_from(content_disposition)
|
|
238
|
+
content_disposition.to_s[/filename="([^"]*)"/, 1]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# The /media routes are token-gated when the service has
|
|
242
|
+
# WHATSAPP_WEBHOOK_TOKEN set — the same shared secret the service uses to
|
|
243
|
+
# sign its webhook pushes, reused in the other direction.
|
|
244
|
+
def webhook_token
|
|
245
|
+
token = ENV["WHATSAPP_WEBHOOK_TOKEN"].to_s
|
|
246
|
+
token.empty? ? nil : token
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Net::HTTP does NOT infer TLS from the URL scheme — without an explicit
|
|
250
|
+
# use_ssl a https:// service URL would silently speak plaintext to port
|
|
251
|
+
# 443. Both request paths (JSON control plane + binary media fetch) must
|
|
252
|
+
# honor the scheme.
|
|
253
|
+
def use_ssl?(uri)
|
|
254
|
+
uri.scheme == "https"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def binary_get(path)
|
|
258
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
259
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
260
|
+
req["X-WA-Token"] = webhook_token if webhook_token
|
|
261
|
+
|
|
262
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
263
|
+
use_ssl: use_ssl?(uri),
|
|
264
|
+
open_timeout: MEDIA_OPEN_TIMEOUT,
|
|
265
|
+
read_timeout: MEDIA_READ_TIMEOUT) { |http| http.request(req) }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def request(method, path, body: nil, allow_404: false)
|
|
88
269
|
uri = URI.parse("#{@base_url}#{path}")
|
|
89
|
-
|
|
90
|
-
req = klass.new(uri.request_uri)
|
|
270
|
+
req = HTTP_CLASSES.fetch(method).new(uri.request_uri)
|
|
91
271
|
req["Content-Type"] = "application/json"
|
|
272
|
+
req["X-WA-Token"] = webhook_token if webhook_token
|
|
92
273
|
req.body = JSON.generate(body) if body
|
|
93
274
|
|
|
94
275
|
res = Net::HTTP.start(uri.host, uri.port,
|
|
276
|
+
use_ssl: use_ssl?(uri),
|
|
95
277
|
open_timeout: @open_timeout,
|
|
96
278
|
read_timeout: @read_timeout) { |http| http.request(req) }
|
|
97
279
|
parsed = parse_body(res.body)
|
|
98
280
|
return parsed if res.is_a?(Net::HTTPSuccess)
|
|
281
|
+
# Callers opting in treat "route/resource not there" as a soft miss
|
|
282
|
+
# (e.g. delete_media against a 0.6.0 service) — the parsed error body
|
|
283
|
+
# carries no "success" key, so they degrade rather than raise.
|
|
284
|
+
return parsed if allow_404 && res.code.to_s == "404"
|
|
99
285
|
|
|
100
286
|
raise "service request failed (#{res.code}): #{parsed["error"] || res.body}"
|
|
101
287
|
end
|
data/lib/whatsapp_notifier.rb
CHANGED
|
@@ -66,6 +66,26 @@ module WhatsAppNotifier
|
|
|
66
66
|
client.fetch_inbound(provider: provider, metadata: metadata)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
def fetch_media(message_id:, provider: nil, metadata: {})
|
|
70
|
+
client.fetch_media(message_id: message_id, provider: provider, metadata: metadata)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def delete_media(message_id:, provider: nil, metadata: {})
|
|
74
|
+
client.delete_media(message_id: message_id, provider: provider, metadata: metadata)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def refetch_media(message_id:, chat_id:, provider: nil, metadata: {})
|
|
78
|
+
client.refetch_media(message_id: message_id, chat_id: chat_id, provider: provider, metadata: metadata)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def list_chats(provider: nil, metadata: {})
|
|
82
|
+
client.list_chats(provider: provider, metadata: metadata)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fetch_history(chat_id:, limit: 50, provider: nil, metadata: {})
|
|
86
|
+
client.fetch_history(chat_id: chat_id, limit: limit, provider: provider, metadata: metadata)
|
|
87
|
+
end
|
|
88
|
+
|
|
69
89
|
def logout(provider: nil, metadata: {})
|
|
70
90
|
client.logout(provider: provider, metadata: metadata)
|
|
71
91
|
end
|
data/spec/client_spec.rb
CHANGED
|
@@ -67,6 +67,54 @@ RSpec.describe WhatsAppNotifier::Client do
|
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
it "delegates fetch_media and delete_media to the provider" do
|
|
71
|
+
Dir.mktmpdir do |dir|
|
|
72
|
+
config.provider = :web_automation
|
|
73
|
+
config.web_automation_enabled = true
|
|
74
|
+
config.web_session_path = File.join(dir, "session.json")
|
|
75
|
+
config.web_adapter = double(
|
|
76
|
+
send_message: { success: true, session: {} },
|
|
77
|
+
fetch_qr_code: "qr",
|
|
78
|
+
connection_status: { state: "AUTHENTICATED", authenticated: true },
|
|
79
|
+
fetch_media: { body: "bytes", mime: "image/jpeg", filename: nil, size: 5 },
|
|
80
|
+
delete_media: { success: true },
|
|
81
|
+
refetch_media: { mime: "image/jpeg", filename: nil, size: 5, status: "available" }
|
|
82
|
+
)
|
|
83
|
+
client = described_class.new(configuration: config)
|
|
84
|
+
|
|
85
|
+
expect(client.fetch_media(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 }))
|
|
86
|
+
.to include(body: "bytes", size: 5)
|
|
87
|
+
expect(client.delete_media(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 }))
|
|
88
|
+
.to eq(success: true)
|
|
89
|
+
expect(client.refetch_media(message_id: "m1", chat_id: "919@c.us", provider: :web_automation, metadata: { user_id: 1 }))
|
|
90
|
+
.to include(status: "available")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "delegates list_chats and fetch_history to the provider" do
|
|
95
|
+
Dir.mktmpdir do |dir|
|
|
96
|
+
config.provider = :web_automation
|
|
97
|
+
config.web_automation_enabled = true
|
|
98
|
+
config.web_session_path = File.join(dir, "session.json")
|
|
99
|
+
adapter = double(
|
|
100
|
+
send_message: { success: true, session: {} },
|
|
101
|
+
fetch_qr_code: "qr",
|
|
102
|
+
connection_status: { state: "AUTHENTICATED", authenticated: true },
|
|
103
|
+
list_chats: [{ id: "919@c.us", name: "Asha", last_message_at: 9 }],
|
|
104
|
+
fetch_history: [{ from: "919@c.us", body: "old", message_id: "h1" }]
|
|
105
|
+
)
|
|
106
|
+
config.web_adapter = adapter
|
|
107
|
+
client = described_class.new(configuration: config)
|
|
108
|
+
|
|
109
|
+
expect(client.list_chats(provider: :web_automation, metadata: { user_id: 1 }))
|
|
110
|
+
.to eq([{ id: "919@c.us", name: "Asha", last_message_at: 9 }])
|
|
111
|
+
expect(client.fetch_history(chat_id: "919@c.us", provider: :web_automation, metadata: { user_id: 1 }))
|
|
112
|
+
.to eq([{ from: "919@c.us", body: "old", message_id: "h1" }])
|
|
113
|
+
# The default page size survives the delegation chain untouched.
|
|
114
|
+
expect(adapter).to have_received(:fetch_history).with(chat_id: "919@c.us", limit: 50, metadata: { user_id: 1 })
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
70
118
|
it "delegates logout to the provider" do
|
|
71
119
|
Dir.mktmpdir do |dir|
|
|
72
120
|
config.provider = :web_automation
|
|
@@ -58,7 +58,7 @@ RSpec.describe "WhatsAppNotifier::Generators::InstallServiceGenerator" do
|
|
|
58
58
|
generator.copy_service_files
|
|
59
59
|
|
|
60
60
|
sources = generator.copied.map(&:first)
|
|
61
|
-
expect(sources).to match_array(%w[index.ts inbound.ts init_gate.ts metrics.ts sessions.ts package.json bun.lock])
|
|
61
|
+
expect(sources).to match_array(%w[index.ts history.ts inbound.ts init_gate.ts media.ts metrics.ts send.ts sessions.ts package.json bun.lock])
|
|
62
62
|
expect(sources.grep(/test|node_modules|\.wwebjs|\.puppeteer/)).to be_empty
|
|
63
63
|
expect(generator.copied.map(&:last)).to all(start_with("whatsapp_service/"))
|
|
64
64
|
end
|
|
@@ -69,6 +69,17 @@ RSpec.describe "WhatsAppNotifier::Generators::InstallServiceGenerator" do
|
|
|
69
69
|
"SERVICE_FILES lists #{file} but it is missing from the service dir"
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
|
+
|
|
73
|
+
# An ejected service that is missing a module index.ts imports dies on
|
|
74
|
+
# boot — pin the file list to the imports so a new module (like media.ts
|
|
75
|
+
# in 0.7.0) can never be forgotten again.
|
|
76
|
+
it "ships every local module index.ts imports" do
|
|
77
|
+
index = File.read(File.join(generator_class.source_root, "index.ts"), encoding: "UTF-8")
|
|
78
|
+
local_imports = index.scan(%r{from '\./([A-Za-z0-9_]+)'}).flatten.map { |name| "#{name}.ts" }
|
|
79
|
+
|
|
80
|
+
expect(local_imports).not_to be_empty
|
|
81
|
+
expect(generator_class.const_get(:SERVICE_FILES)).to include(*local_imports)
|
|
82
|
+
end
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
describe "#add_to_gitignore" do
|
|
@@ -137,6 +137,103 @@ RSpec.describe WhatsAppNotifier::Providers::WebAutomation do
|
|
|
137
137
|
end
|
|
138
138
|
end
|
|
139
139
|
|
|
140
|
+
it "fetches, deletes and refetches media via the adapter when enabled" do
|
|
141
|
+
Dir.mktmpdir do |dir|
|
|
142
|
+
adapter = double(
|
|
143
|
+
fetch_qr_code: "qr", connection_status: {},
|
|
144
|
+
fetch_media: { body: "bytes", mime: "image/jpeg", filename: "a.jpg", size: 5 },
|
|
145
|
+
delete_media: { success: true },
|
|
146
|
+
refetch_media: { mime: "image/jpeg", filename: "a.jpg", size: 5, status: "available" }
|
|
147
|
+
)
|
|
148
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
|
|
149
|
+
provider = described_class.new(configuration: config)
|
|
150
|
+
|
|
151
|
+
expect(provider.fetch_media(message_id: "m1", metadata: { user_id: 1 })).to include(mime: "image/jpeg")
|
|
152
|
+
expect(provider.delete_media(message_id: "m1", metadata: { user_id: 1 })).to eq(success: true)
|
|
153
|
+
expect(provider.refetch_media(message_id: "m1", chat_id: "919@c.us", metadata: { user_id: 1 }))
|
|
154
|
+
.to include(status: "available")
|
|
155
|
+
expect(adapter).to have_received(:fetch_media).with(message_id: "m1", metadata: { user_id: 1 })
|
|
156
|
+
expect(adapter).to have_received(:delete_media).with(message_id: "m1", metadata: { user_id: 1 })
|
|
157
|
+
expect(adapter).to have_received(:refetch_media).with(message_id: "m1", chat_id: "919@c.us", metadata: { user_id: 1 })
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "raises on the media helpers when the provider is disabled" do
|
|
162
|
+
Dir.mktmpdir do |dir|
|
|
163
|
+
adapter = double(fetch_qr_code: "qr", connection_status: {}, fetch_media: nil, delete_media: { success: true }, refetch_media: nil)
|
|
164
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter, enabled: false)
|
|
165
|
+
provider = described_class.new(configuration: config)
|
|
166
|
+
|
|
167
|
+
expect { provider.fetch_media(message_id: "m1") }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
|
|
168
|
+
expect { provider.delete_media(message_id: "m1") }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
|
|
169
|
+
expect { provider.refetch_media(message_id: "m1", chat_id: "919@c.us") }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "raises on the media helpers when the adapter lacks media support" do
|
|
174
|
+
Dir.mktmpdir do |dir|
|
|
175
|
+
adapter = double(fetch_qr_code: "qr", connection_status: {})
|
|
176
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
|
|
177
|
+
provider = described_class.new(configuration: config)
|
|
178
|
+
|
|
179
|
+
expect { provider.fetch_media(message_id: "m1") }.to raise_error(WhatsAppNotifier::ConfigurationError, /media fetch/)
|
|
180
|
+
expect { provider.delete_media(message_id: "m1") }.to raise_error(WhatsAppNotifier::ConfigurationError, /media deletion/)
|
|
181
|
+
expect { provider.refetch_media(message_id: "m1", chat_id: "919@c.us") }.to raise_error(WhatsAppNotifier::ConfigurationError, /media refetch/)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "lists chats and fetches history via the adapter when enabled" do
|
|
186
|
+
Dir.mktmpdir do |dir|
|
|
187
|
+
adapter = double(
|
|
188
|
+
fetch_qr_code: "qr", connection_status: {},
|
|
189
|
+
list_chats: [{ id: "919@c.us", name: "Asha", last_message_at: 9 }],
|
|
190
|
+
fetch_history: [{ from: "919@c.us", body: "old", message_id: "h1" }]
|
|
191
|
+
)
|
|
192
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
|
|
193
|
+
provider = described_class.new(configuration: config)
|
|
194
|
+
|
|
195
|
+
expect(provider.list_chats(metadata: { user_id: 1 })).to eq([{ id: "919@c.us", name: "Asha", last_message_at: 9 }])
|
|
196
|
+
expect(provider.fetch_history(chat_id: "919@c.us", limit: 20, metadata: { user_id: 1 }))
|
|
197
|
+
.to eq([{ from: "919@c.us", body: "old", message_id: "h1" }])
|
|
198
|
+
expect(adapter).to have_received(:list_chats).with(metadata: { user_id: 1 })
|
|
199
|
+
expect(adapter).to have_received(:fetch_history).with(chat_id: "919@c.us", limit: 20, metadata: { user_id: 1 })
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "defaults the history limit to 50 through the provider" do
|
|
204
|
+
Dir.mktmpdir do |dir|
|
|
205
|
+
adapter = double(fetch_qr_code: "qr", connection_status: {}, fetch_history: [])
|
|
206
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
|
|
207
|
+
provider = described_class.new(configuration: config)
|
|
208
|
+
|
|
209
|
+
provider.fetch_history(chat_id: "919@c.us")
|
|
210
|
+
|
|
211
|
+
expect(adapter).to have_received(:fetch_history).with(chat_id: "919@c.us", limit: 50, metadata: {})
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "raises on the history helpers when the provider is disabled" do
|
|
216
|
+
Dir.mktmpdir do |dir|
|
|
217
|
+
adapter = double(fetch_qr_code: "qr", connection_status: {}, list_chats: [], fetch_history: [])
|
|
218
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter, enabled: false)
|
|
219
|
+
provider = described_class.new(configuration: config)
|
|
220
|
+
|
|
221
|
+
expect { provider.list_chats }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
|
|
222
|
+
expect { provider.fetch_history(chat_id: "919@c.us") }.to raise_error(WhatsAppNotifier::ConfigurationError, /disabled/)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it "raises on the history helpers when the adapter lacks history support" do
|
|
227
|
+
Dir.mktmpdir do |dir|
|
|
228
|
+
adapter = double(fetch_qr_code: "qr", connection_status: {})
|
|
229
|
+
config = build_config(path: File.join(dir, "session.json"), adapter: adapter)
|
|
230
|
+
provider = described_class.new(configuration: config)
|
|
231
|
+
|
|
232
|
+
expect { provider.list_chats }.to raise_error(WhatsAppNotifier::ConfigurationError, /chat listing/)
|
|
233
|
+
expect { provider.fetch_history(chat_id: "919@c.us") }.to raise_error(WhatsAppNotifier::ConfigurationError, /history replay/)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
140
237
|
it "logs out via adapter when enabled" do
|
|
141
238
|
Dir.mktmpdir do |dir|
|
|
142
239
|
adapter = double(fetch_qr_code: "qr", connection_status: {}, logout: { success: true })
|