slack-ruby-bot-server 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +45 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +16 -0
- data/CONTRIBUTING.md +125 -0
- data/DEBUGGING.md +25 -0
- data/Gemfile +3 -0
- data/Guardfile +8 -0
- data/LICENSE +21 -0
- data/README.md +49 -0
- data/RELEASING.md +69 -0
- data/Rakefile +16 -0
- data/app.json +9 -0
- data/config/initializers/bson/object_id.rb +12 -0
- data/config/initializers/grape/sort_extension.rb +8 -0
- data/config/initializers/slack-ruby-bot/client.rb +17 -0
- data/config/initializers/slack-ruby-bot/commands/base.rb +21 -0
- data/images/new.png +0 -0
- data/images/slackbotserver.gif +0 -0
- data/images/slackbutton.gif +0 -0
- data/lib/slack-ruby-bot-server.rb +17 -0
- data/lib/slack-ruby-bot-server/api.rb +8 -0
- data/lib/slack-ruby-bot-server/api/endpoints.rb +3 -0
- data/lib/slack-ruby-bot-server/api/endpoints/root_endpoint.rb +22 -0
- data/lib/slack-ruby-bot-server/api/endpoints/status_endpoint.rb +16 -0
- data/lib/slack-ruby-bot-server/api/endpoints/teams_endpoint.rb +70 -0
- data/lib/slack-ruby-bot-server/api/helpers.rb +4 -0
- data/lib/slack-ruby-bot-server/api/helpers/cursor_helpers.rb +37 -0
- data/lib/slack-ruby-bot-server/api/helpers/error_helpers.rb +52 -0
- data/lib/slack-ruby-bot-server/api/helpers/pagination_parameters.rb +19 -0
- data/lib/slack-ruby-bot-server/api/helpers/sort_helpers.rb +53 -0
- data/lib/slack-ruby-bot-server/api/middleware.rb +44 -0
- data/lib/slack-ruby-bot-server/api/presenters.rb +9 -0
- data/lib/slack-ruby-bot-server/api/presenters/paginated_presenter.rb +38 -0
- data/lib/slack-ruby-bot-server/api/presenters/root_presenter.rb +44 -0
- data/lib/slack-ruby-bot-server/api/presenters/status_presenter.rb +40 -0
- data/lib/slack-ruby-bot-server/api/presenters/team_presenter.rb +24 -0
- data/lib/slack-ruby-bot-server/api/presenters/teams_presenter.rb +14 -0
- data/lib/slack-ruby-bot-server/app.rb +85 -0
- data/lib/slack-ruby-bot-server/info.rb +11 -0
- data/lib/slack-ruby-bot-server/models.rb +1 -0
- data/lib/slack-ruby-bot-server/models/team.rb +66 -0
- data/lib/slack-ruby-bot-server/rspec.rb +5 -0
- data/lib/slack-ruby-bot-server/rspec/fabricators/team.rb +5 -0
- data/lib/slack-ruby-bot-server/server.rb +21 -0
- data/lib/slack-ruby-bot-server/service.rb +81 -0
- data/lib/slack-ruby-bot-server/version.rb +3 -0
- data/public/favicon.ico +0 -0
- data/public/img/slack.png +0 -0
- data/public/index.html.erb +28 -0
- data/public/robots.txt +2 -0
- data/public/scripts/jquery-1.7.1.min.js +4 -0
- data/public/scripts/register.js +50 -0
- data/public/scripts/stats.js +14 -0
- data/public/scripts/url.min.js +1 -0
- data/sample_app/.rspec +3 -0
- data/sample_app/Gemfile +14 -0
- data/sample_app/Procfile +1 -0
- data/sample_app/Rakefile +10 -0
- data/sample_app/commands.rb +2 -0
- data/sample_app/commands/help.rb +19 -0
- data/sample_app/commands/whoami.rb +6 -0
- data/sample_app/config.ru +20 -0
- data/sample_app/config/mongoid.yml +27 -0
- data/sample_app/config/newrelic.yml +217 -0
- data/sample_app/spec/commands/help_spec.rb +14 -0
- data/sample_app/spec/commands/whoami_spec.rb +14 -0
- data/sample_app/spec/spec_helper.rb +25 -0
- data/script/console +9 -0
- data/slack-ruby-bot-server.gemspec +48 -0
- data/tasks/db.rake +44 -0
- metadata +523 -0
@@ -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
|
data/images/new.png
ADDED
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,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,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
|