bobot 1.0.52

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 (109) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +66 -0
  4. data/Rakefile +19 -0
  5. data/app/controllers/bobot/application_controller.rb +5 -0
  6. data/app/controllers/bobot/webhook_controller.rb +76 -0
  7. data/app/jobs/bobot/application_job.rb +4 -0
  8. data/app/jobs/bobot/commander_job.rb +9 -0
  9. data/app/jobs/bobot/deliver_job.rb +16 -0
  10. data/app/mailers/bobot/application_mailer.rb +6 -0
  11. data/app/models/bobot/application_record.rb +5 -0
  12. data/config/locales/bobot.en.yml +28 -0
  13. data/config/routes.rb +6 -0
  14. data/lib/bobot.rb +18 -0
  15. data/lib/bobot/buttons.rb +168 -0
  16. data/lib/bobot/commander.rb +68 -0
  17. data/lib/bobot/configuration.rb +206 -0
  18. data/lib/bobot/engine.rb +33 -0
  19. data/lib/bobot/error_parser.rb +102 -0
  20. data/lib/bobot/event.rb +40 -0
  21. data/lib/bobot/events/account_linking.rb +15 -0
  22. data/lib/bobot/events/common.rb +170 -0
  23. data/lib/bobot/events/delivery.rb +20 -0
  24. data/lib/bobot/events/message.rb +72 -0
  25. data/lib/bobot/events/message_echo.rb +8 -0
  26. data/lib/bobot/events/optin.rb +11 -0
  27. data/lib/bobot/events/postback.rb +20 -0
  28. data/lib/bobot/events/read.rb +15 -0
  29. data/lib/bobot/events/referral.rb +33 -0
  30. data/lib/bobot/exceptions.rb +73 -0
  31. data/lib/bobot/graph_facebook.rb +90 -0
  32. data/lib/bobot/profile.rb +23 -0
  33. data/lib/bobot/subscription.rb +19 -0
  34. data/lib/bobot/user.rb +13 -0
  35. data/lib/bobot/version.rb +14 -0
  36. data/lib/generators/bobot/install_generator.rb +28 -0
  37. data/lib/generators/bobot/templates/app/bobot/message.rb +3 -0
  38. data/lib/generators/bobot/templates/app/bobot/postback.rb +22 -0
  39. data/lib/generators/bobot/templates/app/bobot/workflow.rb +17 -0
  40. data/lib/generators/bobot/templates/config/bobot.yml +39 -0
  41. data/lib/generators/bobot/templates/config/initializers/bobot.rb +30 -0
  42. data/lib/generators/bobot/templates/config/locales/bobot.en.yml +30 -0
  43. data/lib/generators/bobot/templates/config/locales/bobot.fr.yml +29 -0
  44. data/lib/generators/bobot/uninstall_generator.rb +24 -0
  45. data/lib/generators/bobot/utils.rb +30 -0
  46. data/lib/tasks/bobot_tasks.rake +11 -0
  47. data/spec/bobot/bobot_spec.rb +24 -0
  48. data/spec/bobot/event/account_linking_spec.rb +59 -0
  49. data/spec/bobot/event/common_spec.rb +259 -0
  50. data/spec/bobot/event/delivery_spec.rb +62 -0
  51. data/spec/bobot/event/message_echo_spec.rb +276 -0
  52. data/spec/bobot/event/message_spec.rb +276 -0
  53. data/spec/bobot/event/optin_spec.rb +50 -0
  54. data/spec/bobot/event/postback_spec.rb +94 -0
  55. data/spec/bobot/event/read_spec.rb +51 -0
  56. data/spec/bobot/event/referral_spec.rb +66 -0
  57. data/spec/bobot/event_spec.rb +167 -0
  58. data/spec/bobot/install_generator_spec.rb +43 -0
  59. data/spec/bobot/profile_spec.rb +170 -0
  60. data/spec/bobot/subscription_spec.rb +139 -0
  61. data/spec/bobot/user_spec.rb +91 -0
  62. data/spec/controllers/bobot/application_controller_spec.rb +4 -0
  63. data/spec/controllers/bobot/webhook_controller_spec.rb +5 -0
  64. data/spec/dummy/Rakefile +6 -0
  65. data/spec/dummy/app/assets/config/manifest.js +3 -0
  66. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  67. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  68. data/spec/dummy/app/bobot/workflow.rb +17 -0
  69. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  70. data/spec/dummy/app/jobs/application_job.rb +2 -0
  71. data/spec/dummy/app/models/application_record.rb +3 -0
  72. data/spec/dummy/bin/bundle +3 -0
  73. data/spec/dummy/bin/rails +4 -0
  74. data/spec/dummy/bin/rake +4 -0
  75. data/spec/dummy/bin/setup +35 -0
  76. data/spec/dummy/bin/update +29 -0
  77. data/spec/dummy/config.ru +5 -0
  78. data/spec/dummy/config/application.rb +30 -0
  79. data/spec/dummy/config/bobot.yml +27 -0
  80. data/spec/dummy/config/boot.rb +5 -0
  81. data/spec/dummy/config/database.yml +19 -0
  82. data/spec/dummy/config/environment.rb +5 -0
  83. data/spec/dummy/config/environments/development.rb +42 -0
  84. data/spec/dummy/config/environments/production.rb +78 -0
  85. data/spec/dummy/config/environments/test.rb +38 -0
  86. data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
  87. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  88. data/spec/dummy/config/initializers/bobot.rb +30 -0
  89. data/spec/dummy/config/initializers/cors.rb +16 -0
  90. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  91. data/spec/dummy/config/initializers/inflections.rb +16 -0
  92. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  93. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  94. data/spec/dummy/config/locales/bobot.en.yml +28 -0
  95. data/spec/dummy/config/locales/bobot.fr.yml +27 -0
  96. data/spec/dummy/config/locales/en.yml +33 -0
  97. data/spec/dummy/config/puma.rb +56 -0
  98. data/spec/dummy/config/routes.rb +3 -0
  99. data/spec/dummy/config/secrets.yml +32 -0
  100. data/spec/dummy/config/spring.rb +6 -0
  101. data/spec/dummy/db/schema.rb +15 -0
  102. data/spec/examples.txt +111 -0
  103. data/spec/helpers/graph_api_helpers.rb +6 -0
  104. data/spec/jobs/bobot/commander_job_spec.rb +31 -0
  105. data/spec/lint/rubocop_spec.rb +8 -0
  106. data/spec/rails_helper.rb +67 -0
  107. data/spec/spec_helper.rb +105 -0
  108. data/spec/travis/database.travis.mysql.yml +19 -0
  109. metadata +251 -0
@@ -0,0 +1,68 @@
1
+ module Bobot
2
+ module Commander
3
+ class Error < Bobot::FacebookError; end
4
+
5
+ EVENTS = %i[
6
+ message
7
+ delivery
8
+ postback
9
+ optin
10
+ read
11
+ account_linking
12
+ referral
13
+ message_echo
14
+ ].freeze
15
+
16
+ include Bobot::GraphFacebook
17
+
18
+ class << self
19
+ def deliver(body:, query:)
20
+ graph_post '/me/messages', body: body, query: {
21
+ access_token: query.fetch(:access_token),
22
+ }
23
+ end
24
+
25
+ def on(event, &block)
26
+ unless EVENTS.include? event
27
+ raise Error.new("#{event} is not a valid event; available events are #{EVENTS.join(',')}")
28
+ end
29
+ hooks[event] = block
30
+ end
31
+
32
+ def receive(payload)
33
+ event = Bobot::Event.parse(payload)
34
+ hooks.fetch(Bobot::Event::EVENTS.invert[event.class].to_sym)
35
+ puts "[ActiveJob] << Bobot::HookJob with event #{event.class}" if Bobot.debug_log
36
+ #event.mark_as_seen
37
+ job = Bobot::CommanderJob
38
+ if Bobot.async
39
+ job.perform_later(payload: payload)
40
+ else
41
+ job.perform_now(payload: payload)
42
+ end
43
+ rescue KeyError
44
+ $stderr.puts "Ignoring #{event.class} (no hook registered)"
45
+ end
46
+
47
+ def trigger(payload)
48
+ event = Bobot::Event.parse(payload)
49
+ hook = hooks.fetch(Bobot::Event::EVENTS.invert[event.class].to_sym)
50
+ puts "[ActiveJob] >> Bobot::HookJob related to event #{name.class}" if Bobot.debug_log
51
+ #event.show_typing(state: true)
52
+ hook.call(event)
53
+ #event.show_typing(state: false)
54
+ [event, event.payloads_sent[1..-2]]
55
+ rescue KeyError
56
+ $stderr.puts "Ignoring #{event.class} (no hook registered)"
57
+ end
58
+
59
+ def hooks
60
+ @hooks ||= {}
61
+ end
62
+
63
+ def unhook
64
+ @hooks = {}
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,206 @@
1
+ module Bobot
2
+ # Defines constants and methods related to configuration
3
+ module Configuration
4
+ # An array of valid keys in the options hash when configuring
5
+ VALID_CONFIGURATION_KEYS = %i[
6
+ page_id
7
+ app_id
8
+ app_secret
9
+ page_access_token
10
+ verify_token
11
+ domains
12
+ debug_log
13
+ async
14
+ ].freeze
15
+
16
+ # By default, don't set a facebook page id
17
+ DEFAULT_PAGE_ID = nil
18
+
19
+ # By default, don't set a facebook developper app id
20
+ DEFAULT_APP_ID = nil
21
+
22
+ # By default, don't set a facebook developper app secret
23
+ DEFAULT_APP_SECRET = nil
24
+
25
+ # By default, don't set a facebook developper page access token
26
+ DEFAULT_PAGE_ACCESS_TOKEN = nil
27
+
28
+ # By default, don't set a facebook webhook_verify_token
29
+ DEFAULT_VERIFY_TOKEN = nil
30
+
31
+ # By default, don't set a facebook whitelisting domains
32
+ DEFAULT_DOMAINS = nil
33
+
34
+ # By default, debug log is to false
35
+ DEFAULT_DEBUG_LOG = false
36
+
37
+ # By default, async is to false
38
+ DEFAULT_ASYNC = false
39
+
40
+ # @private
41
+ attr_accessor(*VALID_CONFIGURATION_KEYS)
42
+
43
+ # Our host application root path
44
+ # We set this when the engine is initialized
45
+ mattr_accessor :app_root
46
+
47
+ # When this module is extended, set all configuration options to their default values
48
+ def self.extended(base)
49
+ base.reset!
50
+ end
51
+
52
+ # Convenience method to allow configuration options to be set in a block
53
+ def configure
54
+ yield self
55
+ end
56
+
57
+ # Custom self assignments
58
+ def domains=(rhs)
59
+ return unless rhs.present?
60
+ if rhs.respond_to?(:to_str)
61
+ @domains = rhs.split(",").map(&:strip)
62
+ elsif rhs.is_a?(Array)
63
+ @domains = rhs
64
+ else
65
+ raise Bobot::InvalidParameter.new(:domains, "should be a string or an array")
66
+ end
67
+ end
68
+
69
+ # Create a hash of options and their values
70
+ def configurations
71
+ VALID_CONFIGURATION_KEYS.inject({}) do |option, key|
72
+ option.merge!(key => send(key))
73
+ end
74
+ end
75
+
76
+ # Reset all configuration options to defaults
77
+ def reset!
78
+ self.page_id = DEFAULT_PAGE_ID
79
+ self.app_id = DEFAULT_APP_ID
80
+ self.app_secret = DEFAULT_APP_SECRET
81
+ self.page_access_token = DEFAULT_PAGE_ACCESS_TOKEN
82
+ self.verify_token = DEFAULT_VERIFY_TOKEN
83
+ self.domains = DEFAULT_DOMAINS
84
+ self.debug_log = DEFAULT_DEBUG_LOG
85
+ self.async = DEFAULT_ASYNC
86
+ end
87
+
88
+ def update_facebook_setup!
89
+ subscribe_to_facebook_page!
90
+ set_greeting_text!
91
+ set_whitelist_domains!
92
+ set_get_started_button!
93
+ set_persistent_menu!
94
+ end
95
+
96
+ ## == Subcribe your bot to your page ==
97
+ def subscribe_to_facebook_page!
98
+ raise Bobot::InvalidParameter.new(:page_id) unless Bobot.page_id.present?
99
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
100
+ Bobot::Subscription.set(
101
+ query: {
102
+ page_id: Bobot.page_id,
103
+ access_token: Bobot.page_access_token,
104
+ },
105
+ )
106
+ end
107
+
108
+ ## == Unsubcribe your bot from your page ==
109
+ def unsubscribe_to_facebook_page!
110
+ raise Bobot::InvalidParameter.new(:page_id) unless Bobot.page_id.present?
111
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
112
+ Bobot::Subscription.unset(
113
+ query: {
114
+ page_id: Bobot.page_id,
115
+ access_token: Bobot.page_access_token,
116
+ },
117
+ )
118
+ end
119
+
120
+ ## == Set bot description (only displayed on first time). ==
121
+ def set_greeting_text!
122
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
123
+ greeting_texts = []
124
+ # Default text
125
+ greeting_text = I18n.t('bobot.config.greeting_text', locale: I18n.default_locale, default: nil)
126
+ if greeting_text.present?
127
+ greeting_texts << {
128
+ locale: 'default',
129
+ text: greeting_text,
130
+ }
131
+ end
132
+ # Each languages
133
+ I18n.available_locales.each do |locale|
134
+ greeting_text = I18n.t('bobot.config.greeting_text', locale: locale, default: nil)
135
+ next unless greeting_text.present?
136
+ facebook_locales = I18n.t('bobot.config.facebook_locales', locale: locale, default: nil)
137
+ facebook_locales.to_a.each do |locale_long|
138
+ greeting_texts << { locale: locale_long, text: greeting_text }
139
+ end
140
+ end
141
+ raise Bobot::InvalidParameter.new(:greeting_texts) unless greeting_texts.present?
142
+ Bobot::Profile.set(
143
+ body: { greeting: greeting_texts },
144
+ query: { access_token: Bobot.page_access_token },
145
+ )
146
+ end
147
+
148
+ ## == Set bot whitelist domains (only displayed on first time) ==
149
+ ## == Some features like Messenger Extensions and Checkbox Plugin require ==
150
+ ## == a page to specify a domain whitelist. ==
151
+ def set_whitelist_domains!
152
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
153
+ raise Bobot::InvalidParameter.new(:domains) unless Bobot.domains.present?
154
+ Bobot::Profile.set(
155
+ body: { whitelisted_domains: Bobot.domains },
156
+ query: { access_token: Bobot.page_access_token },
157
+ )
158
+ end
159
+
160
+ ## == You can define the action to trigger when new humans click on ==
161
+ ## == the Get Started button. Before doing it you should check to select the ==
162
+ ## == messaging_postbacks field when setting up your webhook. ==
163
+ def set_get_started_button!
164
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
165
+ Bobot::Profile.set(
166
+ body: { get_started: { payload: I18n.t('bobot.config.get_started.payload') } },
167
+ query: { access_token: Bobot.page_access_token },
168
+ )
169
+ end
170
+
171
+ ## == You can show a persistent menu to humans. ==
172
+ ## == If you want to have a persistent menu, you have to set get_started ==
173
+ ## == button before. ==
174
+ def set_persistent_menu!
175
+ raise Bobot::InvalidParameter.new(:access_token) unless Bobot.page_access_token.present?
176
+ persistent_menus = []
177
+ # Default text
178
+ persistent_menu = I18n.t('bobot.config.persistent_menu', locale: I18n.default_locale, default: nil)
179
+ if persistent_menu.present?
180
+ persistent_menus << {
181
+ locale: 'default',
182
+ composer_input_disabled: persistent_menu[:composer_input_disabled],
183
+ call_to_actions: persistent_menu[:call_to_actions],
184
+ }
185
+ end
186
+ # Each languages
187
+ I18n.available_locales.each do |locale|
188
+ persistent_menu = I18n.t('bobot.config.persistent_menu', locale: locale, default: nil)
189
+ facebook_locales = I18n.t('bobot.config.facebook_locales', locale: locale, default: nil)
190
+ next unless persistent_menu.present?
191
+ facebook_locales.to_a.each do |locale_long|
192
+ persistent_menus << {
193
+ locale: locale_long,
194
+ composer_input_disabled: persistent_menu[:composer_input_disabled],
195
+ call_to_actions: persistent_menu[:call_to_actions],
196
+ }
197
+ end
198
+ end
199
+ raise Bobot::InvalidParameter.new(:persistent_menus) unless persistent_menus.present?
200
+ Bobot::Profile.set(
201
+ body: { persistent_menu: persistent_menus },
202
+ query: { access_token: Bobot.page_access_token },
203
+ )
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,33 @@
1
+ require 'rails'
2
+ require 'bobot'
3
+
4
+ module Bobot
5
+ class Engine < ::Rails::Engine
6
+ isolate_namespace Bobot
7
+ config.generators.api_only = true
8
+
9
+ config.action_dispatch.rescue_responses['Bobot::ActionNotAllowed'] = :forbidden
10
+
11
+ initializer 'bobot.load_app_root' do |app|
12
+ Bobot.app_root = app.root
13
+ end
14
+
15
+ initializer 'Bobot setup middlewares' do |app|
16
+ app.config.middleware.use ActionDispatch::Flash
17
+ end
18
+
19
+ config.generators do |g|
20
+ g.test_framework :rspec,
21
+ fixtures: true,
22
+ view_specs: false,
23
+ helper_specs: false,
24
+ routing_specs: false,
25
+ controller_specs: true,
26
+ request_specs: true
27
+ end
28
+
29
+ rake_tasks do
30
+ Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,102 @@
1
+ module Bobot
2
+ # Parses and raises Facebook response errors for the send API.
3
+ class ErrorParser
4
+ INTERNAL_ERROR_CODES = {
5
+ 1200 => [],
6
+ }.freeze
7
+
8
+ ACCESS_TOKEN_ERROR_CODES = {
9
+ 190 => [],
10
+ }.freeze
11
+
12
+ ACCOUNT_LINKING_ERROR_CODES = {
13
+ 10_303 => [],
14
+ }.freeze
15
+
16
+ LIMIT_ERROR_CODES = {
17
+ 4 => [2_018_022],
18
+ 100 => [2_018_109],
19
+ 613 => [nil],
20
+ }.freeze
21
+
22
+ BAD_PARAMETER_ERROR_CODES = {
23
+ 100 => [nil, 2_018_001],
24
+ }.freeze
25
+
26
+ PERMISSION_ERROR_CODES = {
27
+ 10 => [2_018_065, 2_018_108],
28
+ 200 => [1_545_041, 2_018_028, 2_018_027, 2_018_021],
29
+ 230 => [nil],
30
+ }.freeze
31
+
32
+ class << self
33
+ # Raise any errors in the given response.
34
+ #
35
+ # response - A HTTParty::Response object.
36
+ #
37
+ # Returns nil if no errors were found, otherwises raises appropriately
38
+ def raise_errors_from(response)
39
+ return false unless response.key?('error')
40
+
41
+ error = response['error']
42
+
43
+ error_code = error['code']
44
+ error_subcode = error['error_subcode']
45
+
46
+ raise_code_only_error(error_code, error) if error_subcode.nil?
47
+
48
+ raise_code_subcode_error(error_code, error_subcode, error)
49
+
50
+ # Default to unidentified error
51
+ raise FacebookError.new(error)
52
+ end
53
+
54
+ private
55
+
56
+ def raise_code_only_error(error_code, args)
57
+ raise InternalError.new(args) if internal_error?(error_code)
58
+ raise AccessTokenError.new(args) if access_token_error?(error_code)
59
+ raise AccountLinkingError.new(args) if account_linking_error?(error_code)
60
+ end
61
+
62
+ def raise_code_subcode_error(error_code, error_subcode, args)
63
+ raise LimitError.new(args) if limit_error?(error_code, error_subcode)
64
+ raise BadParameterError.new(args) if bad_parameter_error?(error_code, error_subcode)
65
+ raise PermissionError.new(args) if permission_error?(error_code, error_subcode)
66
+ end
67
+
68
+ def internal_error?(error_code)
69
+ INTERNAL_ERROR_CODES.keys.include? error_code
70
+ end
71
+
72
+ def access_token_error?(error_code)
73
+ ACCESS_TOKEN_ERROR_CODES.keys.include? error_code
74
+ end
75
+
76
+ def account_linking_error?(error_code)
77
+ ACCOUNT_LINKING_ERROR_CODES.keys.include? error_code
78
+ end
79
+
80
+ def limit_error?(error_code, error_subcode)
81
+ limit_errors = LIMIT_ERROR_CODES[error_code]
82
+ return unless limit_errors
83
+
84
+ limit_errors.include? error_subcode
85
+ end
86
+
87
+ def bad_parameter_error?(error_code, error_subcode)
88
+ bad_parameter_errors = BAD_PARAMETER_ERROR_CODES[error_code]
89
+ return unless bad_parameter_errors
90
+
91
+ bad_parameter_errors.include? error_subcode
92
+ end
93
+
94
+ def permission_error?(error_code, error_subcode)
95
+ permission_errors = PERMISSION_ERROR_CODES[error_code]
96
+ return unless permission_errors
97
+
98
+ permission_errors.include? error_subcode
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,40 @@
1
+ require 'bobot/events/common'
2
+ require 'bobot/events/message'
3
+ require 'bobot/events/message_echo'
4
+ require 'bobot/events/delivery'
5
+ require 'bobot/events/postback'
6
+ require 'bobot/events/optin'
7
+ require 'bobot/events/read'
8
+ require 'bobot/events/account_linking'
9
+ require 'bobot/events/referral'
10
+
11
+ module Bobot
12
+ module Event
13
+ EVENTS = {
14
+ 'message' => Message,
15
+ 'delivery' => Delivery,
16
+ 'postback' => Postback,
17
+ 'optin' => Optin,
18
+ 'read' => Read,
19
+ 'account_linking' => AccountLinking,
20
+ 'referral' => Referral,
21
+ 'message_echo' => MessageEcho,
22
+ }.freeze
23
+
24
+ def self.parse(payload)
25
+ return MessageEcho.new(payload) if payload_is_echo?(payload)
26
+
27
+ EVENTS.each do |event, klass|
28
+ return klass.new(payload) if payload.key?(event)
29
+ end
30
+
31
+ raise UnknownPayload.new(payload)
32
+ end
33
+
34
+ def self.payload_is_echo?(payload)
35
+ payload.key?('message') && payload['message']['is_echo'] == true
36
+ end
37
+
38
+ class UnknownPayload < Bobot::Error; end
39
+ end
40
+ end