Skip to content

Commit a7264ff

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

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
@@ -2,6 +2,7 @@
22

33
### 2.2.2 (Next)
44

5+
* 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).
56
* Your contribution here.
67

78
### 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)
@@ -151,6 +153,58 @@ Navigate to either [localhost:9292](http://localhost:9292) or the ngrok URL abov
151153

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

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

156210
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
@@ -37,7 +37,7 @@ def self.instance
3737
end
3838

3939
def call(env)
40-
Endpoints::RootEndpoint.call(env)
40+
(SlackRubyBotServer::Config.api || Endpoints::RootEndpoint).call(env)
4141
end
4242
end
4343
end

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ 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
18+
self.api = nil
1719

1820
self.view_paths = [
1921
'views',

0 commit comments

Comments
 (0)