warb 1.0.0 → 1.0.1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -5
  3. data/README.md +32 -9
  4. data/Rakefile +3 -3
  5. data/docs/README.md +4 -1
  6. data/docs/components/README.md +4 -1
  7. data/docs/components/button.md +62 -0
  8. data/docs/components/copy_code_button.md +57 -0
  9. data/docs/components/flow_button.md +102 -0
  10. data/docs/components/url_button.md +57 -0
  11. data/docs/messages/README.md +2 -1
  12. data/docs/messages/flow.md +241 -5
  13. data/docs/messages/interactive_call_to_action_url.md +9 -9
  14. data/docs/messages/interactive_list.md +2 -2
  15. data/docs/messages/interactive_reply_button.md +9 -9
  16. data/docs/messages/template.md +373 -0
  17. data/docs/resources/README.md +14 -0
  18. data/docs/resources/currency.md +22 -0
  19. data/docs/resources/date_time.md +11 -0
  20. data/docs/resources/text.md +9 -0
  21. data/docs/setup.md +45 -1
  22. data/examples/audio.rb +10 -10
  23. data/examples/document.rb +34 -34
  24. data/examples/image.rb +22 -22
  25. data/examples/interactive_call_to_action_url.rb +46 -46
  26. data/examples/interactive_list.rb +61 -61
  27. data/examples/interactive_reply_button.rb +43 -43
  28. data/examples/location.rb +32 -32
  29. data/examples/location_request.rb +11 -11
  30. data/examples/message.rb +8 -8
  31. data/examples/sticker.rb +10 -10
  32. data/examples/video.rb +22 -22
  33. data/examples/webhook.rb +77 -43
  34. data/lib/warb/category.rb +8 -0
  35. data/lib/warb/client.rb +7 -5
  36. data/lib/warb/components/action.rb +12 -8
  37. data/lib/warb/components/button.rb +29 -0
  38. data/lib/warb/components/component.rb +19 -0
  39. data/lib/warb/components/copy_code_button.rb +30 -0
  40. data/lib/warb/components/flow_button.rb +32 -0
  41. data/lib/warb/components/quick_reply_button.rb +15 -0
  42. data/lib/warb/components/url_button.rb +30 -0
  43. data/lib/warb/components/voice_call_button.rb +15 -0
  44. data/lib/warb/configuration.rb +4 -1
  45. data/lib/warb/connection.rb +15 -9
  46. data/lib/warb/dispatcher.rb +4 -3
  47. data/lib/warb/dispatcher_concern.rb +6 -0
  48. data/lib/warb/errors.rb +27 -0
  49. data/lib/warb/indicator_dispatcher.rb +4 -4
  50. data/lib/warb/language.rb +8 -0
  51. data/lib/warb/media_dispatcher.rb +10 -10
  52. data/lib/warb/resources/audio.rb +1 -1
  53. data/lib/warb/resources/contact.rb +22 -20
  54. data/lib/warb/resources/currency.rb +47 -0
  55. data/lib/warb/resources/date_time.rb +34 -0
  56. data/lib/warb/resources/document.rb +1 -1
  57. data/lib/warb/resources/flow.rb +82 -20
  58. data/lib/warb/resources/helpers/header.rb +35 -0
  59. data/lib/warb/resources/image.rb +1 -1
  60. data/lib/warb/resources/interactive_call_to_action_url.rb +10 -8
  61. data/lib/warb/resources/interactive_list.rb +7 -5
  62. data/lib/warb/resources/interactive_reply_button.rb +10 -8
  63. data/lib/warb/resources/location.rb +11 -1
  64. data/lib/warb/resources/location_request.rb +5 -3
  65. data/lib/warb/resources/reaction.rb +1 -1
  66. data/lib/warb/resources/resource.rb +14 -4
  67. data/lib/warb/resources/sticker.rb +1 -1
  68. data/lib/warb/resources/template.rb +163 -0
  69. data/lib/warb/resources/text.rb +31 -3
  70. data/lib/warb/resources/validation.rb +30 -0
  71. data/lib/warb/resources/video.rb +1 -1
  72. data/lib/warb/response.rb +33 -0
  73. data/lib/warb/response_error_handler.rb +42 -0
  74. data/lib/warb/template_dispatcher.rb +21 -0
  75. data/lib/warb/utils.rb +3 -1
  76. data/lib/warb/version.rb +1 -1
  77. data/lib/warb.rb +67 -31
  78. metadata +34 -3
@@ -5,11 +5,12 @@ module Warb
5
5
  class InteractiveReplyButton < Resource
6
6
  attr_accessor :header, :body, :footer, :action
7
7
 
8
+ # rubocop:disable Metrics/MethodLength
8
9
  def build_payload
9
10
  {
10
- type: "interactive",
11
+ type: 'interactive',
11
12
  interactive: {
12
- type: "button",
13
+ type: 'button',
13
14
  header: header || @params[:header]&.to_h,
14
15
  body: {
15
16
  text: body || @params[:body]
@@ -21,27 +22,28 @@ module Warb
21
22
  }
22
23
  }
23
24
  end
25
+ # rubocop:enable Metrics/MethodLength
24
26
 
25
- def set_text_header(text)
27
+ def add_text_header(text)
26
28
  @header = Warb::Resources::Text.new(text:).build_header
27
29
  end
28
30
 
29
- def set_image_header(media_id: nil, link: nil)
31
+ def add_image_header(media_id: nil, link: nil)
30
32
  @header = Warb::Resources::Image.new(media_id:, link:).build_header
31
33
  end
32
34
 
33
- def set_video_header(media_id: nil, link: nil)
35
+ def add_video_header(media_id: nil, link: nil)
34
36
  @header = Warb::Resources::Video.new(media_id:, link:).build_header
35
37
  end
36
38
 
37
- def set_document_header(media_id: nil, link: nil, filename: nil)
39
+ def add_document_header(media_id: nil, link: nil, filename: nil)
38
40
  @header = Warb::Resources::Document.new(media_id:, link:, filename:).build_header
39
41
  end
40
42
 
41
- def build_action(**params, &block)
43
+ def build_action(**params, &)
42
44
  @action = Warb::Components::ReplyButtonAction.new(**params)
43
45
 
44
- block_given? ? @action.tap(&block) : @action
46
+ block_given? ? @action.tap(&) : @action
45
47
  end
46
48
  end
47
49
  end
@@ -5,9 +5,19 @@ module Warb
5
5
  class Location < Resource
6
6
  attr_accessor :latitude, :longitude, :name, :address
7
7
 
8
+ def build_header
9
+ common_location_params
10
+ end
11
+
8
12
  def build_payload
13
+ common_location_params
14
+ end
15
+
16
+ private
17
+
18
+ def common_location_params
9
19
  {
10
- type: "location",
20
+ type: 'location',
11
21
  location: {
12
22
  latitude: latitude || @params[:latitude],
13
23
  longitude: longitude || @params[:longitude],
@@ -5,20 +5,22 @@ module Warb
5
5
  class LocationRequest < Resource
6
6
  attr_accessor :body_text
7
7
 
8
+ # rubocop:disable Metrics/MethodLength
8
9
  def build_payload
9
10
  {
10
- type: "interactive",
11
+ type: 'interactive',
11
12
  interactive: {
12
- type: "location_request_message",
13
+ type: 'location_request_message',
13
14
  body: {
14
15
  text: body_text || @params[:body_text]
15
16
  },
16
17
  action: {
17
- name: "send_location"
18
+ name: 'send_location'
18
19
  }
19
20
  }
20
21
  }
21
22
  end
23
+ # rubocop:enable Metrics/MethodLength
22
24
  end
23
25
  end
24
26
  end
@@ -7,7 +7,7 @@ module Warb
7
7
 
8
8
  def build_payload
9
9
  {
10
- type: "reaction",
10
+ type: 'reaction',
11
11
  reaction: {
12
12
  message_id: message_id || @params[:message_id],
13
13
  emoji: emoji || @params[:emoji]
@@ -3,6 +3,8 @@
3
3
  module Warb
4
4
  module Resources
5
5
  class Resource
6
+ include Validation
7
+
6
8
  def initialize(**params)
7
9
  @params = params
8
10
  end
@@ -19,19 +21,27 @@ module Warb
19
21
  raise NotImplementedError
20
22
  end
21
23
 
22
- def set_text_header
24
+ def build_template_positional_parameter
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def build_template_named_parameter(paramater_name)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def add_text_header
23
33
  raise NotImplementedError
24
34
  end
25
35
 
26
- def set_image_header
36
+ def add_image_header
27
37
  raise NotImplementedError
28
38
  end
29
39
 
30
- def set_video_header
40
+ def add_video_header
31
41
  raise NotImplementedError
32
42
  end
33
43
 
34
- def set_document_header
44
+ def add_document_header
35
45
  raise NotImplementedError
36
46
  end
37
47
 
@@ -7,7 +7,7 @@ module Warb
7
7
 
8
8
  def build_payload
9
9
  {
10
- type: "sticker",
10
+ type: 'sticker',
11
11
  sticker: {
12
12
  id: media_id || @params[:media_id],
13
13
  link: link || @params[:link]
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warb
4
+ module Resources
5
+ class Template < Resource
6
+ include Helpers::Header
7
+
8
+ attr_accessor :name, :language, :resources, :header, :category, :body, :buttons
9
+
10
+ def initialize(**params)
11
+ super
12
+
13
+ @name = params[:name]
14
+ @language = params[:language]
15
+ @resources = params[:resources]
16
+ @category = params[:category]
17
+ @body = params[:body]
18
+ @buttons = []
19
+ end
20
+
21
+ # rubocop:disable Metrics/MethodLength
22
+ def build_payload
23
+ {
24
+ type: 'template',
25
+ template: {
26
+ name: name,
27
+ language: {
28
+ code: language
29
+ },
30
+ components: [
31
+ component_header,
32
+ component_body,
33
+ *buttons
34
+ ].compact
35
+ }
36
+ }
37
+ end
38
+ # rubocop:enable Metrics/MethodLength
39
+
40
+ def creation_payload
41
+ {
42
+ name: name,
43
+ language: language,
44
+ category: category,
45
+ components: [
46
+ body&.build_template_example_parameter
47
+ ].compact
48
+ }
49
+ end
50
+
51
+ def add_currency_parameter(parameter_name = nil, **params, &)
52
+ add_parameter(parameter_name, Currency.new(**params), &)
53
+ end
54
+
55
+ def add_date_time_parameter(parameter_name = nil, **params, &)
56
+ add_parameter(parameter_name, DateTime.new(**params), &)
57
+ end
58
+
59
+ def add_text_parameter(parameter_name = nil, **params, &)
60
+ add_parameter(parameter_name, Text.new(**params), &)
61
+ end
62
+
63
+ def add_quick_reply_button(index: position, &block)
64
+ add_button(Warb::Components::QuickReplyButton.new(index:), &block)
65
+ end
66
+
67
+ def add_dynamic_url_button(index: position, text: nil, &block)
68
+ add_button(Warb::Components::UrlButton.new(index:, text:), &block)
69
+ end
70
+
71
+ alias add_auth_code_button add_dynamic_url_button
72
+
73
+ def add_copy_code_button(index: position, coupon_code: nil, &block)
74
+ add_button(Warb::Components::CopyCodeButton.new(index:, coupon_code:), &block)
75
+ end
76
+
77
+ def add_voice_call_button(index: position, &block)
78
+ add_button(Warb::Components::VoiceCallButton.new(index:), &block)
79
+ end
80
+
81
+ def add_flow_button(index: position, flow_token: nil, flow_action_data: nil, &block)
82
+ add_button(
83
+ Warb::Components::FlowButton.new(index: index, flow_token: flow_token,
84
+ flow_action_data: flow_action_data), &block
85
+ )
86
+ end
87
+
88
+ def add_button(instance, &)
89
+ return @buttons << instance.to_h unless block_given?
90
+
91
+ @buttons << instance.tap(&).to_h
92
+ end
93
+
94
+ private
95
+
96
+ def component_header
97
+ return unless header.is_a? Resource
98
+
99
+ {
100
+ type: 'header',
101
+ parameters: [
102
+ header.build_header
103
+ ]
104
+ }
105
+ end
106
+
107
+ def component_body
108
+ return if resources.nil? || resources.empty?
109
+
110
+ {
111
+ type: 'body',
112
+ parameters: build_parameters
113
+ }
114
+ end
115
+
116
+ def build_parameters
117
+ case resources
118
+ when Hash
119
+ named_parameters
120
+ when Array
121
+ positional_parameters
122
+ end
123
+ end
124
+
125
+ def named_parameters
126
+ resources.map do |parameter_name, resource|
127
+ resource.build_template_named_parameter(parameter_name.to_s)
128
+ end
129
+ end
130
+
131
+ def positional_parameters
132
+ resources.map(&:build_template_positional_parameter)
133
+ end
134
+
135
+ def add_parameter(parameter_name, instance, &)
136
+ case resources
137
+ when Hash
138
+ resources[parameter_name.to_s] = instance
139
+ when Array
140
+ resources << instance
141
+ else
142
+ initialize_resources(parameter_name, instance, &)
143
+ end
144
+
145
+ block_given? ? instance.tap(&) : instance
146
+ end
147
+
148
+ def initialize_resources(parameter_name, instance)
149
+ if parameter_name.nil?
150
+ @resources = []
151
+ @resources << instance
152
+ else
153
+ @resources = {}
154
+ @resources[parameter_name] = instance
155
+ end
156
+ end
157
+
158
+ def position
159
+ buttons.count
160
+ end
161
+ end
162
+ end
163
+ end
@@ -3,15 +3,18 @@
3
3
  module Warb
4
4
  module Resources
5
5
  class Text < Resource
6
- attr_accessor :content, :text, :message, :preview_url
6
+ attr_accessor :content, :text, :message, :preview_url, :parameter_name, :examples
7
7
 
8
8
  def build_header
9
- { type: "text", text: message_per_priority }
9
+ { type: 'text', text: message_per_priority }.tap do |header|
10
+ parameter_name ||= @params[:parameter_name]
11
+ header[:parameter_name] = parameter_name unless parameter_name.nil?
12
+ end
10
13
  end
11
14
 
12
15
  def build_payload
13
16
  {
14
- type: "text",
17
+ type: 'text',
15
18
  text: {
16
19
  preview_url: preview_url || @params[:preview_url],
17
20
  body: message_per_priority
@@ -19,6 +22,31 @@ module Warb
19
22
  }
20
23
  end
21
24
 
25
+ def build_template_named_parameter(parameter_name)
26
+ {
27
+ type: 'text',
28
+ text: message_per_priority,
29
+ parameter_name: parameter_name
30
+ }
31
+ end
32
+
33
+ def build_template_positional_parameter
34
+ {
35
+ type: 'text',
36
+ text: message_per_priority
37
+ }
38
+ end
39
+
40
+ def build_template_example_parameter
41
+ { type: 'body', text: message_per_priority }.tap do |param|
42
+ examples ||= @params[:examples]
43
+
44
+ next unless examples.is_a?(Array)
45
+
46
+ param[:example] = { body_text: [examples] }
47
+ end
48
+ end
49
+
22
50
  private
23
51
 
24
52
  def message_per_priority
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warb
4
+ module Resources
5
+ module Validation
6
+ def blank?(val)
7
+ val.respond_to?(:empty?) ? val.empty? : !val
8
+ end
9
+
10
+ def raw_value(field)
11
+ respond_to?(field) ? public_send(field) : nil
12
+ end
13
+
14
+ def resolve(field, default = nil)
15
+ val = raw_value(field)
16
+ val = @params[field] if blank?(val) && defined?(@params) && @params&.key?(field)
17
+ val = default if blank?(val) && !default.nil?
18
+ val
19
+ end
20
+
21
+ def validates(field, required: false, message: nil)
22
+ needed = required.respond_to?(:call) ? required.call : required
23
+ return unless needed
24
+ return unless blank?(resolve(field))
25
+
26
+ raise ArgumentError, (message || "#{field} is required")
27
+ end
28
+ end
29
+ end
30
+ end
@@ -19,7 +19,7 @@ module Warb
19
19
 
20
20
  def common_video_params
21
21
  {
22
- type: "video",
22
+ type: 'video',
23
23
  video: {
24
24
  id: media_id || @params[:media_id],
25
25
  link: link || @params[:link]
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warb
4
+ class Response
5
+ attr_reader :input, :wa_id, :message_id, :body
6
+
7
+ def initialize(body)
8
+ @body = body || {}
9
+ extract_contact_data
10
+ extract_message_data
11
+ end
12
+
13
+ private
14
+
15
+ def extract_contact_data
16
+ first_contact = contacts&.first
17
+ @input = first_contact&.dig('input')
18
+ @wa_id = first_contact&.dig('wa_id')
19
+ end
20
+
21
+ def extract_message_data
22
+ @message_id = messages&.first&.dig('id')
23
+ end
24
+
25
+ def contacts
26
+ @body['contacts']
27
+ end
28
+
29
+ def messages
30
+ @body['messages']
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warb
4
+ class ResponseErrorHandler
5
+ def initialize(body, status)
6
+ @body = body
7
+ @status = status
8
+ end
9
+
10
+ def handle
11
+ raise Warb::RequestError, 'invalid body' if @body.nil?
12
+
13
+ http_class = Warb::HTTP_ERRORS[@status]
14
+ error_class = custom_class || http_class || Warb::RequestError
15
+ Warb.configuration.logger.error(message.to_s)
16
+
17
+ raise error_class, message_from_error
18
+ end
19
+
20
+ private
21
+
22
+ def custom_class
23
+ Warb.configuration.custom_errors[@status]&.dig(code)
24
+ end
25
+
26
+ def message_from_error
27
+ details ? "(##{code}) #{details}" : message
28
+ end
29
+
30
+ def code
31
+ @code ||= @body.dig('error', 'code')
32
+ end
33
+
34
+ def message
35
+ @message ||= @body.dig('error', 'message')
36
+ end
37
+
38
+ def details
39
+ @details ||= @body.dig('error', 'error_data', 'details')
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Warb
4
+ class TemplateDispatcher < Dispatcher
5
+ def create(**args)
6
+ template = Resources::Template.new(**args)
7
+ @client.post('message_templates', template.creation_payload, endpoint_prefix: :business_id)
8
+ end
9
+
10
+ def delete(template_name)
11
+ @client.delete('message_templates', { name: template_name }, endpoint_prefix: :business_id).body
12
+ end
13
+
14
+ def list(**args)
15
+ filter = args.slice(:limit, :fields, :after, :before)
16
+ filter[:fields] = filter[:fields].join(',') if filter[:fields].is_a?(Array)
17
+
18
+ @client.get('message_templates', endpoint_prefix: :business_id, data: filter).body
19
+ end
20
+ end
21
+ end
data/lib/warb/utils.rb CHANGED
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class String
2
4
  def normalize
3
- unicode_normalize(:nfd).gsub(/\p{Mn}/, "")
5
+ unicode_normalize(:nfd).gsub(/\p{Mn}/, '')
4
6
  end
5
7
  end
data/lib/warb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Warb
4
- VERSION = "1.0.0"
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/warb.rb CHANGED
@@ -1,38 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "faraday"
4
- require "faraday/multipart"
5
- require_relative "warb/version"
6
- require_relative "warb/configuration"
7
- require_relative "warb/dispatcher_concern"
8
- require_relative "warb/client"
9
- require_relative "warb/resources/resource"
10
- require_relative "warb/resources/text"
11
- require_relative "warb/resources/image"
12
- require_relative "warb/resources/video"
13
- require_relative "warb/resources/sticker"
14
- require_relative "warb/resources/audio"
15
- require_relative "warb/resources/document"
16
- require_relative "warb/resources/location"
17
- require_relative "warb/resources/reaction"
18
- require_relative "warb/resources/location_request"
19
- require_relative "warb/resources/interactive_reply_button"
20
- require_relative "warb/resources/interactive_list"
21
- require_relative "warb/resources/interactive_call_to_action_url"
22
- require_relative "warb/resources/contact"
23
- require_relative "warb/resources/flow"
24
- require_relative "warb/dispatcher"
25
- require_relative "warb/media_dispatcher"
26
- require_relative "warb/indicator_dispatcher"
27
- require_relative "warb/utils"
28
- require_relative "warb/components/action"
3
+ require 'faraday'
4
+ require 'faraday/multipart'
29
5
 
30
- module Warb
31
- MESSAGING_PRODUCT = "whatsapp"
32
- RECIPIENT_TYPE = "individual"
6
+ require_relative 'warb/version'
7
+ require_relative 'warb/language'
8
+ require_relative 'warb/category'
9
+ require_relative 'warb/configuration'
10
+ require_relative 'warb/dispatcher_concern'
11
+ require_relative 'warb/client'
12
+
13
+ # Error/response stack
14
+ require_relative 'warb/errors'
15
+ require_relative 'warb/response_error_handler'
16
+ require_relative 'warb/response'
17
+
18
+ # Resources
19
+ require_relative 'warb/resources/validation'
20
+ require_relative 'warb/resources/helpers/header'
21
+ require_relative 'warb/resources/resource'
22
+ require_relative 'warb/resources/text'
23
+ require_relative 'warb/resources/image'
24
+ require_relative 'warb/resources/video'
25
+ require_relative 'warb/resources/sticker'
26
+ require_relative 'warb/resources/audio'
27
+ require_relative 'warb/resources/document'
28
+ require_relative 'warb/resources/location'
29
+ require_relative 'warb/resources/reaction'
30
+ require_relative 'warb/resources/location_request'
31
+ require_relative 'warb/resources/interactive_reply_button'
32
+ require_relative 'warb/resources/interactive_list'
33
+ require_relative 'warb/resources/interactive_call_to_action_url'
34
+ require_relative 'warb/resources/contact'
35
+ require_relative 'warb/resources/template'
36
+ require_relative 'warb/resources/currency'
37
+ require_relative 'warb/resources/date_time'
38
+ require_relative 'warb/resources/flow'
39
+
40
+ # Dispatchers
41
+ require_relative 'warb/dispatcher'
42
+ require_relative 'warb/media_dispatcher'
43
+ require_relative 'warb/template_dispatcher'
44
+ require_relative 'warb/indicator_dispatcher'
33
45
 
34
- class Error < StandardError; end
35
- # Your code goes here...
46
+ # Utils and components
47
+ require_relative 'warb/utils'
48
+ require_relative 'warb/components/component'
49
+ require_relative 'warb/components/button'
50
+ require_relative 'warb/components/flow_button'
51
+ require_relative 'warb/components/quick_reply_button'
52
+ require_relative 'warb/components/url_button'
53
+ require_relative 'warb/components/copy_code_button'
54
+ require_relative 'warb/components/voice_call_button'
55
+ require_relative 'warb/components/action'
56
+
57
+ module Warb
58
+ MESSAGING_PRODUCT = 'whatsapp'
59
+ RECIPIENT_TYPE = 'individual'
60
+ HTTP_ERRORS = {
61
+ 400 => BadRequest,
62
+ 401 => Unauthorized,
63
+ 403 => Forbidden,
64
+ 404 => NotFound,
65
+ 500 => InternalServerError,
66
+ 503 => ServiceUnavailable
67
+ }.freeze
36
68
 
37
69
  class << self
38
70
  include DispatcherConcern
@@ -54,5 +86,9 @@ module Warb
54
86
 
55
87
  client
56
88
  end
89
+
90
+ def list_phone_numbers
91
+ client.get('phone_numbers', endpoint_prefix: :business_id).body['data']
92
+ end
57
93
  end
58
94
  end