slack-ruby-bot-server 0.1.0

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 (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