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,90 @@
1
+ module Bobot
2
+ module GraphFacebook
3
+ GRAPH_FB_URL = 'https://graph.facebook.com'.freeze
4
+ GRAPH_FB_VERSION = 'v2.10'.freeze
5
+
6
+ module ClassMethods
7
+ def graph_get(path, query: {})
8
+ uri = URI.parse(File.join(GRAPH_FB_URL, GRAPH_FB_VERSION, path))
9
+ uri.query = URI.encode_www_form(query)
10
+ req = Net::HTTP::Get.new(uri.request_uri)
11
+ req['Content-Type'] = 'application/json; charset=utf-8'
12
+ req['Accept'] = 'application/json'
13
+ req['User-Agent'] = nil
14
+ req['Accept-Encoding'] = nil
15
+ https = Net::HTTP.new(uri.host, uri.port).tap do |http|
16
+ http.use_ssl = true
17
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
18
+ http.read_timeout = 300
19
+ end
20
+ res = https.request(req)
21
+ json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
22
+ if Bobot.debug_log
23
+ puts "[GET] >> #{uri.request_uri}"
24
+ puts "[GET] << #{json}"
25
+ end
26
+ Bobot::ErrorParser.raise_errors_from(json)
27
+ json
28
+ end
29
+ module_function :graph_get
30
+
31
+ def graph_post(path, query: {}, body: {})
32
+ uri = URI.parse(File.join(GRAPH_FB_URL, GRAPH_FB_VERSION, path))
33
+ uri.query = URI.encode_www_form(query)
34
+ req = Net::HTTP::Post.new(uri.request_uri)
35
+ req['Content-Type'] = 'application/json; charset=utf-8'
36
+ req['Accept'] = 'application/json'
37
+ req['User-Agent'] = nil
38
+ req['Accept-Encoding'] = nil
39
+ req.body = ActiveSupport::JSON.encode(body)
40
+ https = Net::HTTP.new(uri.host, uri.port).tap do |http|
41
+ http.use_ssl = true
42
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
43
+ http.read_timeout = 300
44
+ end
45
+ res = https.request(req)
46
+ json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
47
+ if Bobot.debug_log
48
+ puts "[POST] >> #{uri.request_uri}"
49
+ puts "[POST] << #{json}"
50
+ end
51
+ Bobot::ErrorParser.raise_errors_from(json)
52
+ json
53
+ end
54
+ module_function :graph_post
55
+
56
+ def graph_delete(path, query: {}, body: {})
57
+ uri = URI.parse(File.join(GRAPH_FB_URL, GRAPH_FB_VERSION, path))
58
+ uri.query = URI.encode_www_form(query)
59
+ req = Net::HTTP::Delete.new(uri.request_uri)
60
+ req['Content-Type'] = 'application/json; charset=utf-8'
61
+ req['Accept'] = 'application/json'
62
+ req['User-Agent'] = nil
63
+ req['Accept-Encoding'] = nil
64
+ req.body = ActiveSupport::JSON.encode(body)
65
+ https = Net::HTTP.new(uri.host, uri.port).tap do |http|
66
+ http.use_ssl = true
67
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
68
+ http.read_timeout = 300
69
+ end
70
+ res = https.request(req)
71
+ json = ActiveSupport::JSON.decode(res.send(:body) || '{}')
72
+ if Bobot.debug_log
73
+ puts "[DELETE] >> #{uri.request_uri}"
74
+ puts "[DELETE] << #{json}"
75
+ end
76
+ Bobot::ErrorParser.raise_errors_from(json)
77
+ json
78
+ end
79
+ module_function :graph_delete
80
+ end
81
+
82
+ module InstanceMethods
83
+ end
84
+
85
+ def self.included(receiver)
86
+ receiver.extend ClassMethods
87
+ receiver.send :include, InstanceMethods
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,23 @@
1
+ module Bobot
2
+ module Profile
3
+ include Bobot::GraphFacebook
4
+
5
+ def set(body:, query: nil)
6
+ query ||= { access_token: Bobot.page_access_token }
7
+ query[:access_token] = Bobot.page_access_token unless query.key?("access_token")
8
+ graph_post '/me/messenger_profile', body: body, query: {
9
+ access_token: query.fetch(:access_token),
10
+ }
11
+ end
12
+ module_function :set
13
+
14
+ def unset(body:, query: nil)
15
+ query ||= { access_token: Bobot.page_access_token }
16
+ query[:access_token] = Bobot.page_access_token unless query.key?("access_token")
17
+ graph_delete '/me/messenger_profile', body: body, query: {
18
+ access_token: query.fetch(:access_token),
19
+ }
20
+ end
21
+ module_function :unset
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ module Bobot
2
+ module Subscription
3
+ include Bobot::GraphFacebook
4
+
5
+ def set(query: {})
6
+ graph_post "/#{query.fetch(:page_id)}/subscribed_apps", query: {
7
+ access_token: query.fetch(:access_token),
8
+ }
9
+ end
10
+ module_function :set
11
+
12
+ def unset(query: {})
13
+ graph_delete "/#{query.fetch(:page_id)}/subscribed_apps", query: {
14
+ access_token: query.fetch(:access_token),
15
+ }
16
+ end
17
+ module_function :unset
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ module Bobot
2
+ module User
3
+ include Bobot::GraphFacebook
4
+
5
+ def get_profile(query:)
6
+ graph_get "/#{query.fetch(:fb_id)}", query: {
7
+ fields: query.fetch(:fields).join(','),
8
+ access_token: query.fetch(:access_token),
9
+ }
10
+ end
11
+ module_function :get_profile
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Bobot
2
+ class Version
3
+ MAJOR = 1
4
+ MINOR = 0
5
+ PATCH = 52
6
+ PRE = nil
7
+
8
+ class << self
9
+ def to_s
10
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+ require 'rails/generators'
3
+ require File.expand_path('../utils', __FILE__)
4
+
5
+ module Bobot
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ include Generators::Utils::InstanceMethods
9
+ extend Generators::Utils::ClassMethods
10
+ source_root File.expand_path('../templates', __FILE__)
11
+
12
+ argument :_namespace, type: :string, required: false, desc: 'Bot url namespace'
13
+ desc 'Bobot install'
14
+
15
+ def install
16
+ namespace = ask_for('Where do you want to mount bobot?', 'bot', _namespace)
17
+ create_file 'config/routes.rb'
18
+ gsub_file 'config/routes.rb', %r{mount Bobot::Engine => \'\/.+\'(, as: \'bot\')?}, ''
19
+ route("mount Bobot::Engine => '/#{namespace}', as: 'bot'")
20
+ template 'config/initializers/bobot.rb', 'config/initializers/bobot.rb'
21
+ template 'app/bobot/workflow.rb', 'app/bobot/workflow.rb'
22
+ copy_file 'config/bobot.yml', 'config/bobot.yml'
23
+ copy_file 'config/locales/bobot.en.yml', 'config/locales/bobot.en.yml'
24
+ copy_file 'config/locales/bobot.fr.yml', 'config/locales/bobot.fr.yml'
25
+ display 'Installation done', :green
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ class Message
2
+ def perform(message); end
3
+ end
@@ -0,0 +1,22 @@
1
+ module Bobot
2
+ class Postback
3
+ def self.perform(event)
4
+ payload = event.payload
5
+ begin
6
+ payload = ActiveSupport::JSON.decode(payload)
7
+ step = payload["step"]
8
+ params = payload["params"].hash
9
+ rescue ::ActiveSupport::JSON.parse_error
10
+ step = payload.to_sym
11
+ params = nil
12
+ end
13
+
14
+ bot = Postback.new(event)
15
+ if bot.respond_to?(step)
16
+ bot.public_send(*([step, params].compact))
17
+ else
18
+ bot.unknown_payload
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ Bobot::Commander.on :message do |message|
2
+ klass = message.quick_reply.present? ? Bobot::Postback : Bobot::Message
3
+ klass.perform(message)
4
+ end
5
+
6
+ Bobot::Commander.on :postback do |postback|
7
+ klass = Bobot::Postback
8
+ klass.perform(postback)
9
+ end
10
+
11
+ # Bobot::Commander.on :optin do |optin|
12
+ # optin.reply_with_text(text: 'Ah, human! Clicked on Send To Messenger')
13
+ # end
14
+ #
15
+ # Bobot::Commander.on :referral do |referral|
16
+ # optin.reply_with_text(text: "Great you came from #{referal.ref}")
17
+ # end
@@ -0,0 +1,39 @@
1
+ development:
2
+ app_id: "123"
3
+ app_secret: "456"
4
+ page_id: "789"
5
+ page_access_token: "abc"
6
+ verify_token: "your token"
7
+ domains: "whitelisted-domain.com,second-whitelisted-domain.com"
8
+ debug_log: true
9
+ async: false
10
+
11
+ test:
12
+ app_id: "123"
13
+ app_secret: "456"
14
+ page_id: "789"
15
+ page_access_token: "abc"
16
+ verify_token: "your token"
17
+ domains: "whitelisted-domain.com,second-whitelisted-domain.com"
18
+ debug_log: true
19
+ async: false
20
+
21
+ staging:
22
+ app_id: <%= ENV["STAGING_BOBOT_APP_ID"] %>
23
+ app_secret: <%= ENV["STAGING_BOBOT_APP_SECRET"] %>
24
+ page_id: <%= ENV["STAGING_BOBOT_PAGE_ID"] %>
25
+ page_access_token: <%= ENV["STAGING_BOBOT_PAGE_ACCESS_TOKEN"] %>
26
+ verify_token: <%= ENV["STAGING_BOBOT_VERIFY_TOKEN"] %>
27
+ domains: <%= ENV["STAGING_BOBOT_DOMAINS"] %>
28
+ debug_log: <%= ENV["STAGING_BOBOT_DEBUG_LOG"] %>
29
+ async: <%= ENV["STAGING_BOBOT_ASYNC"] %>
30
+
31
+ production:
32
+ app_id: <%= ENV["PRODUCTION_BOBOT_APP_ID"] %>
33
+ app_secret: <%= ENV["PRODUCTION_BOBOT_APP_SECRET"] %>
34
+ page_id: <%= ENV["PRODUCTION_BOBOT_PAGE_ID"] %>
35
+ page_access_token: <%= ENV["PRODUCTION_BOBOT_PAGE_ACCESS_TOKEN"] %>
36
+ verify_token: <%= ENV["PRODUCTION_BOBOT_VERIFY_TOKEN"] %>
37
+ domains: <%= ENV["PRODUCTION_BOBOT_DOMAINS"] %>
38
+ debug_log: <%= ENV["PRODUCTION_BOBOT_DEBUG_LOG"] %>
39
+ async: <%= ENV["PRODUCTION_BOBOT_ASYNC"] %>
@@ -0,0 +1,30 @@
1
+ bobot_config_path = Rails.root.join("config", "bobot.yml")
2
+ bobot_config = YAML.safe_load(ERB.new(File.read(bobot_config_path)).result)[Rails.env]
3
+
4
+ if bobot_config.present?
5
+ Bobot.configure do |config|
6
+ config.app_id = bobot_config["app_id"]
7
+ config.app_secret = bobot_config["app_secret"]
8
+ config.page_access_token = bobot_config["page_access_token"]
9
+ config.page_id = bobot_config["page_id"]
10
+ config.verify_token = bobot_config["verify_token"]
11
+ config.domains = bobot_config["domains"]
12
+ config.debug_log = bobot_config["debug_log"]
13
+ config.async = bobot_config["async"]
14
+ end
15
+ else
16
+ warn "#{bobot_config_path} not configured yet in #{Rails.env} environment."
17
+ end
18
+
19
+ unless Rails.env.production?
20
+ bot_files = Dir[Rails.root.join("app", "bobot", "**", "*.rb")]
21
+ bot_reloader = ActiveSupport::FileUpdateChecker.new(bot_files) do
22
+ bot_files.each { |file| require_dependency file }
23
+ end
24
+
25
+ ActiveSupport::Reloader.to_prepare do
26
+ bot_reloader.execute_if_updated
27
+ end
28
+
29
+ bot_files.each { |file| require_dependency file }
30
+ end
@@ -0,0 +1,30 @@
1
+ en:
2
+ bobot:
3
+ config:
4
+ facebook_locales:
5
+ - "en_US"
6
+ - "en_UD"
7
+ - "en_GB"
8
+ greeting_text: "Bobot is an intelligent robot."
9
+ get_started:
10
+ payload: 'get_started'
11
+ persistent_menu:
12
+ composer_input_disabled: false
13
+ call_to_actions:
14
+ - title: "My Account"
15
+ type: "nested"
16
+ call_to_actions:
17
+ - title: "What is a chatbot?"
18
+ type: "postback"
19
+ payload: "WHAT_IS_A_CHATBOT"
20
+ - title: "History"
21
+ type: "postback"
22
+ payload: "HISTORY_PAYLOAD"
23
+ - title: "Contact Info"
24
+ type: "postback"
25
+ payload: "CONTACT_INFO_PAYLOAD"
26
+ - type: "web_url"
27
+ title: "Get some help"
28
+ url: "https://github.com/navidemad/bobot"
29
+ webview_height_ratio: "full"
30
+ what_is_a_chatbot: "A chatbot is a computer program which conducts a conversation via auditory or textual methods."
@@ -0,0 +1,29 @@
1
+ fr:
2
+ bobot:
3
+ config:
4
+ facebook_locales:
5
+ - "fr_FR"
6
+ - "fr_CA"
7
+ greeting_text: 'Bobot est un robot intelligent.'
8
+ get_started:
9
+ payload: 'get_started'
10
+ persistent_menu:
11
+ composer_input_disabled: false
12
+ call_to_actions:
13
+ - title: "Mon compte"
14
+ type: "nested"
15
+ call_to_actions:
16
+ - title: "C'est quoi un chatbot ?"
17
+ type: "postback"
18
+ payload: "WHAT_IS_A_CHATBOT"
19
+ - title: "Historique"
20
+ type: "postback"
21
+ payload: "HISTORY_PAYLOAD"
22
+ - title: "Contact Info"
23
+ type: "postback"
24
+ payload: "CONTACT_INFO_PAYLOAD"
25
+ - type: "web_url"
26
+ title: "Obtenir de l'aide"
27
+ url: "https://github.com/navidemad/bobot"
28
+ webview_height_ratio: "full"
29
+ what_is_a_chatbot: "Un chatbot est un programme qui tente de converser avec une personne durant quelques minutes ou plus en lui donnant l'impression de converser elle-même avec une personne."
@@ -0,0 +1,24 @@
1
+ require 'rails/generators'
2
+ require File.expand_path('../utils', __FILE__)
3
+
4
+ module Bobot
5
+ class UninstallGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+ include Generators::Utils::InstanceMethods
8
+ extend Generators::Utils::ClassMethods
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ desc 'Bobot uninstall'
12
+
13
+ def uninstall
14
+ display 'Why you leaving so soon? :('
15
+ gsub_file 'config/routes.rb', %r{mount Bobot::Engine => \'\/.+\'(, as: \'bot\')?}, ''
16
+ remove_file 'config/initializers/bobot.rb'
17
+ remove_file 'app/bobot/workflow.rb'
18
+ remove_file 'config/bobot.yml'
19
+ remove_file 'config/locales/bobot.en.yml'
20
+ remove_file 'config/locales/bobot.fr.yml'
21
+ display 'Done! Bobot has been uninstalled.'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,30 @@
1
+ module Bobot
2
+ module Generators
3
+ module Utils
4
+ module InstanceMethods
5
+ def display(output, color = :green)
6
+ say(" - #{output}", color)
7
+ end
8
+
9
+ def ask_for(wording, default_value = nil, override_if_present_value = nil)
10
+ if override_if_present_value.present?
11
+ display("Using [#{override_if_present_value}] for question '#{wording}'") && override_if_present_value
12
+ else
13
+ ask(" ? #{wording} Press <enter> for [#{default_value}] >", :yellow).presence || default_value
14
+ end
15
+ end
16
+ end
17
+ module ClassMethods
18
+ def next_migration_number(dirname)
19
+ if ActiveRecord::Base.timestamped_migrations
20
+ migration_number = Time.current.strftime("%Y%m%d%H%M%S").to_i
21
+ migration_number += 1
22
+ migration_number.to_s
23
+ else
24
+ format("%.3d", current_migration_number(dirname) + 1)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,11 @@
1
+ namespace :bobot do
2
+ desc 'Install bobot'
3
+ task :install do
4
+ system 'rails g bobot:install'
5
+ end
6
+
7
+ desc 'Uninstall bobot'
8
+ task :uninstall do
9
+ system 'rails g bobot:uninstall'
10
+ end
11
+ end