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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +81 -81
  3. data/CHANGELOG.md +262 -91
  4. data/Gemfile +20 -20
  5. data/RAILS_INTEGRATION.md +477 -477
  6. data/README.md +1053 -752
  7. data/Rakefile +40 -40
  8. data/TEMPLATE_TOOLS_GUIDE.md +120 -120
  9. data/WHATSAPP_24_HOUR_GUIDE.md +133 -133
  10. data/examples/advanced_features.rb +352 -349
  11. data/examples/advanced_messaging.rb +241 -0
  12. data/examples/basic_messaging.rb +139 -136
  13. data/examples/enhanced_interactive.rb +400 -0
  14. data/examples/flows_usage.rb +307 -0
  15. data/examples/interactive_messages.rb +343 -0
  16. data/examples/media_management.rb +256 -253
  17. data/examples/rails/jobs.rb +387 -387
  18. data/examples/rails/models.rb +239 -239
  19. data/examples/rails/notifications_controller.rb +226 -226
  20. data/examples/template_management.rb +393 -390
  21. data/kapso-ruby-logo.jpg +0 -0
  22. data/lib/kapso_client_ruby/client.rb +321 -316
  23. data/lib/kapso_client_ruby/errors.rb +348 -329
  24. data/lib/kapso_client_ruby/rails/generators/install_generator.rb +75 -75
  25. data/lib/kapso_client_ruby/rails/generators/templates/env.erb +20 -20
  26. data/lib/kapso_client_ruby/rails/generators/templates/initializer.rb.erb +32 -32
  27. data/lib/kapso_client_ruby/rails/generators/templates/message_service.rb.erb +137 -137
  28. data/lib/kapso_client_ruby/rails/generators/templates/webhook_controller.rb.erb +61 -61
  29. data/lib/kapso_client_ruby/rails/railtie.rb +54 -54
  30. data/lib/kapso_client_ruby/rails/service.rb +188 -188
  31. data/lib/kapso_client_ruby/rails/tasks.rake +166 -166
  32. data/lib/kapso_client_ruby/resources/calls.rb +172 -172
  33. data/lib/kapso_client_ruby/resources/contacts.rb +190 -190
  34. data/lib/kapso_client_ruby/resources/conversations.rb +103 -103
  35. data/lib/kapso_client_ruby/resources/flows.rb +382 -0
  36. data/lib/kapso_client_ruby/resources/media.rb +205 -205
  37. data/lib/kapso_client_ruby/resources/messages.rb +760 -380
  38. data/lib/kapso_client_ruby/resources/phone_numbers.rb +85 -85
  39. data/lib/kapso_client_ruby/resources/templates.rb +283 -283
  40. data/lib/kapso_client_ruby/types.rb +348 -262
  41. data/lib/kapso_client_ruby/version.rb +5 -5
  42. data/lib/kapso_client_ruby.rb +75 -74
  43. data/scripts/.env.example +17 -17
  44. data/scripts/kapso_template_finder.rb +91 -91
  45. data/scripts/sdk_setup.rb +404 -404
  46. data/scripts/test.rb +60 -60
  47. 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