kapso-client-ruby 1.0.1 → 1.0.2
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/.rubocop.yml +81 -81
- data/CHANGELOG.md +262 -91
- data/Gemfile +20 -20
- data/RAILS_INTEGRATION.md +477 -477
- data/README.md +1053 -752
- data/Rakefile +40 -40
- data/TEMPLATE_TOOLS_GUIDE.md +120 -120
- data/WHATSAPP_24_HOUR_GUIDE.md +133 -133
- data/examples/advanced_features.rb +352 -349
- data/examples/advanced_messaging.rb +241 -0
- data/examples/basic_messaging.rb +139 -136
- data/examples/enhanced_interactive.rb +400 -0
- data/examples/flows_usage.rb +307 -0
- data/examples/interactive_messages.rb +343 -0
- data/examples/media_management.rb +256 -253
- data/examples/rails/jobs.rb +387 -387
- data/examples/rails/models.rb +239 -239
- data/examples/rails/notifications_controller.rb +226 -226
- data/examples/template_management.rb +393 -390
- data/kapso-ruby-logo.jpg +0 -0
- data/lib/kapso_client_ruby/client.rb +321 -316
- data/lib/kapso_client_ruby/errors.rb +348 -329
- data/lib/kapso_client_ruby/rails/generators/install_generator.rb +75 -75
- data/lib/kapso_client_ruby/rails/generators/templates/env.erb +20 -20
- data/lib/kapso_client_ruby/rails/generators/templates/initializer.rb.erb +32 -32
- data/lib/kapso_client_ruby/rails/generators/templates/message_service.rb.erb +137 -137
- data/lib/kapso_client_ruby/rails/generators/templates/webhook_controller.rb.erb +61 -61
- data/lib/kapso_client_ruby/rails/railtie.rb +54 -54
- data/lib/kapso_client_ruby/rails/service.rb +188 -188
- data/lib/kapso_client_ruby/rails/tasks.rake +166 -166
- data/lib/kapso_client_ruby/resources/calls.rb +172 -172
- data/lib/kapso_client_ruby/resources/contacts.rb +190 -190
- data/lib/kapso_client_ruby/resources/conversations.rb +103 -103
- data/lib/kapso_client_ruby/resources/flows.rb +382 -0
- data/lib/kapso_client_ruby/resources/media.rb +205 -205
- data/lib/kapso_client_ruby/resources/messages.rb +760 -380
- data/lib/kapso_client_ruby/resources/phone_numbers.rb +85 -85
- data/lib/kapso_client_ruby/resources/templates.rb +283 -283
- data/lib/kapso_client_ruby/types.rb +348 -262
- data/lib/kapso_client_ruby/version.rb +5 -5
- data/lib/kapso_client_ruby.rb +75 -74
- data/scripts/.env.example +17 -17
- data/scripts/kapso_template_finder.rb +91 -91
- data/scripts/sdk_setup.rb +404 -404
- data/scripts/test.rb +60 -60
- metadata +12 -3
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'mime/types'
|
|
4
|
-
|
|
5
|
-
module KapsoClientRuby
|
|
6
|
-
module Resources
|
|
7
|
-
class Media
|
|
8
|
-
def initialize(client)
|
|
9
|
-
@client = client
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Upload media file
|
|
13
|
-
def upload(phone_number_id:, type:, file:, filename: nil, messaging_product: 'whatsapp',
|
|
14
|
-
upload_strategy: nil)
|
|
15
|
-
validate_media_type(type)
|
|
16
|
-
|
|
17
|
-
# Build multipart form data
|
|
18
|
-
form_data = {
|
|
19
|
-
'messaging_product' => messaging_product,
|
|
20
|
-
'type' => type
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
# Handle file parameter - can be File, IO, or file path string
|
|
24
|
-
file_obj = case file
|
|
25
|
-
when String
|
|
26
|
-
# Assume it's a file path
|
|
27
|
-
File.open(file, 'rb')
|
|
28
|
-
when File, IO, StringIO
|
|
29
|
-
file
|
|
30
|
-
else
|
|
31
|
-
raise ArgumentError, 'file must be a File, IO object, or file path string'
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Determine filename and content type
|
|
35
|
-
if filename.nil? && file.is_a?(String)
|
|
36
|
-
filename = File.basename(file)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
content_type = determine_content_type(file_obj, filename, type)
|
|
40
|
-
|
|
41
|
-
form_data['file'] = Faraday::UploadIO.new(file_obj, content_type, filename)
|
|
42
|
-
form_data['upload_strategy'] = upload_strategy if upload_strategy
|
|
43
|
-
|
|
44
|
-
# Set multipart content type header
|
|
45
|
-
headers = { 'Content-Type' => 'multipart/form-data' }
|
|
46
|
-
|
|
47
|
-
response = @client.request(:post, "#{phone_number_id}/media",
|
|
48
|
-
body: form_data, headers: headers, response_type: :json)
|
|
49
|
-
|
|
50
|
-
# Close file if we opened it
|
|
51
|
-
file_obj.close if file.is_a?(String) && file_obj.respond_to?(:close)
|
|
52
|
-
|
|
53
|
-
Types::MediaUploadResponse.new(response)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Get media metadata
|
|
57
|
-
def get(media_id:, phone_number_id: nil)
|
|
58
|
-
# phone_number_id is required for Kapso proxy
|
|
59
|
-
if @client.kapso_proxy? && phone_number_id.nil?
|
|
60
|
-
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
query_params = {}
|
|
64
|
-
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
|
65
|
-
|
|
66
|
-
response = @client.request(:get, media_id,
|
|
67
|
-
query: query_params, response_type: :json)
|
|
68
|
-
Types::MediaMetadataResponse.new(response)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Delete media
|
|
72
|
-
def delete(media_id:, phone_number_id: nil)
|
|
73
|
-
# phone_number_id is required for Kapso proxy
|
|
74
|
-
if @client.kapso_proxy? && phone_number_id.nil?
|
|
75
|
-
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
query_params = {}
|
|
79
|
-
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
|
80
|
-
|
|
81
|
-
response = @client.request(:delete, media_id,
|
|
82
|
-
query: query_params, response_type: :json)
|
|
83
|
-
Types::GraphSuccessResponse.new(response)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# Download media content
|
|
87
|
-
def download(media_id:, phone_number_id: nil, headers: {},
|
|
88
|
-
auth: :auto, as: :binary)
|
|
89
|
-
# First get the media metadata to get the download URL
|
|
90
|
-
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
|
91
|
-
download_url = metadata.url
|
|
92
|
-
|
|
93
|
-
# Determine authentication strategy
|
|
94
|
-
use_auth = case auth
|
|
95
|
-
when :auto
|
|
96
|
-
# Auto-detect: use auth for graph.facebook.com URLs, no auth for CDNs
|
|
97
|
-
download_url.include?('graph.facebook.com')
|
|
98
|
-
when :always
|
|
99
|
-
true
|
|
100
|
-
when :never
|
|
101
|
-
false
|
|
102
|
-
else
|
|
103
|
-
raise ArgumentError, 'auth must be :auto, :always, or :never'
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Prepare headers
|
|
107
|
-
download_headers = headers.dup
|
|
108
|
-
|
|
109
|
-
# Make the download request
|
|
110
|
-
if use_auth
|
|
111
|
-
response = @client.fetch(download_url, headers: download_headers)
|
|
112
|
-
else
|
|
113
|
-
response = @client.raw_request(:get, download_url, headers: download_headers)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
unless response.success?
|
|
117
|
-
raise Errors::GraphApiError.new(
|
|
118
|
-
message: "Failed to download media: #{response.status}",
|
|
119
|
-
http_status: response.status,
|
|
120
|
-
raw_response: response.body
|
|
121
|
-
)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Return response based on requested format
|
|
125
|
-
case as
|
|
126
|
-
when :binary
|
|
127
|
-
response.body
|
|
128
|
-
when :response
|
|
129
|
-
response
|
|
130
|
-
when :base64
|
|
131
|
-
require 'base64'
|
|
132
|
-
Base64.strict_encode64(response.body)
|
|
133
|
-
else
|
|
134
|
-
raise ArgumentError, 'as must be :binary, :response, or :base64'
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Save media to file
|
|
139
|
-
def save_to_file(media_id:, filepath:, phone_number_id: nil, headers: {}, auth: :auto)
|
|
140
|
-
content = download(
|
|
141
|
-
media_id: media_id,
|
|
142
|
-
phone_number_id: phone_number_id,
|
|
143
|
-
headers: headers,
|
|
144
|
-
auth: auth,
|
|
145
|
-
as: :binary
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
File.binwrite(filepath, content)
|
|
149
|
-
filepath
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Get media info including size, type, and download URL
|
|
153
|
-
def info(media_id:, phone_number_id: nil)
|
|
154
|
-
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
|
155
|
-
|
|
156
|
-
{
|
|
157
|
-
id: metadata.id,
|
|
158
|
-
url: metadata.url,
|
|
159
|
-
mime_type: metadata.mime_type,
|
|
160
|
-
sha256: metadata.sha256,
|
|
161
|
-
file_size: metadata.file_size.to_i,
|
|
162
|
-
messaging_product: metadata.messaging_product
|
|
163
|
-
}
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
def validate_media_type(type)
|
|
169
|
-
valid_types = %w[image audio video document sticker]
|
|
170
|
-
unless valid_types.include?(type.to_s)
|
|
171
|
-
raise ArgumentError, "Invalid media type '#{type}'. Must be one of: #{valid_types.join(', ')}"
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def determine_content_type(file_obj, filename, media_type)
|
|
176
|
-
# First try to determine from filename
|
|
177
|
-
if filename
|
|
178
|
-
mime_types = MIME::Types.type_for(filename)
|
|
179
|
-
return mime_types.first.content_type unless mime_types.empty?
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# Try to determine from file extension if file_obj responds to path
|
|
183
|
-
if file_obj.respond_to?(:path) && file_obj.path
|
|
184
|
-
mime_types = MIME::Types.type_for(file_obj.path)
|
|
185
|
-
return mime_types.first.content_type unless mime_types.empty?
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Fall back to generic types based on media_type
|
|
189
|
-
case media_type.to_s
|
|
190
|
-
when 'image'
|
|
191
|
-
'image/jpeg'
|
|
192
|
-
when 'audio'
|
|
193
|
-
'audio/mpeg'
|
|
194
|
-
when 'video'
|
|
195
|
-
'video/mp4'
|
|
196
|
-
when 'document'
|
|
197
|
-
'application/pdf'
|
|
198
|
-
when 'sticker'
|
|
199
|
-
'image/webp'
|
|
200
|
-
else
|
|
201
|
-
'application/octet-stream'
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mime/types'
|
|
4
|
+
|
|
5
|
+
module KapsoClientRuby
|
|
6
|
+
module Resources
|
|
7
|
+
class Media
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Upload media file
|
|
13
|
+
def upload(phone_number_id:, type:, file:, filename: nil, messaging_product: 'whatsapp',
|
|
14
|
+
upload_strategy: nil)
|
|
15
|
+
validate_media_type(type)
|
|
16
|
+
|
|
17
|
+
# Build multipart form data
|
|
18
|
+
form_data = {
|
|
19
|
+
'messaging_product' => messaging_product,
|
|
20
|
+
'type' => type
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Handle file parameter - can be File, IO, or file path string
|
|
24
|
+
file_obj = case file
|
|
25
|
+
when String
|
|
26
|
+
# Assume it's a file path
|
|
27
|
+
File.open(file, 'rb')
|
|
28
|
+
when File, IO, StringIO
|
|
29
|
+
file
|
|
30
|
+
else
|
|
31
|
+
raise ArgumentError, 'file must be a File, IO object, or file path string'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Determine filename and content type
|
|
35
|
+
if filename.nil? && file.is_a?(String)
|
|
36
|
+
filename = File.basename(file)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
content_type = determine_content_type(file_obj, filename, type)
|
|
40
|
+
|
|
41
|
+
form_data['file'] = Faraday::UploadIO.new(file_obj, content_type, filename)
|
|
42
|
+
form_data['upload_strategy'] = upload_strategy if upload_strategy
|
|
43
|
+
|
|
44
|
+
# Set multipart content type header
|
|
45
|
+
headers = { 'Content-Type' => 'multipart/form-data' }
|
|
46
|
+
|
|
47
|
+
response = @client.request(:post, "#{phone_number_id}/media",
|
|
48
|
+
body: form_data, headers: headers, response_type: :json)
|
|
49
|
+
|
|
50
|
+
# Close file if we opened it
|
|
51
|
+
file_obj.close if file.is_a?(String) && file_obj.respond_to?(:close)
|
|
52
|
+
|
|
53
|
+
Types::MediaUploadResponse.new(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Get media metadata
|
|
57
|
+
def get(media_id:, phone_number_id: nil)
|
|
58
|
+
# phone_number_id is required for Kapso proxy
|
|
59
|
+
if @client.kapso_proxy? && phone_number_id.nil?
|
|
60
|
+
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
query_params = {}
|
|
64
|
+
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
|
65
|
+
|
|
66
|
+
response = @client.request(:get, media_id,
|
|
67
|
+
query: query_params, response_type: :json)
|
|
68
|
+
Types::MediaMetadataResponse.new(response)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Delete media
|
|
72
|
+
def delete(media_id:, phone_number_id: nil)
|
|
73
|
+
# phone_number_id is required for Kapso proxy
|
|
74
|
+
if @client.kapso_proxy? && phone_number_id.nil?
|
|
75
|
+
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
query_params = {}
|
|
79
|
+
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
|
80
|
+
|
|
81
|
+
response = @client.request(:delete, media_id,
|
|
82
|
+
query: query_params, response_type: :json)
|
|
83
|
+
Types::GraphSuccessResponse.new(response)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Download media content
|
|
87
|
+
def download(media_id:, phone_number_id: nil, headers: {},
|
|
88
|
+
auth: :auto, as: :binary)
|
|
89
|
+
# First get the media metadata to get the download URL
|
|
90
|
+
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
|
91
|
+
download_url = metadata.url
|
|
92
|
+
|
|
93
|
+
# Determine authentication strategy
|
|
94
|
+
use_auth = case auth
|
|
95
|
+
when :auto
|
|
96
|
+
# Auto-detect: use auth for graph.facebook.com URLs, no auth for CDNs
|
|
97
|
+
download_url.include?('graph.facebook.com')
|
|
98
|
+
when :always
|
|
99
|
+
true
|
|
100
|
+
when :never
|
|
101
|
+
false
|
|
102
|
+
else
|
|
103
|
+
raise ArgumentError, 'auth must be :auto, :always, or :never'
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Prepare headers
|
|
107
|
+
download_headers = headers.dup
|
|
108
|
+
|
|
109
|
+
# Make the download request
|
|
110
|
+
if use_auth
|
|
111
|
+
response = @client.fetch(download_url, headers: download_headers)
|
|
112
|
+
else
|
|
113
|
+
response = @client.raw_request(:get, download_url, headers: download_headers)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
unless response.success?
|
|
117
|
+
raise Errors::GraphApiError.new(
|
|
118
|
+
message: "Failed to download media: #{response.status}",
|
|
119
|
+
http_status: response.status,
|
|
120
|
+
raw_response: response.body
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Return response based on requested format
|
|
125
|
+
case as
|
|
126
|
+
when :binary
|
|
127
|
+
response.body
|
|
128
|
+
when :response
|
|
129
|
+
response
|
|
130
|
+
when :base64
|
|
131
|
+
require 'base64'
|
|
132
|
+
Base64.strict_encode64(response.body)
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, 'as must be :binary, :response, or :base64'
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Save media to file
|
|
139
|
+
def save_to_file(media_id:, filepath:, phone_number_id: nil, headers: {}, auth: :auto)
|
|
140
|
+
content = download(
|
|
141
|
+
media_id: media_id,
|
|
142
|
+
phone_number_id: phone_number_id,
|
|
143
|
+
headers: headers,
|
|
144
|
+
auth: auth,
|
|
145
|
+
as: :binary
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
File.binwrite(filepath, content)
|
|
149
|
+
filepath
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get media info including size, type, and download URL
|
|
153
|
+
def info(media_id:, phone_number_id: nil)
|
|
154
|
+
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
|
155
|
+
|
|
156
|
+
{
|
|
157
|
+
id: metadata.id,
|
|
158
|
+
url: metadata.url,
|
|
159
|
+
mime_type: metadata.mime_type,
|
|
160
|
+
sha256: metadata.sha256,
|
|
161
|
+
file_size: metadata.file_size.to_i,
|
|
162
|
+
messaging_product: metadata.messaging_product
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def validate_media_type(type)
|
|
169
|
+
valid_types = %w[image audio video document sticker]
|
|
170
|
+
unless valid_types.include?(type.to_s)
|
|
171
|
+
raise ArgumentError, "Invalid media type '#{type}'. Must be one of: #{valid_types.join(', ')}"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def determine_content_type(file_obj, filename, media_type)
|
|
176
|
+
# First try to determine from filename
|
|
177
|
+
if filename
|
|
178
|
+
mime_types = MIME::Types.type_for(filename)
|
|
179
|
+
return mime_types.first.content_type unless mime_types.empty?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Try to determine from file extension if file_obj responds to path
|
|
183
|
+
if file_obj.respond_to?(:path) && file_obj.path
|
|
184
|
+
mime_types = MIME::Types.type_for(file_obj.path)
|
|
185
|
+
return mime_types.first.content_type unless mime_types.empty?
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Fall back to generic types based on media_type
|
|
189
|
+
case media_type.to_s
|
|
190
|
+
when 'image'
|
|
191
|
+
'image/jpeg'
|
|
192
|
+
when 'audio'
|
|
193
|
+
'audio/mpeg'
|
|
194
|
+
when 'video'
|
|
195
|
+
'video/mp4'
|
|
196
|
+
when 'document'
|
|
197
|
+
'application/pdf'
|
|
198
|
+
when 'sticker'
|
|
199
|
+
'image/webp'
|
|
200
|
+
else
|
|
201
|
+
'application/octet-stream'
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
206
|
end
|