bobot 2.6.2 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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