Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### 2.3.0 (Next)

* [#183](https://github.com/slack-ruby/slack-ruby-bot-server/pull/183): Add a `config.server_pages` block to configure page middleware options centrally without changing the installed library's defaults - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* [#184](https://github.com/slack-ruby/slack-ruby-bot-server/pull/184): Add a configurable API application and separately mountable team read/registration endpoints, preserving the default API while supporting custom routes and registration-only applications - [@dblock](https://github.com/dblock), [@Copilot](https://github.com/apps/copilot-swe-agent).
* Your contribution here.

### 2.2.1 (2026/10/24)
Expand Down
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Build a complete Slack bot service with Slack button integration, in Ruby.
- [OAuth Version and Scopes](#oauth-version-and-scopes)
- [Slack App](#slack-app)
- [API](#api)
- [Custom Endpoints](#custom-endpoints)
- [Team Endpoint Visibility](#team-endpoint-visibility)
- [App](#app)
- [Service Manager](#service-manager)
- [Lifecycle Callbacks](#lifecycle-callbacks)
Expand Down Expand Up @@ -152,6 +154,58 @@ Navigate to either [localhost:9292](http://localhost:9292) or the ngrok URL abov

This library implements an app, [SlackRubyBotServer::App](lib/slack-ruby-bot-server/app.rb) and a service manager, [SlackRubyBotServer::Service](lib/slack-ruby-bot-server/service.rb). It also provides [default HTML templates and JS scripts](public) for Slack integration.

#### Custom Endpoints

Set `config.api` to a Rack application, typically a `Grape::API` class, to customize the API without reopening the framework's classes. The default is `nil`, which uses `SlackRubyBotServer::Api::Endpoints::RootEndpoint`, including extensions mounted there by other gems. `SlackRubyBotServer::Config.reset!` restores this default.

To add an endpoint alongside the default API:

```ruby
class ExtraApi < Grape::API
prefix 'api'
format :json

get :foobar do
{ ok: true }
end
end

class ApplicationApi < Grape::API
mount SlackRubyBotServer::Api::Endpoints::RootEndpoint
mount ExtraApi
end

SlackRubyBotServer.configure do |config|
config.api = ApplicationApi
end
```

Configure this before starting the application. In `config.ru`, continue running `SlackRubyBotServer::Api::Middleware.instance`; both `/api/foobar` and the existing `/api` routes then share the framework's CORS, URL-rewrite, and page middleware. A separate `Rack::Cascade` stack is unnecessary.

In Rails, require the file defining `ApplicationApi` from your initializer before assigning `config.api`. For reloadable classes, make the assignment inside `Rails.application.config.to_prepare` so it references the current class. Mount `SlackRubyBotServer::Api::Middleware.instance` in your routes, for example `mount SlackRubyBotServer::Api::Middleware.instance => '/'`, rather than bypassing that middleware stack.

#### Team Endpoint Visibility

The default API exposes `GET /api/teams` and `GET /api/teams/:id` publicly. To retain OAuth registration without those read endpoints, use the separately mountable `TeamsEndpoint::Create`. `TeamsEndpoint::Read` contains the list and detail routes; `TeamsEndpoint` continues to mount both for backward compatibility.

```ruby
class RegistrationApi < Grape::API
include SlackRubyBotServer::Api::Helpers::ErrorHelpers

prefix 'api'
format :json
formatter :json, Grape::Formatter::Roar

mount SlackRubyBotServer::Api::Endpoints::TeamsEndpoint::Create
end

SlackRubyBotServer.configure do |config|
config.api = RegistrationApi
end
```

This replaces the default root API: it does not expose its status, root discovery, Swagger, or team-read routes. Do not also mount `RootEndpoint` or `TeamsEndpoint`, since either would restore the public team reads. If you use the Events extension or other custom endpoints, explicitly mount their routes with their required signature verification too. Add your own authentication before mounting `TeamsEndpoint::Read` if you need protected team queries. OAuth registration still requires a valid Slack authorization code; this configuration does not add a general authentication layer.

#### App

The app instance checks for a working database connection, ensures indexes, performs migrations, sets up bot aliases and log levels. You can introduce custom behavior into the app lifecycle by subclassing `SlackRubyBotServer::App` and creating an instance of the child class in `config.ru`.
Expand Down
217 changes: 114 additions & 103 deletions lib/slack-ruby-bot-server/api/endpoints/teams_endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,118 +2,129 @@ module SlackRubyBotServer
module Api
module Endpoints
class TeamsEndpoint < Grape::API
format :json
helpers Helpers::CursorHelpers
helpers Helpers::SortHelpers
helpers Helpers::PaginationParameters

namespace :teams do
desc 'Get a team.'
params do
requires :id, type: String, desc: 'Team ID.'
end
get ':id' do
team = Team.find(params[:id]) || error!('Not Found', 404)
present team, with: Presenters::TeamPresenter
end
class Read < Grape::API
format :json
helpers Helpers::CursorHelpers
helpers Helpers::SortHelpers
helpers Helpers::PaginationParameters

desc 'Get all the teams.'
params do
optional :active, type: ::Grape::API::Boolean, desc: 'Return active teams only.'
use :pagination
end
sort Team::SORT_ORDERS
get do
teams = Team.all
teams = teams.active if params[:active]
teams = paginate_and_sort_by_cursor(teams, default_sort_order: '-id')
present teams, with: Presenters::TeamsPresenter
end
namespace :teams do
desc 'Get a team.'
params do
requires :id, type: String, desc: 'Team ID.'
end
get ':id' do
team = Team.find(params[:id]) || error!('Not Found', 404)
present team, with: Presenters::TeamPresenter
end

desc 'Create a team using an OAuth token.'
params do
requires :code, type: String
optional :state, type: String
end
post do
client = Slack::Web::Client.new

raise 'Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET.' unless ENV.key?('SLACK_CLIENT_ID') && ENV.key?('SLACK_CLIENT_SECRET')

options = {
client_id: ENV.fetch('SLACK_CLIENT_ID', nil),
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', nil),
code: params[:code]
}

rc = client.send(SlackRubyBotServer.config.oauth_access_method, options)

token = nil
access_token = nil
user_id = nil
bot_user_id = nil
team_id = nil
team_name = nil
oauth_scope = nil
oauth_version = SlackRubyBotServer::Config.oauth_version

case oauth_version
when :v2
access_token = rc.access_token
token = rc.access_token
user_id = rc.authed_user&.id
bot_user_id = rc.bot_user_id
team_id = rc.team&.id
team_name = rc.team&.name
oauth_scope = rc.scope
when :v1
access_token = rc.access_token
bot = rc.bot if rc.key?(:bot)
token = bot ? bot.bot_access_token : access_token
user_id = rc.user_id
bot_user_id = bot ? bot.bot_user_id : nil
team_id = rc.team_id
team_name = rc.team_name
oauth_scope = rc.scope
desc 'Get all the teams.'
params do
optional :active, type: ::Grape::API::Boolean, desc: 'Return active teams only.'
use :pagination
end
sort Team::SORT_ORDERS
get do
teams = Team.all
teams = teams.active if params[:active]
teams = paginate_and_sort_by_cursor(teams, default_sort_order: '-id')
present teams, with: Presenters::TeamsPresenter
end
end
end

class Create < Grape::API
format :json

team = Team.where(token: token).first
team ||= Team.where(team_id: team_id, oauth_version: oauth_version).first
team ||= Team.where(team_id: team_id).first

if team
team.ping_if_active!

team.update!(
oauth_version: oauth_version,
oauth_scope: oauth_scope,
activated_user_id: user_id,
activated_user_access_token: access_token,
bot_user_id: bot_user_id
)

raise "Team #{team.name} is already registered." if team.active?

team.activate!(token)
else
team = Team.create!(
token: token,
oauth_version: oauth_version,
oauth_scope: oauth_scope,
team_id: team_id,
name: team_name,
activated_user_id: user_id,
activated_user_access_token: access_token,
bot_user_id: bot_user_id
)
namespace :teams do
desc 'Create a team using an OAuth token.'
params do
requires :code, type: String
optional :state, type: String
end
post do
client = Slack::Web::Client.new

raise 'Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET.' unless ENV.key?('SLACK_CLIENT_ID') && ENV.key?('SLACK_CLIENT_SECRET')

options = params.slice(:state)
options = {
client_id: ENV.fetch('SLACK_CLIENT_ID', nil),
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', nil),
code: params[:code]
}

Service.instance.create!(team, options)
present team, with: Presenters::TeamPresenter
rc = client.send(SlackRubyBotServer.config.oauth_access_method, options)

token = nil
access_token = nil
user_id = nil
bot_user_id = nil
team_id = nil
team_name = nil
oauth_scope = nil
oauth_version = SlackRubyBotServer::Config.oauth_version

case oauth_version
when :v2
access_token = rc.access_token
token = rc.access_token
user_id = rc.authed_user&.id
bot_user_id = rc.bot_user_id
team_id = rc.team&.id
team_name = rc.team&.name
oauth_scope = rc.scope
when :v1
access_token = rc.access_token
bot = rc.bot if rc.key?(:bot)
token = bot ? bot.bot_access_token : access_token
user_id = rc.user_id
bot_user_id = bot ? bot.bot_user_id : nil
team_id = rc.team_id
team_name = rc.team_name
oauth_scope = rc.scope
end

team = Team.where(token: token).first
team ||= Team.where(team_id: team_id, oauth_version: oauth_version).first
team ||= Team.where(team_id: team_id).first

if team
team.ping_if_active!

team.update!(
oauth_version: oauth_version,
oauth_scope: oauth_scope,
activated_user_id: user_id,
activated_user_access_token: access_token,
bot_user_id: bot_user_id
)

raise "Team #{team.name} is already registered." if team.active?

team.activate!(token)
else
team = Team.create!(
token: token,
oauth_version: oauth_version,
oauth_scope: oauth_scope,
team_id: team_id,
name: team_name,
activated_user_id: user_id,
activated_user_access_token: access_token,
bot_user_id: bot_user_id
)
end

options = params.slice(:state)

Service.instance.create!(team, options)
present team, with: Presenters::TeamPresenter
end
end
end

mount Read
mount Create
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/slack-ruby-bot-server/api/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def self.instance
end

def call(env)
Endpoints::RootEndpoint.call(env)
(SlackRubyBotServer::Config.api || Endpoints::RootEndpoint).call(env)
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions lib/slack-ruby-bot-server/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ module Config
attr_accessor :view_paths
attr_accessor :oauth_scope
attr_accessor :oauth_version
attr_accessor :api

def reset!
self.logger = nil
self.service_class = SlackRubyBotServer::Service
self.oauth_scope = nil
self.oauth_version = :v2
@server_pages = nil
self.api = nil

self.view_paths = [
'views',
Expand Down
Loading
Loading