heathrow 0.7.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 +7 -0
- data/.gitignore +58 -0
- data/README.md +205 -0
- data/bin/heathrow +42 -0
- data/bin/heathrowd +283 -0
- data/docs/ARCHITECTURE.md +1172 -0
- data/docs/DATABASE_SCHEMA.md +685 -0
- data/docs/DEVELOPMENT_WORKFLOW.md +867 -0
- data/docs/DISCORD_SETUP.md +142 -0
- data/docs/GMAIL_OAUTH_SETUP.md +120 -0
- data/docs/PLUGIN_SYSTEM.md +1370 -0
- data/docs/PROJECT_PLAN.md +1022 -0
- data/docs/README.md +417 -0
- data/docs/REDDIT_SETUP.md +174 -0
- data/docs/REPLY_FORWARD.md +182 -0
- data/docs/WHATSAPP_TELEGRAM_SETUP.md +306 -0
- data/heathrow.gemspec +34 -0
- data/heathrowd.service +21 -0
- data/img/heathrow.svg +95 -0
- data/img/rss_threaded.png +0 -0
- data/img/sources.png +0 -0
- data/lib/heathrow/address_book.rb +42 -0
- data/lib/heathrow/config.rb +332 -0
- data/lib/heathrow/database.rb +731 -0
- data/lib/heathrow/database_new.rb +392 -0
- data/lib/heathrow/event_bus.rb +175 -0
- data/lib/heathrow/logger.rb +122 -0
- data/lib/heathrow/message.rb +176 -0
- data/lib/heathrow/message_composer.rb +399 -0
- data/lib/heathrow/message_organizer.rb +774 -0
- data/lib/heathrow/migrations/001_initial_schema.rb +248 -0
- data/lib/heathrow/notmuch.rb +45 -0
- data/lib/heathrow/oauth2_smtp.rb +254 -0
- data/lib/heathrow/plugin/base.rb +212 -0
- data/lib/heathrow/plugin_manager.rb +141 -0
- data/lib/heathrow/poller.rb +93 -0
- data/lib/heathrow/smtp_sender.rb +204 -0
- data/lib/heathrow/source.rb +39 -0
- data/lib/heathrow/sources/base.rb +74 -0
- data/lib/heathrow/sources/discord.rb +357 -0
- data/lib/heathrow/sources/gmail.rb +294 -0
- data/lib/heathrow/sources/imap.rb +198 -0
- data/lib/heathrow/sources/instagram.rb +307 -0
- data/lib/heathrow/sources/instagram_fetch.py +101 -0
- data/lib/heathrow/sources/instagram_send.py +55 -0
- data/lib/heathrow/sources/instagram_send_marionette.py +104 -0
- data/lib/heathrow/sources/maildir.rb +606 -0
- data/lib/heathrow/sources/messenger.rb +212 -0
- data/lib/heathrow/sources/messenger_fetch.js +297 -0
- data/lib/heathrow/sources/messenger_fetch_marionette.py +138 -0
- data/lib/heathrow/sources/messenger_send.js +32 -0
- data/lib/heathrow/sources/messenger_send.py +100 -0
- data/lib/heathrow/sources/reddit.rb +461 -0
- data/lib/heathrow/sources/rss.rb +299 -0
- data/lib/heathrow/sources/slack.rb +375 -0
- data/lib/heathrow/sources/source_manager.rb +328 -0
- data/lib/heathrow/sources/telegram.rb +498 -0
- data/lib/heathrow/sources/webpage.rb +207 -0
- data/lib/heathrow/sources/weechat.rb +479 -0
- data/lib/heathrow/sources/whatsapp.rb +474 -0
- data/lib/heathrow/ui/application.rb +8098 -0
- data/lib/heathrow/ui/navigation.rb +8 -0
- data/lib/heathrow/ui/panes.rb +8 -0
- data/lib/heathrow/ui/source_wizard.rb +567 -0
- data/lib/heathrow/ui/threaded_view.rb +780 -0
- data/lib/heathrow/ui/views.rb +8 -0
- data/lib/heathrow/version.rb +3 -0
- data/lib/heathrow/wizards/discord_wizard.rb +193 -0
- data/lib/heathrow/wizards/slack_wizard.rb +140 -0
- data/lib/heathrow.rb +55 -0
- metadata +147 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# WhatsApp source using WAHA (WhatsApp HTTP API)
|
|
5
|
+
# Requires: docker run -p 3000:3000 devlikeapro/waha
|
|
6
|
+
|
|
7
|
+
require 'net/http'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'uri'
|
|
10
|
+
require 'base64'
|
|
11
|
+
require 'time'
|
|
12
|
+
|
|
13
|
+
module Heathrow
|
|
14
|
+
module Sources
|
|
15
|
+
class Whatsapp
|
|
16
|
+
attr_reader :source, :last_fetch_time
|
|
17
|
+
|
|
18
|
+
DEFAULT_API_URL = 'http://localhost:3000'
|
|
19
|
+
DEFAULT_SESSION = 'default'
|
|
20
|
+
|
|
21
|
+
def initialize(source)
|
|
22
|
+
@source = source
|
|
23
|
+
@config = source.config.is_a?(String) ? JSON.parse(source.config) : source.config
|
|
24
|
+
@last_fetch_time = Time.now
|
|
25
|
+
@session = @config['session'] || DEFAULT_SESSION
|
|
26
|
+
@api_url = @config['api_url'] || DEFAULT_API_URL
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fetch_messages
|
|
30
|
+
messages = []
|
|
31
|
+
|
|
32
|
+
begin
|
|
33
|
+
unless authenticated?
|
|
34
|
+
puts "WhatsApp not authenticated. Run setup first." if ENV['DEBUG']
|
|
35
|
+
return messages
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Get list of chats
|
|
39
|
+
chats = fetch_chats
|
|
40
|
+
return messages if chats.empty?
|
|
41
|
+
|
|
42
|
+
# Fetch recent messages from each chat
|
|
43
|
+
limit = @config['fetch_limit'] || 20
|
|
44
|
+
chats.each do |chat|
|
|
45
|
+
chat_id = chat['id']
|
|
46
|
+
chat_messages = fetch_chat_messages(chat_id, limit)
|
|
47
|
+
|
|
48
|
+
chat_messages.each do |msg|
|
|
49
|
+
message = convert_to_heathrow_message(msg, chat)
|
|
50
|
+
messages << message if message
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@last_fetch_time = Time.now
|
|
55
|
+
|
|
56
|
+
rescue => e
|
|
57
|
+
puts "WhatsApp fetch error: #{e.message}" if ENV['DEBUG']
|
|
58
|
+
puts e.backtrace.join("\n") if ENV['DEBUG']
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
messages
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_connection
|
|
65
|
+
begin
|
|
66
|
+
# Check session status
|
|
67
|
+
uri = URI("#{@api_url}/api/sessions/#{@session}")
|
|
68
|
+
response = Net::HTTP.get_response(uri)
|
|
69
|
+
|
|
70
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
71
|
+
data = JSON.parse(response.body)
|
|
72
|
+
status = data['status']
|
|
73
|
+
|
|
74
|
+
case status
|
|
75
|
+
when 'WORKING'
|
|
76
|
+
me = data.dig('me', 'id') || 'Unknown'
|
|
77
|
+
phone = me.split('@').first
|
|
78
|
+
{ success: true, message: "Connected as +#{phone}" }
|
|
79
|
+
when 'SCAN_QR_CODE'
|
|
80
|
+
{ success: false, message: "Session needs QR code scan. Run setup." }
|
|
81
|
+
when 'STARTING'
|
|
82
|
+
{ success: false, message: "Session is starting..." }
|
|
83
|
+
when 'STOPPED'
|
|
84
|
+
{ success: false, message: "Session stopped. Run setup to start." }
|
|
85
|
+
else
|
|
86
|
+
{ success: false, message: "Session status: #{status}" }
|
|
87
|
+
end
|
|
88
|
+
elsif response.code == '404'
|
|
89
|
+
{ success: false, message: "Session '#{@session}' not found. Run setup." }
|
|
90
|
+
else
|
|
91
|
+
{ success: false, message: "API error: #{response.code}" }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
rescue Errno::ECONNREFUSED
|
|
95
|
+
{ success: false, message: "WAHA not running. Start with: docker run -p 3000:3000 devlikeapro/waha" }
|
|
96
|
+
rescue => e
|
|
97
|
+
{ success: false, message: "Connection failed: #{e.message}" }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def can_reply?
|
|
102
|
+
authenticated?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def send_message(to, subject, body, in_reply_to = nil)
|
|
106
|
+
unless can_reply?
|
|
107
|
+
return { success: false, message: "WhatsApp not authenticated" }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
chat_id = format_chat_id(to)
|
|
112
|
+
|
|
113
|
+
payload = {
|
|
114
|
+
session: @session,
|
|
115
|
+
chatId: chat_id,
|
|
116
|
+
text: body
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Add reply context if replying
|
|
120
|
+
payload[:reply_to] = in_reply_to if in_reply_to
|
|
121
|
+
|
|
122
|
+
uri = URI("#{@api_url}/api/sendText")
|
|
123
|
+
response = post_json(uri, payload)
|
|
124
|
+
|
|
125
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
126
|
+
{ success: true, message: "Message sent to #{to}" }
|
|
127
|
+
else
|
|
128
|
+
error = parse_error(response)
|
|
129
|
+
{ success: false, message: "Failed to send: #{error}" }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
rescue Errno::ECONNREFUSED
|
|
133
|
+
{ success: false, message: "WAHA not running" }
|
|
134
|
+
rescue => e
|
|
135
|
+
{ success: false, message: "Send failed: #{e.message}" }
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def send_media(to, file_path, caption = nil)
|
|
140
|
+
unless can_reply?
|
|
141
|
+
return { success: false, message: "WhatsApp not authenticated" }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
chat_id = format_chat_id(to)
|
|
146
|
+
mime_type = detect_mime_type(file_path)
|
|
147
|
+
|
|
148
|
+
# Determine endpoint based on media type
|
|
149
|
+
endpoint = case mime_type
|
|
150
|
+
when /^image/ then '/api/sendImage'
|
|
151
|
+
when /^video/ then '/api/sendVideo'
|
|
152
|
+
when /^audio/ then '/api/sendVoice'
|
|
153
|
+
else '/api/sendFile'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Read and encode file
|
|
157
|
+
file_data = Base64.strict_encode64(File.binread(file_path))
|
|
158
|
+
|
|
159
|
+
payload = {
|
|
160
|
+
session: @session,
|
|
161
|
+
chatId: chat_id,
|
|
162
|
+
file: {
|
|
163
|
+
mimetype: mime_type,
|
|
164
|
+
filename: File.basename(file_path),
|
|
165
|
+
data: file_data
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
payload[:caption] = caption if caption
|
|
169
|
+
|
|
170
|
+
uri = URI("#{@api_url}#{endpoint}")
|
|
171
|
+
response = post_json(uri, payload)
|
|
172
|
+
|
|
173
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
174
|
+
{ success: true, message: "Media sent to #{to}" }
|
|
175
|
+
else
|
|
176
|
+
error = parse_error(response)
|
|
177
|
+
{ success: false, message: "Failed to send media: #{error}" }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
rescue => e
|
|
181
|
+
{ success: false, message: "Send media failed: #{e.message}" }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def authenticate
|
|
186
|
+
begin
|
|
187
|
+
# Create session if it doesn't exist
|
|
188
|
+
unless session_exists?
|
|
189
|
+
puts "Creating WhatsApp session '#{@session}'..."
|
|
190
|
+
create_session
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Start session if stopped
|
|
194
|
+
status = get_session_status
|
|
195
|
+
if status == 'STOPPED'
|
|
196
|
+
start_session
|
|
197
|
+
sleep(2)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Get QR code and wait for scan
|
|
201
|
+
authenticate_with_qr_code
|
|
202
|
+
|
|
203
|
+
rescue => e
|
|
204
|
+
puts "Authentication error: #{e.message}"
|
|
205
|
+
false
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def post_configure
|
|
210
|
+
if authenticated?
|
|
211
|
+
puts "WhatsApp already authenticated!"
|
|
212
|
+
return true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
puts "\nWhatsApp requires authentication via QR code."
|
|
216
|
+
print "Would you like to authenticate now? (y/n): "
|
|
217
|
+
response = gets.chomp.downcase
|
|
218
|
+
|
|
219
|
+
if response == 'y'
|
|
220
|
+
authenticate
|
|
221
|
+
else
|
|
222
|
+
puts "You can authenticate later by running: ruby setup_whatsapp.rb"
|
|
223
|
+
true
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def authenticated?
|
|
230
|
+
status = get_session_status
|
|
231
|
+
status == 'WORKING'
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def session_exists?
|
|
235
|
+
uri = URI("#{@api_url}/api/sessions/#{@session}")
|
|
236
|
+
response = Net::HTTP.get_response(uri)
|
|
237
|
+
response.is_a?(Net::HTTPSuccess)
|
|
238
|
+
rescue
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def get_session_status
|
|
243
|
+
uri = URI("#{@api_url}/api/sessions/#{@session}")
|
|
244
|
+
response = Net::HTTP.get_response(uri)
|
|
245
|
+
|
|
246
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
247
|
+
data = JSON.parse(response.body)
|
|
248
|
+
data['status']
|
|
249
|
+
else
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
rescue
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def create_session
|
|
257
|
+
uri = URI("#{@api_url}/api/sessions")
|
|
258
|
+
payload = { name: @session }
|
|
259
|
+
post_json(uri, payload)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def start_session
|
|
263
|
+
uri = URI("#{@api_url}/api/sessions/#{@session}/start")
|
|
264
|
+
post_json(uri, {})
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def authenticate_with_qr_code
|
|
268
|
+
puts "\n=== WhatsApp QR Code Authentication ==="
|
|
269
|
+
puts "1. Open WhatsApp on your phone"
|
|
270
|
+
puts "2. Go to Settings > Linked Devices"
|
|
271
|
+
puts "3. Tap 'Link a Device'"
|
|
272
|
+
puts "4. Scan the QR code\n\n"
|
|
273
|
+
|
|
274
|
+
max_attempts = 60
|
|
275
|
+
attempt = 0
|
|
276
|
+
last_qr = nil
|
|
277
|
+
|
|
278
|
+
while attempt < max_attempts
|
|
279
|
+
status = get_session_status
|
|
280
|
+
|
|
281
|
+
case status
|
|
282
|
+
when 'WORKING'
|
|
283
|
+
puts "\n[OK] Successfully authenticated!"
|
|
284
|
+
return true
|
|
285
|
+
when 'SCAN_QR_CODE'
|
|
286
|
+
qr = fetch_qr_code
|
|
287
|
+
if qr && qr != last_qr
|
|
288
|
+
display_qr_terminal(qr)
|
|
289
|
+
last_qr = qr
|
|
290
|
+
end
|
|
291
|
+
when 'STARTING'
|
|
292
|
+
print "."
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
sleep(2)
|
|
296
|
+
attempt += 1
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
puts "\nAuthentication timeout. Please try again."
|
|
300
|
+
false
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def fetch_qr_code
|
|
304
|
+
uri = URI("#{@api_url}/api/#{@session}/auth/qr?format=raw")
|
|
305
|
+
response = Net::HTTP.get_response(uri)
|
|
306
|
+
|
|
307
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
308
|
+
data = JSON.parse(response.body)
|
|
309
|
+
data['value']
|
|
310
|
+
else
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
rescue
|
|
314
|
+
nil
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def display_qr_terminal(qr_string)
|
|
318
|
+
# Use qrencode if available, otherwise print raw
|
|
319
|
+
begin
|
|
320
|
+
require 'rqrcode'
|
|
321
|
+
qr = RQRCode::QRCode.new(qr_string)
|
|
322
|
+
puts qr.as_ansi(
|
|
323
|
+
light: "\e[47m", dark: "\e[40m",
|
|
324
|
+
fill_character: ' ',
|
|
325
|
+
quiet_zone_size: 1
|
|
326
|
+
)
|
|
327
|
+
rescue LoadError
|
|
328
|
+
# Fallback: try system qrencode
|
|
329
|
+
result = `echo '#{qr_string}' | qrencode -t ANSIUTF8 2>/dev/null`
|
|
330
|
+
if $?.success?
|
|
331
|
+
puts result
|
|
332
|
+
else
|
|
333
|
+
puts "QR Data: #{qr_string}"
|
|
334
|
+
puts "\nInstall 'rqrcode' gem or 'qrencode' for visual QR code"
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def fetch_chats
|
|
340
|
+
uri = URI("#{@api_url}/api/#{@session}/chats?limit=50")
|
|
341
|
+
response = Net::HTTP.get_response(uri)
|
|
342
|
+
|
|
343
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
344
|
+
JSON.parse(response.body)
|
|
345
|
+
else
|
|
346
|
+
[]
|
|
347
|
+
end
|
|
348
|
+
rescue
|
|
349
|
+
[]
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def fetch_chat_messages(chat_id, limit = 20)
|
|
353
|
+
encoded_chat_id = URI.encode_www_form_component(chat_id)
|
|
354
|
+
uri = URI("#{@api_url}/api/#{@session}/chats/#{encoded_chat_id}/messages?limit=#{limit}")
|
|
355
|
+
response = Net::HTTP.get_response(uri)
|
|
356
|
+
|
|
357
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
358
|
+
JSON.parse(response.body)
|
|
359
|
+
else
|
|
360
|
+
[]
|
|
361
|
+
end
|
|
362
|
+
rescue
|
|
363
|
+
[]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def convert_to_heathrow_message(msg, chat)
|
|
367
|
+
sender_id = msg['from'] || msg['_data']&.dig('from')
|
|
368
|
+
sender = format_phone_number(sender_id)
|
|
369
|
+
|
|
370
|
+
# Get chat name
|
|
371
|
+
chat_name = chat['name'] || chat['id']&.split('@')&.first || 'Unknown'
|
|
372
|
+
|
|
373
|
+
content = msg['body'] || ''
|
|
374
|
+
subject = content[0..50]
|
|
375
|
+
subject += "..." if content.length > 50
|
|
376
|
+
|
|
377
|
+
# Handle timestamps
|
|
378
|
+
timestamp = if msg['timestamp']
|
|
379
|
+
Time.at(msg['timestamp']).iso8601
|
|
380
|
+
else
|
|
381
|
+
Time.now.iso8601
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
{
|
|
385
|
+
source_id: @source.id,
|
|
386
|
+
source_type: 'whatsapp',
|
|
387
|
+
external_id: "whatsapp_#{msg['id']}",
|
|
388
|
+
sender: sender,
|
|
389
|
+
recipient: chat_name,
|
|
390
|
+
subject: subject,
|
|
391
|
+
content: content,
|
|
392
|
+
raw_data: msg.to_json,
|
|
393
|
+
attachments: extract_attachments(msg),
|
|
394
|
+
timestamp: timestamp,
|
|
395
|
+
is_read: msg['ack'].to_i >= 1 ? 1 : 0
|
|
396
|
+
}
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def format_phone_number(jid)
|
|
400
|
+
return '[Unknown]' unless jid
|
|
401
|
+
|
|
402
|
+
phone = jid.split('@').first
|
|
403
|
+
if jid.include?('@c.us') || jid.include?('@s.whatsapp.net')
|
|
404
|
+
"+#{phone}"
|
|
405
|
+
elsif jid.include?('@g.us')
|
|
406
|
+
phone # Group ID
|
|
407
|
+
else
|
|
408
|
+
jid
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def format_chat_id(to)
|
|
413
|
+
# Clean phone number
|
|
414
|
+
phone = to.gsub(/[^\d+]/, '').sub(/^\+/, '')
|
|
415
|
+
|
|
416
|
+
if phone.include?('-')
|
|
417
|
+
"#{phone}@g.us"
|
|
418
|
+
else
|
|
419
|
+
"#{phone}@c.us"
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def extract_attachments(msg)
|
|
424
|
+
attachments = []
|
|
425
|
+
|
|
426
|
+
if msg['hasMedia']
|
|
427
|
+
attachments << {
|
|
428
|
+
type: msg['type'],
|
|
429
|
+
url: msg.dig('media', 'url'),
|
|
430
|
+
mime_type: msg['mimetype'],
|
|
431
|
+
filename: msg['filename']
|
|
432
|
+
}
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
attachments.empty? ? nil : attachments.to_json
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def post_json(uri, payload)
|
|
439
|
+
request = Net::HTTP::Post.new(uri)
|
|
440
|
+
request['Content-Type'] = 'application/json'
|
|
441
|
+
request.body = payload.to_json
|
|
442
|
+
|
|
443
|
+
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
|
444
|
+
http.request(request)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def parse_error(response)
|
|
449
|
+
data = JSON.parse(response.body)
|
|
450
|
+
data['message'] || data['error'] || response.message
|
|
451
|
+
rescue
|
|
452
|
+
response.message
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def detect_mime_type(file_path)
|
|
456
|
+
ext = File.extname(file_path).downcase
|
|
457
|
+
|
|
458
|
+
case ext
|
|
459
|
+
when '.jpg', '.jpeg' then 'image/jpeg'
|
|
460
|
+
when '.png' then 'image/png'
|
|
461
|
+
when '.gif' then 'image/gif'
|
|
462
|
+
when '.webp' then 'image/webp'
|
|
463
|
+
when '.mp4' then 'video/mp4'
|
|
464
|
+
when '.mp3' then 'audio/mpeg'
|
|
465
|
+
when '.ogg' then 'audio/ogg'
|
|
466
|
+
when '.pdf' then 'application/pdf'
|
|
467
|
+
when '.doc' then 'application/msword'
|
|
468
|
+
when '.docx' then 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
469
|
+
else 'application/octet-stream'
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|