bobot 1.0.52

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