slack-ruby-bot-server 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +2 -0
  5. data/.rubocop_todo.yml +45 -0
  6. data/.travis.yml +9 -0
  7. data/CHANGELOG.md +16 -0
  8. data/CONTRIBUTING.md +125 -0
  9. data/DEBUGGING.md +25 -0
  10. data/Gemfile +3 -0
  11. data/Guardfile +8 -0
  12. data/LICENSE +21 -0
  13. data/README.md +49 -0
  14. data/RELEASING.md +69 -0
  15. data/Rakefile +16 -0
  16. data/app.json +9 -0
  17. data/config/initializers/bson/object_id.rb +12 -0
  18. data/config/initializers/grape/sort_extension.rb +8 -0
  19. data/config/initializers/slack-ruby-bot/client.rb +17 -0
  20. data/config/initializers/slack-ruby-bot/commands/base.rb +21 -0
  21. data/images/new.png +0 -0
  22. data/images/slackbotserver.gif +0 -0
  23. data/images/slackbutton.gif +0 -0
  24. data/lib/slack-ruby-bot-server.rb +17 -0
  25. data/lib/slack-ruby-bot-server/api.rb +8 -0
  26. data/lib/slack-ruby-bot-server/api/endpoints.rb +3 -0
  27. data/lib/slack-ruby-bot-server/api/endpoints/root_endpoint.rb +22 -0
  28. data/lib/slack-ruby-bot-server/api/endpoints/status_endpoint.rb +16 -0
  29. data/lib/slack-ruby-bot-server/api/endpoints/teams_endpoint.rb +70 -0
  30. data/lib/slack-ruby-bot-server/api/helpers.rb +4 -0
  31. data/lib/slack-ruby-bot-server/api/helpers/cursor_helpers.rb +37 -0
  32. data/lib/slack-ruby-bot-server/api/helpers/error_helpers.rb +52 -0
  33. data/lib/slack-ruby-bot-server/api/helpers/pagination_parameters.rb +19 -0
  34. data/lib/slack-ruby-bot-server/api/helpers/sort_helpers.rb +53 -0
  35. data/lib/slack-ruby-bot-server/api/middleware.rb +44 -0
  36. data/lib/slack-ruby-bot-server/api/presenters.rb +9 -0
  37. data/lib/slack-ruby-bot-server/api/presenters/paginated_presenter.rb +38 -0
  38. data/lib/slack-ruby-bot-server/api/presenters/root_presenter.rb +44 -0
  39. data/lib/slack-ruby-bot-server/api/presenters/status_presenter.rb +40 -0
  40. data/lib/slack-ruby-bot-server/api/presenters/team_presenter.rb +24 -0
  41. data/lib/slack-ruby-bot-server/api/presenters/teams_presenter.rb +14 -0
  42. data/lib/slack-ruby-bot-server/app.rb +85 -0
  43. data/lib/slack-ruby-bot-server/info.rb +11 -0
  44. data/lib/slack-ruby-bot-server/models.rb +1 -0
  45. data/lib/slack-ruby-bot-server/models/team.rb +66 -0
  46. data/lib/slack-ruby-bot-server/rspec.rb +5 -0
  47. data/lib/slack-ruby-bot-server/rspec/fabricators/team.rb +5 -0
  48. data/lib/slack-ruby-bot-server/server.rb +21 -0
  49. data/lib/slack-ruby-bot-server/service.rb +81 -0
  50. data/lib/slack-ruby-bot-server/version.rb +3 -0
  51. data/public/favicon.ico +0 -0
  52. data/public/img/slack.png +0 -0
  53. data/public/index.html.erb +28 -0
  54. data/public/robots.txt +2 -0
  55. data/public/scripts/jquery-1.7.1.min.js +4 -0
  56. data/public/scripts/register.js +50 -0
  57. data/public/scripts/stats.js +14 -0
  58. data/public/scripts/url.min.js +1 -0
  59. data/sample_app/.rspec +3 -0
  60. data/sample_app/Gemfile +14 -0
  61. data/sample_app/Procfile +1 -0
  62. data/sample_app/Rakefile +10 -0
  63. data/sample_app/commands.rb +2 -0
  64. data/sample_app/commands/help.rb +19 -0
  65. data/sample_app/commands/whoami.rb +6 -0
  66. data/sample_app/config.ru +20 -0
  67. data/sample_app/config/mongoid.yml +27 -0
  68. data/sample_app/config/newrelic.yml +217 -0
  69. data/sample_app/spec/commands/help_spec.rb +14 -0
  70. data/sample_app/spec/commands/whoami_spec.rb +14 -0
  71. data/sample_app/spec/spec_helper.rb +25 -0
  72. data/script/console +9 -0
  73. data/slack-ruby-bot-server.gemspec +48 -0
  74. data/tasks/db.rake +44 -0
  75. metadata +523 -0
@@ -0,0 +1,8 @@
1
+ module Grape
2
+ class API
3
+ def self.sort(value)
4
+ route_setting :sort, sort: value
5
+ value
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,17 @@
1
+ module SlackRubyBot
2
+ class Client
3
+ # keep track of the team that the client is connected to
4
+ attr_accessor :owner
5
+ end
6
+ end
7
+
8
+ Slack.configure do |config|
9
+ config.logger = Logger.new(STDOUT)
10
+ config.logger.level = Logger::WARN
11
+ end
12
+
13
+ SlackRubyBot::Client.logger.level = Logger::WARN
14
+
15
+ Slack::RealTime::Client.configure do |config|
16
+ config.store_class = Slack::RealTime::Stores::Starter
17
+ end
@@ -0,0 +1,21 @@
1
+ module SlackRubyBot
2
+ module Commands
3
+ class Base
4
+ class << self
5
+ alias_method :_invoke, :invoke
6
+
7
+ def invoke(client, data)
8
+ _invoke client, data
9
+ rescue Mongoid::Errors::Validations => e
10
+ logger.info "#{name.demodulize.upcase}: #{client.owner}, error - #{e.document.class}, #{e.document.errors.to_hash}"
11
+ client.say(channel: data.channel, text: e.document.errors.first[1], gif: 'error')
12
+ true
13
+ rescue StandardError => e
14
+ logger.info "#{name.demodulize.upcase}: #{client.owner}, #{e.class}: #{e}"
15
+ client.say(channel: data.channel, text: e.message, gif: 'error')
16
+ true
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,17 @@
1
+ require 'celluloid/current'
2
+ require 'kaminari/grape'
3
+ require 'mongoid-scroll'
4
+ require 'grape-swagger'
5
+ require 'slack-ruby-bot'
6
+
7
+ Dir[File.expand_path('../config/initializers', __dir__) + '/**/*.rb'].each do |file|
8
+ require file
9
+ end
10
+
11
+ require 'slack-ruby-bot-server/version'
12
+ require 'slack-ruby-bot-server/info'
13
+ require 'slack-ruby-bot-server/models'
14
+ require 'slack-ruby-bot-server/api'
15
+ require 'slack-ruby-bot-server/app'
16
+ require 'slack-ruby-bot-server/server'
17
+ require 'slack-ruby-bot-server/service'
@@ -0,0 +1,8 @@
1
+ require 'grape'
2
+ require 'roar'
3
+ require 'grape-roar'
4
+
5
+ require 'slack-ruby-bot-server/api/helpers'
6
+ require 'slack-ruby-bot-server/api/presenters'
7
+ require 'slack-ruby-bot-server/api/endpoints'
8
+ require 'slack-ruby-bot-server/api/middleware'
@@ -0,0 +1,3 @@
1
+ require 'slack-ruby-bot-server/api/endpoints/teams_endpoint'
2
+ require 'slack-ruby-bot-server/api/endpoints/status_endpoint'
3
+ require 'slack-ruby-bot-server/api/endpoints/root_endpoint'
@@ -0,0 +1,22 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Endpoints
4
+ class RootEndpoint < Grape::API
5
+ include Helpers::ErrorHelpers
6
+
7
+ prefix 'api'
8
+
9
+ format :json
10
+ formatter :json, Grape::Formatter::Roar
11
+ get do
12
+ present self, with: Presenters::RootPresenter
13
+ end
14
+
15
+ mount StatusEndpoint
16
+ mount TeamsEndpoint
17
+
18
+ add_swagger_documentation
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,16 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Endpoints
4
+ class StatusEndpoint < Grape::API
5
+ format :json
6
+
7
+ namespace :status do
8
+ desc 'Get system status.'
9
+ get do
10
+ present self, with: Presenters::StatusPresenter
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Endpoints
4
+ class TeamsEndpoint < Grape::API
5
+ format :json
6
+ helpers Helpers::CursorHelpers
7
+ helpers Helpers::SortHelpers
8
+ helpers Helpers::PaginationParameters
9
+
10
+ namespace :teams do
11
+ desc 'Get a team.'
12
+ params do
13
+ requires :id, type: String, desc: 'Team ID.'
14
+ end
15
+ get ':id' do
16
+ team = Team.find(params[:id]) || error!('Not Found', 404)
17
+ present team, with: Presenters::TeamPresenter
18
+ end
19
+
20
+ desc 'Get all the teams.'
21
+ params do
22
+ optional :active, type: Boolean, desc: 'Return active teams only.'
23
+ use :pagination
24
+ end
25
+ sort Team::SORT_ORDERS
26
+ get do
27
+ teams = Team.all
28
+ teams = teams.active if params[:active]
29
+ teams = paginate_and_sort_by_cursor(teams, default_sort_order: '-_id')
30
+ present teams, with: Presenters::TeamsPresenter
31
+ end
32
+
33
+ desc 'Create a team using an OAuth token.'
34
+ params do
35
+ requires :code, type: String
36
+ end
37
+ post do
38
+ client = Slack::Web::Client.new
39
+
40
+ fail 'Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET.' unless ENV.key?('SLACK_CLIENT_ID') && ENV.key?('SLACK_CLIENT_SECRET')
41
+
42
+ rc = client.oauth_access(
43
+ client_id: ENV['SLACK_CLIENT_ID'],
44
+ client_secret: ENV['SLACK_CLIENT_SECRET'],
45
+ code: params[:code]
46
+ )
47
+
48
+ token = rc['bot']['bot_access_token']
49
+ team = Team.where(token: token).first
50
+ team ||= Team.where(team_id: rc['team_id']).first
51
+ if team && !team.active?
52
+ team.activate!(token)
53
+ elsif team
54
+ fail "Team #{team.name} is already registered."
55
+ else
56
+ team = Team.create!(
57
+ token: token,
58
+ team_id: rc['team_id'],
59
+ name: rc['team_name']
60
+ )
61
+ end
62
+
63
+ Service.instance.start!(team)
64
+ present team, with: Presenters::TeamPresenter
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,4 @@
1
+ require 'slack-ruby-bot-server/api/helpers/cursor_helpers'
2
+ require 'slack-ruby-bot-server/api/helpers/pagination_parameters'
3
+ require 'slack-ruby-bot-server/api/helpers/sort_helpers'
4
+ require 'slack-ruby-bot-server/api/helpers/error_helpers'
@@ -0,0 +1,37 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Helpers
4
+ module CursorHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ # apply cursor-based pagination to a collection
8
+ # returns a hash:
9
+ # results: (paginated collection subset)
10
+ # next: (cursor to the next page)
11
+ def paginate_by_cursor(coll, &_block)
12
+ fail 'Both cursor and offset parameters are present, these are mutually exclusive.' if params.key?(:offset) && params.key?(:cursor)
13
+ results = { results: [], next: nil }
14
+ size = (params[:size] || 10).to_i
15
+ if params.key?(:offset)
16
+ skip = params[:offset].to_i
17
+ coll = coll.skip(skip)
18
+ end
19
+ # some items may be skipped with a block
20
+ query = block_given? ? coll : coll.limit(size)
21
+ query.scroll(params[:cursor]) do |record, next_cursor|
22
+ record = yield(record) if block_given?
23
+ results[:results] << record if record
24
+ results[:next] = next_cursor.to_s
25
+ break if results[:results].count >= size
26
+ end
27
+ results[:total_count] = coll.count if params[:total_count] && coll.respond_to?(:count)
28
+ results
29
+ end
30
+
31
+ def paginate_and_sort_by_cursor(coll, options = {}, &block)
32
+ Hashie::Mash.new(paginate_by_cursor(sort(coll, options), &block))
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Helpers
4
+ module ErrorHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ rescue_from :all, backtrace: true do |e|
9
+ backtrace = e.backtrace[0..5].join("\n ")
10
+ Middleware.logger.error "#{e.class.name}: #{e.message}\n #{backtrace}"
11
+ error = { type: 'other_error', message: e.message }
12
+ error[:backtrace] = backtrace
13
+ rack_response(error.to_json, 400)
14
+ end
15
+ # rescue document validation errors into detail json
16
+ rescue_from Mongoid::Errors::Validations do |e|
17
+ backtrace = e.backtrace[0..5].join("\n ")
18
+ Middleware.logger.warn "#{e.class.name}: #{e.message}\n #{backtrace}"
19
+ rack_response({
20
+ type: 'param_error',
21
+ message: e.document.errors.full_messages.uniq.join(', ') + '.',
22
+ detail: e.document.errors.messages.each_with_object({}) do |(k, v), h|
23
+ h[k] = v.uniq
24
+ end
25
+ }.to_json, 400)
26
+ end
27
+ rescue_from Grape::Exceptions::Validation do |e|
28
+ backtrace = e.backtrace[0..5].join("\n ")
29
+ Middleware.logger.warn "#{e.class.name}: #{e.message}\n #{backtrace}"
30
+ rack_response({
31
+ type: 'param_error',
32
+ message: 'Invalid parameters.',
33
+ detail: { e.params.join(', ') => [e.message] }
34
+ }.to_json, 400)
35
+ end
36
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
37
+ backtrace = e.backtrace[0..5].join("\n ")
38
+ Middleware.logger.warn "#{e.class.name}: #{e.message}\n #{backtrace}"
39
+ rack_response({
40
+ type: 'param_error',
41
+ message: 'Invalid parameters.',
42
+ detail: e.errors.each_with_object({}) do |(k, v), h|
43
+ # JSON does not permit having a key of type Array
44
+ h[k.count == 1 ? k.first : k.join(', ')] = v
45
+ end
46
+ }.to_json, 400)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Helpers
4
+ module PaginationParameters
5
+ extend Grape::API::Helpers
6
+
7
+ params :pagination do
8
+ optional :offset, type: Integer, desc: 'Offset from which to retrieve.'
9
+ optional :size, type: Integer, desc: 'Number of items to retrieve for this page or from the current offset.'
10
+ optional :cursor, type: String, desc: 'Cursor for pagination.'
11
+ optional :total_count, desc: 'Include total count in the response.'
12
+ mutually_exclusive :offset, :cursor
13
+ end
14
+
15
+ ALL = %w(cursor size sort offset total_count)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Helpers
4
+ module SortHelpers
5
+ extend ActiveSupport::Concern
6
+
7
+ def sort_order(options = {})
8
+ params[:sort] = options[:default_sort_order] unless params[:sort]
9
+ return [] unless params[:sort]
10
+ sort_order = params[:sort].to_s
11
+ unless options[:default_sort_order] == sort_order
12
+ supported_sort_orders = route_sort
13
+ error!("This API doesn't support sorting", 400) if supported_sort_orders.blank?
14
+ unless supported_sort_orders.include?(sort_order)
15
+ error!("Invalid sort order: #{sort_order}, must be#{supported_sort_orders.count == 1 ? '' : ' one of'} '#{supported_sort_orders.join("', '")}'", 400)
16
+ end
17
+ end
18
+ sort_order = sort_order.split(',').map do |sort_entry|
19
+ sort_order = {}
20
+ if sort_entry[0] == '-'
21
+ sort_order[:direction] = :desc
22
+ sort_order[:column] = sort_entry[1..-1]
23
+ else
24
+ sort_order[:direction] = :asc
25
+ sort_order[:column] = sort_entry
26
+ end
27
+ error!("Invalid sort: #{sort_entry}", 400) if sort_order[:column].blank?
28
+ sort_order
29
+ end
30
+ sort_order
31
+ end
32
+
33
+ def route_sort
34
+ (env['api.endpoint'].route_setting(:sort) || {})[:sort]
35
+ end
36
+
37
+ def sort(coll, options = {})
38
+ sort_order = sort_order(options)
39
+ unless sort_order.empty?
40
+ if coll.respond_to?(:asc) && coll.respond_to?(:desc)
41
+ sort_order.each do |s|
42
+ coll = coll.send(s[:direction], s[:column])
43
+ end
44
+ else
45
+ error!("Cannot sort #{coll.class.name}", 500)
46
+ end
47
+ end
48
+ coll = coll.is_a?(Module) && coll.respond_to?(:all) ? coll.all : coll
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,44 @@
1
+ %w(rack/cors rack-rewrite rack-server-pages).each { |l| require l }
2
+
3
+ module SlackRubyBotServer
4
+ module Api
5
+ class Middleware
6
+ def self.logger
7
+ @logger ||= begin
8
+ $stdout.sync = true
9
+ Logger.new(STDOUT)
10
+ end
11
+ end
12
+
13
+ def self.instance
14
+ @instance ||= Rack::Builder.new do
15
+ use Rack::Cors do
16
+ allow do
17
+ origins '*'
18
+ resource '*', headers: :any, methods: [:get, :post]
19
+ end
20
+ end
21
+
22
+ # rewrite HAL links to make them clickable in a browser
23
+ use Rack::Rewrite do
24
+ r302 %r{(\/[\w\/]*\/)(%7B|\{)?(.*)(%7D|\})}, '$1'
25
+ end
26
+
27
+ use Rack::ServerPages do |config|
28
+ config.view_path = [
29
+ 'views', # relative to Dir.pwd
30
+ 'public', # relative to Dir.pwd
31
+ File.expand_path(File.join(__dir__, '../../../public')) # built-in fallback
32
+ ]
33
+ end
34
+
35
+ run Middleware.new
36
+ end.to_app
37
+ end
38
+
39
+ def call(env)
40
+ Endpoints::RootEndpoint.call(env)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,9 @@
1
+ require 'roar/representer'
2
+ require 'roar/json'
3
+ require 'roar/json/hal'
4
+
5
+ require 'slack-ruby-bot-server/api/presenters/paginated_presenter'
6
+ require 'slack-ruby-bot-server/api/presenters/status_presenter'
7
+ require 'slack-ruby-bot-server/api/presenters/team_presenter'
8
+ require 'slack-ruby-bot-server/api/presenters/teams_presenter'
9
+ require 'slack-ruby-bot-server/api/presenters/root_presenter'
@@ -0,0 +1,38 @@
1
+ module SlackRubyBotServer
2
+ module Api
3
+ module Presenters
4
+ module PaginatedPresenter
5
+ include Roar::JSON::HAL
6
+ include Roar::Hypermedia
7
+ include Grape::Roar::Representer
8
+
9
+ property :total_count
10
+
11
+ link :self do |opts|
12
+ "#{request_url(opts)}#{query_string_for_cursor(nil, opts)}"
13
+ end
14
+
15
+ link :next do |opts|
16
+ "#{request_url(opts)}#{query_string_for_cursor(represented.next, opts)}" if represented.next
17
+ end
18
+
19
+ private
20
+
21
+ def request_url(opts)
22
+ request = Grape::Request.new(opts[:env])
23
+ "#{request.base_url}#{opts[:env]['PATH_INFO']}"
24
+ end
25
+
26
+ # replace the page and offset parameters in the query string
27
+ def query_string_for_cursor(cursor, opts)
28
+ qs = Hashie::Mash.new(Rack::Utils.parse_nested_query(opts[:env]['QUERY_STRING']))
29
+ if cursor
30
+ qs.merge!(cursor: cursor)
31
+ qs.delete(:offset)
32
+ end
33
+ "?#{qs.to_query}" unless qs.empty?
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end