Skip to content

Commit 7e42e01

Browse files
dblockCopilot
andcommitted
Support custom APIs and separately mountable team routes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df1960b commit 7e42e01

6 files changed

Lines changed: 246 additions & 104 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### 2.3.0 (Next)
44

55
* [#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).
6+
* [#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).
67
* Your contribution here.
78

89
### 2.2.1 (2026/10/24)

‎README.md‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Build a complete Slack bot service with Slack button integration, in Ruby.
2020
- [OAuth Version and Scopes](#oauth-version-and-scopes)
2121
- [Slack App](#slack-app)
2222
- [API](#api)
23+
- [Custom Endpoints](#custom-endpoints)
24+
- [Team Endpoint Visibility](#team-endpoint-visibility)
2325
- [App](#app)
2426
- [Service Manager](#service-manager)
2527
- [Lifecycle Callbacks](#lifecycle-callbacks)
@@ -152,6 +154,58 @@ Navigate to either [localhost:9292](http://localhost:9292) or the ngrok URL abov
152154

153155
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.
154156

157+
#### Custom Endpoints
158+
159+
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.
160+
161+
To add an endpoint alongside the default API:
162+
163+
```ruby
164+
class ExtraApi < Grape::API
165+
prefix 'api'
166+
format :json
167+
168+
get :foobar do
169+
{ ok: true }
170+
end
171+
end
172+
173+
class ApplicationApi < Grape::API
174+
mount SlackRubyBotServer::Api::Endpoints::RootEndpoint
175+
mount ExtraApi
176+
end
177+
178+
SlackRubyBotServer.configure do |config|
179+
config.api = ApplicationApi
180+
end
181+
```
182+
183+
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.
184+
185+
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.
186+
187+
#### Team Endpoint Visibility
188+
189+
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.
190+
191+
```ruby
192+
class RegistrationApi < Grape::API
193+
include SlackRubyBotServer::Api::Helpers::ErrorHelpers
194+
195+
prefix 'api'
196+
format :json
197+
formatter :json, Grape::Formatter::Roar
198+
199+
mount SlackRubyBotServer::Api::Endpoints::TeamsEndpoint::Create
200+
end
201+
202+
SlackRubyBotServer.configure do |config|
203+
config.api = RegistrationApi
204+
end
205+
```
206+
207+
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.
208+
155209
#### App
156210

157211
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`.

‎lib/slack-ruby-bot-server/api/endpoints/teams_endpoint.rb‎

Lines changed: 114 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -2,118 +2,129 @@ module SlackRubyBotServer
22
module Api
33
module Endpoints
44
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
5+
class Read < Grape::API
6+
format :json
7+
helpers Helpers::CursorHelpers
8+
helpers Helpers::SortHelpers
9+
helpers Helpers::PaginationParameters
1910

20-
desc 'Get all the teams.'
21-
params do
22-
optional :active, type: ::Grape::API::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
11+
namespace :teams do
12+
desc 'Get a team.'
13+
params do
14+
requires :id, type: String, desc: 'Team ID.'
15+
end
16+
get ':id' do
17+
team = Team.find(params[:id]) || error!('Not Found', 404)
18+
present team, with: Presenters::TeamPresenter
19+
end
3220

33-
desc 'Create a team using an OAuth token.'
34-
params do
35-
requires :code, type: String
36-
optional :state, type: String
37-
end
38-
post do
39-
client = Slack::Web::Client.new
40-
41-
raise 'Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET.' unless ENV.key?('SLACK_CLIENT_ID') && ENV.key?('SLACK_CLIENT_SECRET')
42-
43-
options = {
44-
client_id: ENV.fetch('SLACK_CLIENT_ID', nil),
45-
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', nil),
46-
code: params[:code]
47-
}
48-
49-
rc = client.send(SlackRubyBotServer.config.oauth_access_method, options)
50-
51-
token = nil
52-
access_token = nil
53-
user_id = nil
54-
bot_user_id = nil
55-
team_id = nil
56-
team_name = nil
57-
oauth_scope = nil
58-
oauth_version = SlackRubyBotServer::Config.oauth_version
59-
60-
case oauth_version
61-
when :v2
62-
access_token = rc.access_token
63-
token = rc.access_token
64-
user_id = rc.authed_user&.id
65-
bot_user_id = rc.bot_user_id
66-
team_id = rc.team&.id
67-
team_name = rc.team&.name
68-
oauth_scope = rc.scope
69-
when :v1
70-
access_token = rc.access_token
71-
bot = rc.bot if rc.key?(:bot)
72-
token = bot ? bot.bot_access_token : access_token
73-
user_id = rc.user_id
74-
bot_user_id = bot ? bot.bot_user_id : nil
75-
team_id = rc.team_id
76-
team_name = rc.team_name
77-
oauth_scope = rc.scope
21+
desc 'Get all the teams.'
22+
params do
23+
optional :active, type: ::Grape::API::Boolean, desc: 'Return active teams only.'
24+
use :pagination
25+
end
26+
sort Team::SORT_ORDERS
27+
get do
28+
teams = Team.all
29+
teams = teams.active if params[:active]
30+
teams = paginate_and_sort_by_cursor(teams, default_sort_order: '-id')
31+
present teams, with: Presenters::TeamsPresenter
7832
end
33+
end
34+
end
35+
36+
class Create < Grape::API
37+
format :json
7938

80-
team = Team.where(token: token).first
81-
team ||= Team.where(team_id: team_id, oauth_version: oauth_version).first
82-
team ||= Team.where(team_id: team_id).first
83-
84-
if team
85-
team.ping_if_active!
86-
87-
team.update!(
88-
oauth_version: oauth_version,
89-
oauth_scope: oauth_scope,
90-
activated_user_id: user_id,
91-
activated_user_access_token: access_token,
92-
bot_user_id: bot_user_id
93-
)
94-
95-
raise "Team #{team.name} is already registered." if team.active?
96-
97-
team.activate!(token)
98-
else
99-
team = Team.create!(
100-
token: token,
101-
oauth_version: oauth_version,
102-
oauth_scope: oauth_scope,
103-
team_id: team_id,
104-
name: team_name,
105-
activated_user_id: user_id,
106-
activated_user_access_token: access_token,
107-
bot_user_id: bot_user_id
108-
)
39+
namespace :teams do
40+
desc 'Create a team using an OAuth token.'
41+
params do
42+
requires :code, type: String
43+
optional :state, type: String
10944
end
45+
post do
46+
client = Slack::Web::Client.new
47+
48+
raise 'Missing SLACK_CLIENT_ID or SLACK_CLIENT_SECRET.' unless ENV.key?('SLACK_CLIENT_ID') && ENV.key?('SLACK_CLIENT_SECRET')
11049

111-
options = params.slice(:state)
50+
options = {
51+
client_id: ENV.fetch('SLACK_CLIENT_ID', nil),
52+
client_secret: ENV.fetch('SLACK_CLIENT_SECRET', nil),
53+
code: params[:code]
54+
}
11255

113-
Service.instance.create!(team, options)
114-
present team, with: Presenters::TeamPresenter
56+
rc = client.send(SlackRubyBotServer.config.oauth_access_method, options)
57+
58+
token = nil
59+
access_token = nil
60+
user_id = nil
61+
bot_user_id = nil
62+
team_id = nil
63+
team_name = nil
64+
oauth_scope = nil
65+
oauth_version = SlackRubyBotServer::Config.oauth_version
66+
67+
case oauth_version
68+
when :v2
69+
access_token = rc.access_token
70+
token = rc.access_token
71+
user_id = rc.authed_user&.id
72+
bot_user_id = rc.bot_user_id
73+
team_id = rc.team&.id
74+
team_name = rc.team&.name
75+
oauth_scope = rc.scope
76+
when :v1
77+
access_token = rc.access_token
78+
bot = rc.bot if rc.key?(:bot)
79+
token = bot ? bot.bot_access_token : access_token
80+
user_id = rc.user_id
81+
bot_user_id = bot ? bot.bot_user_id : nil
82+
team_id = rc.team_id
83+
team_name = rc.team_name
84+
oauth_scope = rc.scope
85+
end
86+
87+
team = Team.where(token: token).first
88+
team ||= Team.where(team_id: team_id, oauth_version: oauth_version).first
89+
team ||= Team.where(team_id: team_id).first
90+
91+
if team
92+
team.ping_if_active!
93+
94+
team.update!(
95+
oauth_version: oauth_version,
96+
oauth_scope: oauth_scope,
97+
activated_user_id: user_id,
98+
activated_user_access_token: access_token,
99+
bot_user_id: bot_user_id
100+
)
101+
102+
raise "Team #{team.name} is already registered." if team.active?
103+
104+
team.activate!(token)
105+
else
106+
team = Team.create!(
107+
token: token,
108+
oauth_version: oauth_version,
109+
oauth_scope: oauth_scope,
110+
team_id: team_id,
111+
name: team_name,
112+
activated_user_id: user_id,
113+
activated_user_access_token: access_token,
114+
bot_user_id: bot_user_id
115+
)
116+
end
117+
118+
options = params.slice(:state)
119+
120+
Service.instance.create!(team, options)
121+
present team, with: Presenters::TeamPresenter
122+
end
115123
end
116124
end
125+
126+
mount Read
127+
mount Create
117128
end
118129
end
119130
end

‎lib/slack-ruby-bot-server/api/middleware.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def self.instance
3838
end
3939

4040
def call(env)
41-
Endpoints::RootEndpoint.call(env)
41+
(SlackRubyBotServer::Config.api || Endpoints::RootEndpoint).call(env)
4242
end
4343
end
4444
end

‎lib/slack-ruby-bot-server/config.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ module Config
88
attr_accessor :view_paths
99
attr_accessor :oauth_scope
1010
attr_accessor :oauth_version
11+
attr_accessor :api
1112

1213
def reset!
1314
self.logger = nil
1415
self.service_class = SlackRubyBotServer::Service
1516
self.oauth_scope = nil
1617
self.oauth_version = :v2
1718
@server_pages = nil
19+
self.api = nil
1820

1921
self.view_paths = [
2022
'views',

0 commit comments

Comments
 (0)