bobot 2.6.2 → 3.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.
@@ -2,18 +2,10 @@ module Bobot
2
2
  module Event
3
3
  # Common attributes for all incoming data from Facebook.
4
4
  module Common
5
- attr_reader :messaging
6
- attr_accessor :delay_options
7
- attr_accessor :payloads_sent
5
+ attr_reader :messaging, :page
8
6
 
9
7
  def initialize(messaging)
10
8
  @messaging = messaging
11
- @delay_options = { wait: 0, wait_until: nil }
12
- @payloads_sent = []
13
- end
14
-
15
- def add_delivery(payload:)
16
- @payloads_sent << payload
17
9
  end
18
10
 
19
11
  def sender
@@ -24,146 +16,65 @@ module Bobot
24
16
  @messaging['recipient']
25
17
  end
26
18
 
27
- def delay(wait: 0, wait_until: nil)
28
- raise Bobot::FieldFormat.new('wait has to be positive integer.') unless wait.present?
29
- if Bobot.config.async
30
- @delay_options[:wait] = wait if wait >= 0
31
- @delay_options[:wait_until] = wait_until if wait_until.present?
32
- else
33
- warn "delay is ignored since you configured Bobot.config.async to 'false'"
34
- end
35
- self
36
- end
37
-
38
19
  def sent_at
39
20
  Time.zone.at(@messaging['timestamp'] / 1000)
40
21
  end
41
22
 
42
- def deliver(payload_template:)
43
- raise Bobot::FieldFormat.new('payload_template is required.') unless payload_template.present?
44
- @payloads_sent << payload_template
45
- job = Bobot::DeliverJob
46
- if Bobot.config.async
47
- job = job.set(wait: @delay_options[:wait]) if @delay_options[:wait] > 0
48
- job = job.set(wait: @delay_options[:wait_until]) if @delay_options[:wait_until].present?
49
- job.perform_later(sender: sender, access_token: access_token, payload_template: payload_template)
50
- else
51
- job.perform_now(sender: sender, access_token: access_token, payload_template: payload_template)
52
- end
53
- end
54
-
55
23
  def sender_action(sender_action:)
56
- deliver(payload_template: { sender_action: sender_action })
24
+ page.sender_action(sender_action: sender_action, to: sender["id"])
57
25
  end
58
26
 
59
27
  def show_typing(state:)
60
- sender_action(sender_action: state ? 'typing_on' : 'typing_off')
28
+ page.show_typing(state: state, to: sender["id"])
61
29
  end
62
30
 
63
31
  def mark_as_seen
64
- sender_action(sender_action: 'mark_seen')
32
+ page.mark_as_seen(to: sender["id"])
65
33
  end
66
34
 
67
35
  def reply(payload_message:)
68
- deliver(payload_template: { message: payload_message })
36
+ page.send(payload_message: payload_message, to: sender["id"])
69
37
  end
70
38
 
71
39
  def reply_with_text(text:)
72
- raise Bobot::FieldFormat.new('text is required.') unless text.present?
73
- raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
74
- reply(
75
- payload_message: {
76
- text: text,
77
- },
78
- )
40
+ page.send_text(text: text, to: sender["id"])
79
41
  end
80
42
 
81
43
  def reply_with_attachment(url:, type:)
82
- raise Bobot::FieldFormat.new('url is required.') unless url.present?
83
- raise Bobot::FieldFormat.new('type is required.') unless type.present?
84
- raise Bobot::FieldFormat.new('type is invalid, only "image, audio, video, file" are permitted.') unless %w[image audio video file].include?(type)
85
- reply(
86
- payload_message: {
87
- attachment: {
88
- type: type,
89
- payload: {
90
- url: url,
91
- }.tap { |properties| properties.merge!(is_reusable: true) if type == 'image' },
92
- },
93
- },
94
- )
44
+ page.send_attachment(url: url, type: type, to: sender["id"])
95
45
  end
96
46
 
97
47
  def reply_with_image(url:)
98
- reply_with_attachment(url: url, type: 'image')
48
+ page.send_image(url: url, to: sender["id"])
99
49
  end
100
50
 
101
51
  def reply_with_audio(url:)
102
- reply_with_attachment(url: url, type: 'audio')
52
+ page.send_audio(url: url, to: sender["id"])
103
53
  end
104
54
 
105
55
  def reply_with_video(url:)
106
- reply_with_attachment(url: url, type: 'video')
56
+ page.send_video(url: url, to: sender["id"])
107
57
  end
108
58
 
109
59
  def reply_with_file(url:)
110
- reply_with_attachment(url: url, type: 'file')
60
+ page.send_file(url: url, to: sender["id"])
111
61
  end
112
62
 
113
63
  def reply_with_quick_replies(text:, quick_replies:)
114
- raise Bobot::FieldFormat.new('text is required.') unless text.present?
115
- raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
116
- raise Bobot::FieldFormat.new('quick_replies are required.') unless quick_replies.present?
117
- raise Bobot::FieldFormat.new('quick_replies are limited to 11.') if quick_replies.size > 11
118
- reply(
119
- payload_message: {
120
- text: text,
121
- quick_replies: quick_replies,
122
- },
123
- )
64
+ page.send_quick_replies(text: text, quick_replies: quick_replies, to: sender["id"])
124
65
  end
125
66
 
126
67
  def reply_with_buttons(text:, buttons:)
127
- raise Bobot::FieldFormat.new('text is required.') unless text.present?
128
- raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
129
- raise Bobot::FieldFormat.new('buttons are required.') unless buttons.present?
130
- raise Bobot::FieldFormat.new('buttons are limited to 3.') if buttons.size > 3
131
- reply(
132
- payload_message: {
133
- attachment: {
134
- type: 'template',
135
- payload: {
136
- template_type: 'button',
137
- text: text,
138
- buttons: buttons,
139
- },
140
- },
141
- },
142
- )
68
+ page.send_buttons(text: text, buttons: buttons, to: sender["id"])
143
69
  end
144
70
 
145
71
  def reply_with_generic(elements:, image_aspect_ratio: 'square')
146
- raise Bobot::FieldFormat.new('elements are required.') if elements.nil?
147
- raise Bobot::FieldFormat.new('elements are limited to 10.') if elements.size > 10
148
- raise Bobot::FieldFormat.new('image_aspect_ratio is required.') if image_aspect_ratio.nil?
149
- raise Bobot::FieldFormat.new('image_aspect_ratio is invalid, only "square, horizontal" are permitted.') unless %w[square horizontal].include?(image_aspect_ratio)
150
- reply(
151
- payload_message: {
152
- attachment: {
153
- type: 'template',
154
- payload: {
155
- template_type: 'generic',
156
- image_aspect_ratio: image_aspect_ratio,
157
- elements: elements,
158
- },
159
- },
160
- },
161
- )
72
+ page.send_generic(elements: elements, image_aspect_ratio: image_aspect_ratio, to: sender["id"])
162
73
  end
163
74
  alias_method :reply_with_carousel, :reply_with_generic
164
75
 
165
- def access_token
166
- Bobot.config.find_page_by_id(recipient["id"]).try(:page_access_token)
76
+ def page
77
+ Bobot::Page.find(recipient["id"])
167
78
  end
168
79
  end
169
80
  end
@@ -2,7 +2,7 @@ module Bobot
2
2
  module Event
3
3
  class MessageEcho < Message
4
4
  def access_token
5
- Bobot.config.find_page_by_id(sender["id"]).try(:page_access_token)
5
+ Bobot::Page.find(sender["id"])
6
6
  end
7
7
  end
8
8
  end
@@ -19,10 +19,8 @@ module Bobot
19
19
  end
20
20
  res = https.request(req)
21
21
  json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
22
- if Bobot.config.debug_log
23
- puts "[GET] >> #{uri.request_uri}"
24
- puts "[GET] << #{json}"
25
- end
22
+ Rails.logger.debug "[GET] >> #{uri.request_uri}"
23
+ Rails.logger.debug "[GET] << #{json}"
26
24
  Bobot::ErrorParser.raise_errors_from(json)
27
25
  json
28
26
  end
@@ -44,10 +42,8 @@ module Bobot
44
42
  end
45
43
  res = https.request(req)
46
44
  json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
47
- if Bobot.config.debug_log
48
- puts "[POST] >> #{uri.request_uri}"
49
- puts "[POST] << #{json}"
50
- end
45
+ Rails.logger.debug "[POST] >> #{uri.request_uri}"
46
+ Rails.logger.debug "[POST] << #{json}"
51
47
  Bobot::ErrorParser.raise_errors_from(json)
52
48
  json
53
49
  end
@@ -69,10 +65,8 @@ module Bobot
69
65
  end
70
66
  res = https.request(req)
71
67
  json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
72
- if Bobot.config.debug_log
73
- puts "[DELETE] >> #{uri.request_uri}"
74
- puts "[DELETE] << #{json}"
75
- end
68
+ Rails.logger.debug "[DELETE] >> #{uri.request_uri}"
69
+ Rails.logger.debug "[DELETE] << #{json}"
76
70
  Bobot::ErrorParser.raise_errors_from(json)
77
71
  json
78
72
  end
data/lib/bobot/page.rb ADDED
@@ -0,0 +1,331 @@
1
+ module Bobot
2
+ class Page
3
+ attr_accessor :slug, :language, :page_id, :page_access_token, :get_started_payload
4
+
5
+ def initialize(options = {})
6
+ self.slug = options[:slug]
7
+ self.language = options[:language]
8
+ self.page_id = options[:page_id]
9
+ self.page_access_token = options[:page_access_token]
10
+ self.get_started_payload = options[:get_started_payload]
11
+ end
12
+
13
+ #####################################
14
+ #
15
+ # FINDERS
16
+ #
17
+ #####################################
18
+
19
+ def self.find(page_id)
20
+ Bobot.config.pages.find { |page| page.page_id.to_s == page_id.to_s }
21
+ end
22
+
23
+ def self.find_by_slug(slug)
24
+ Bobot.config.pages.find { |page| page.slug.to_s == slug.to_s }
25
+ end
26
+
27
+ def self.[](value)
28
+ find(value) || find_by_slug(value)
29
+ end
30
+
31
+ #####################################
32
+ #
33
+ # REQUESTS
34
+ #
35
+ #####################################
36
+
37
+ def deliver(payload_template:, to:)
38
+ raise Bobot::FieldFormat.new('payload_template is required.') unless payload_template.present?
39
+ job = Bobot::DeliverJob
40
+ if Bobot.config.async
41
+ job.perform_later(target_facebook_uid: to, access_token: page_access_token, payload_template: payload_template)
42
+ else
43
+ job.perform_now(target_facebook_uid: to, access_token: page_access_token, payload_template: payload_template)
44
+ end
45
+ end
46
+
47
+ def sender_action(sender_action:, to: nil)
48
+ deliver(payload_template: { sender_action: sender_action }, to: to)
49
+ end
50
+
51
+ def show_typing(state:, to: nil)
52
+ sender_action(sender_action: (state ? 'typing_on' : 'typing_off'), to: to)
53
+ end
54
+
55
+ def mark_as_seen(to: nil)
56
+ sender_action(sender_action: 'mark_seen', to: to)
57
+ end
58
+
59
+ def send(payload_message:, to: nil)
60
+ deliver(payload_template: { message: payload_message }, to: to)
61
+ end
62
+
63
+ def send_text(text:, to: nil)
64
+ raise Bobot::FieldFormat.new('text is required.') unless text.present?
65
+ raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
66
+ send(
67
+ payload_message: {
68
+ text: text,
69
+ },
70
+ to: to,
71
+ )
72
+ end
73
+
74
+ def send_attachment(url:, type:, to: nil)
75
+ raise Bobot::FieldFormat.new('url is required.') unless url.present?
76
+ raise Bobot::FieldFormat.new('type is required.') unless type.present?
77
+ raise Bobot::FieldFormat.new('type is invalid, only "image, audio, video, file" are permitted.') unless %w[image audio video file].include?(type)
78
+ send(
79
+ payload_message: {
80
+ attachment: {
81
+ type: type,
82
+ payload: {
83
+ url: url,
84
+ }.tap { |properties| properties.merge!(is_reusable: true) if type == 'image' },
85
+ },
86
+ },
87
+ to: to,
88
+ )
89
+ end
90
+
91
+ def send_image(url:, to: nil)
92
+ send_attachment(url: url, type: 'image', to: to)
93
+ end
94
+
95
+ def send_audio(url:, to: nil)
96
+ send_attachment(url: url, type: 'audio', to: to)
97
+ end
98
+
99
+ def send_video(url:, to: nil)
100
+ send_attachment(url: url, type: 'video', to: to)
101
+ end
102
+
103
+ def send_file(url:, to: nil)
104
+ send_attachment(url: url, type: 'file', to: to)
105
+ end
106
+
107
+ def send_quick_replies(text:, quick_replies:, to: nil)
108
+ raise Bobot::FieldFormat.new('text is required.') unless text.present?
109
+ raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
110
+ raise Bobot::FieldFormat.new('quick_replies are required.') unless quick_replies.present?
111
+ raise Bobot::FieldFormat.new('quick_replies are limited to 11.') if quick_replies.size > 11
112
+ send(
113
+ payload_message: {
114
+ text: text,
115
+ quick_replies: quick_replies,
116
+ },
117
+ to: to,
118
+ )
119
+ end
120
+
121
+ def send_buttons(text:, buttons:, to: nil)
122
+ raise Bobot::FieldFormat.new('text is required.') unless text.present?
123
+ raise Bobot::FieldFormat.new('text length is limited to 640.') if text.size > 640
124
+ raise Bobot::FieldFormat.new('buttons are required.') unless buttons.present?
125
+ raise Bobot::FieldFormat.new('buttons are limited to 3.') if buttons.size > 3
126
+ send(
127
+ payload_message: {
128
+ attachment: {
129
+ type: 'template',
130
+ payload: {
131
+ template_type: 'button',
132
+ text: text,
133
+ buttons: buttons,
134
+ },
135
+ },
136
+ },
137
+ to: to,
138
+ )
139
+ end
140
+
141
+ def send_generic(elements:, image_aspect_ratio: 'square', to: nil)
142
+ raise Bobot::FieldFormat.new('elements are required.') if elements.nil?
143
+ raise Bobot::FieldFormat.new('elements are limited to 10.') if elements.size > 10
144
+ raise Bobot::FieldFormat.new('image_aspect_ratio is required.') if image_aspect_ratio.nil?
145
+ raise Bobot::FieldFormat.new('image_aspect_ratio is invalid, only "square, horizontal" are permitted.') unless %w[square horizontal].include?(image_aspect_ratio)
146
+ send(
147
+ payload_message: {
148
+ attachment: {
149
+ type: 'template',
150
+ payload: {
151
+ template_type: 'generic',
152
+ image_aspect_ratio: image_aspect_ratio,
153
+ elements: elements,
154
+ },
155
+ },
156
+ },
157
+ to: to,
158
+ )
159
+ end
160
+ alias_method :send_carousel, :send_generic
161
+
162
+ #####################################
163
+ #
164
+ # SETUP
165
+ #
166
+ #####################################
167
+ def update_facebook_setup!
168
+ subscribe_to_facebook_page!
169
+ set_greeting_text!
170
+ set_whitelist_domains!
171
+ set_get_started_button!
172
+ set_persistent_menu!
173
+ end
174
+
175
+ ## == Subcribe your bot to your page ==
176
+ def subscribe_to_facebook_page!
177
+ raise Bobot::InvalidParameter.new(:page_id) unless page_id.present?
178
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
179
+ Bobot::Subscription.set(
180
+ query: {
181
+ page_id: page_id,
182
+ access_token: page_access_token,
183
+ },
184
+ )
185
+ end
186
+
187
+ ## == Unsubcribe your bot from your page ==
188
+ def unsubscribe_to_facebook_page!
189
+ raise Bobot::InvalidParameter.new(:page_id) unless page_id.present?
190
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
191
+ Bobot::Subscription.unset(
192
+ query: {
193
+ page_id: page_id,
194
+ access_token: page_access_token,
195
+ },
196
+ )
197
+ end
198
+
199
+ ## == Set bot description (only displayed on first time). ==
200
+ def set_greeting_text!
201
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
202
+ greeting_texts = []
203
+ if language.nil?
204
+ # Default text
205
+ greeting_text = I18n.t("bobot.#{slug}.config.greeting_text", locale: I18n.default_locale, default: nil)
206
+ greeting_texts << { locale: 'default', text: greeting_text } if greeting_text.present?
207
+ # Each languages
208
+ I18n.available_locales.each do |locale|
209
+ greeting_text = I18n.t("bobot.#{slug}.config.greeting_text", locale: locale, default: nil)
210
+ next unless greeting_text.present?
211
+ facebook_locales = I18n.t("bobot.#{slug}.config.facebook_locales", locale: locale, default: nil)
212
+ facebook_locales.to_a.each do |locale_long|
213
+ greeting_texts << { locale: locale_long, text: greeting_text }
214
+ end
215
+ end
216
+ else
217
+ greeting_text = I18n.t("bobot.#{slug}.config.greeting_text", locale: language, default: nil)
218
+ greeting_texts << { locale: 'default', text: greeting_text } if greeting_text.present?
219
+ end
220
+ if greeting_texts.present?
221
+ Bobot::Profile.set(
222
+ body: { greeting: greeting_texts },
223
+ query: { access_token: page_access_token },
224
+ )
225
+ end
226
+ end
227
+
228
+ def unset_greeting_text!
229
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
230
+ Bobot::Profile.unset(
231
+ body: { fields: %w[greeting] },
232
+ query: { access_token: page_access_token },
233
+ )
234
+ end
235
+
236
+ ## == Set bot whitelist domains (only displayed on first time) ==
237
+ ## == Some features like Messenger Extensions and Checkbox Plugin require ==
238
+ ## == a page to specify a domain whitelist. ==
239
+ def set_whitelist_domains!
240
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
241
+ raise Bobot::InvalidParameter.new(:domains) unless Bobot.config.domains.present?
242
+ Bobot::Profile.set(
243
+ body: { whitelisted_domains: Bobot.config.domains },
244
+ query: { access_token: page_access_token },
245
+ )
246
+ end
247
+
248
+ def unset_whitelist_domains!
249
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
250
+ Bobot::Profile.unset(
251
+ body: { fields: ["whitelisted_domains"] },
252
+ query: { access_token: page_access_token },
253
+ )
254
+ end
255
+
256
+ ## == You can define the action to trigger when new humans click on ==
257
+ ## == the Get Started button. Before doing it you should check to select the ==
258
+ ## == messaging_postbacks field when setting up your webhook. ==
259
+ def set_get_started_button!
260
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
261
+ raise Bobot::InvalidParameter.new(:get_started_payload) unless get_started_payload.present?
262
+ Bobot::Profile.set(
263
+ body: { get_started: { payload: get_started_payload } },
264
+ query: { access_token: page_access_token },
265
+ )
266
+ end
267
+
268
+ def unset_get_started_button!
269
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
270
+ Bobot::Profile.unset(
271
+ body: { fields: %w[persistent_menu get_started] },
272
+ query: { access_token: page_access_token },
273
+ )
274
+ end
275
+
276
+ ## == You can show a persistent menu to humans. ==
277
+ ## == If you want to have a persistent menu, you have to set get_started ==
278
+ ## == button before. ==
279
+ def set_persistent_menu!
280
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
281
+ persistent_menus = []
282
+ # Default text
283
+ if language.nil?
284
+ persistent_menu = I18n.t("bobot.#{slug}.config.persistent_menu", locale: I18n.default_locale, default: nil)
285
+ if persistent_menu.present?
286
+ persistent_menus << {
287
+ locale: 'default',
288
+ composer_input_disabled: persistent_menu[:composer_input_disabled],
289
+ call_to_actions: persistent_menu[:call_to_actions],
290
+ }
291
+ end
292
+ # Each languages
293
+ I18n.available_locales.each do |locale|
294
+ persistent_menu = I18n.t("bobot.#{slug}.config.persistent_menu", locale: locale, default: nil)
295
+ facebook_locales = I18n.t("bobot.#{slug}.config.facebook_locales", locale: locale, default: nil)
296
+ next unless persistent_menu.present?
297
+ facebook_locales.to_a.each do |locale_long|
298
+ persistent_menus << {
299
+ locale: locale_long,
300
+ composer_input_disabled: persistent_menu[:composer_input_disabled],
301
+ call_to_actions: persistent_menu[:call_to_actions],
302
+ }
303
+ end
304
+ end
305
+ else
306
+ persistent_menu = I18n.t("bobot.#{slug}.config.persistent_menu", locale: language, default: nil)
307
+ if persistent_menu.present?
308
+ persistent_menus << {
309
+ locale: 'default',
310
+ composer_input_disabled: persistent_menu[:composer_input_disabled],
311
+ call_to_actions: persistent_menu[:call_to_actions],
312
+ }
313
+ end
314
+ end
315
+ if persistent_menus.present?
316
+ Bobot::Profile.set(
317
+ body: { persistent_menu: persistent_menus },
318
+ query: { access_token: page_access_token },
319
+ )
320
+ end
321
+ end
322
+
323
+ def unset_persistent_menu!
324
+ raise Bobot::InvalidParameter.new(:access_token) unless page_access_token.present?
325
+ Bobot::Profile.unset(
326
+ body: { fields: ["persistent_menu"] },
327
+ query: { access_token: page_access_token },
328
+ )
329
+ end
330
+ end
331
+ end