├── .rspec ├── Dangerfile ├── lib ├── slack-ruby-bot-server │ ├── api.rb │ └── api │ │ └── endpoints.rb ├── slack-ruby-bot-server-events │ ├── api.rb │ ├── api │ │ ├── endpoints.rb │ │ └── endpoints │ │ │ ├── slack.rb │ │ │ └── slack │ │ │ ├── events_endpoint.rb │ │ │ ├── commands_endpoint.rb │ │ │ └── actions_endpoint.rb │ ├── version.rb │ ├── requests.rb │ ├── requests │ │ ├── action.rb │ │ ├── event.rb │ │ ├── command.rb │ │ └── request.rb │ └── config.rb └── slack-ruby-bot-server-events.rb ├── .gitignore ├── spec ├── support │ ├── config.rb │ ├── rspec.rb │ └── api │ │ └── endpoints │ │ └── endpoint_test.rb ├── database_adapters │ ├── mongoid │ │ ├── mongoid.rb │ │ ├── rspec.rb │ │ └── mongoid.yml │ └── activerecord │ │ ├── postgresql.yml │ │ ├── schema.rb │ │ └── activerecord.rb ├── slack-ruby-bot-server-events │ ├── version_spec.rb │ ├── config_spec.rb │ └── api │ │ └── endpoints │ │ └── slack │ │ ├── commands_endpoint_spec.rb │ │ ├── events_endpoint_spec.rb │ │ └── actions_endpoint_spec.rb └── spec_helper.rb ├── Gemfile.danger ├── .rubocop.yml ├── .github └── workflows │ ├── rubocop.yml │ ├── danger.yml │ ├── test-mongodb.yml │ └── test-postgresql.yml ├── Rakefile ├── Gemfile ├── slack-ruby-bot-server-events.gemspec ├── LICENSE ├── CHANGELOG.md ├── RELEASING.md ├── .rubocop_todo.yml ├── CONTRIBUTING.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format=documentation 3 | 4 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | danger.import_dangerfile(gem: 'slack-ruby-danger') 4 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'api/endpoints' 4 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'api/endpoints' 4 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api/endpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'endpoints/slack' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .rvmrc 3 | .irbrc 4 | .bundle 5 | log 6 | .env 7 | *.swp 8 | Gemfile.lock 9 | .ruby-version 10 | pkg 11 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | VERSION = '0.4.1' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.before do 5 | SlackRubyBotServer::Events::Config.reset! 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Gemfile.danger: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :test do 4 | gem 'danger-toc', '~> 0.2.0', require: false 5 | gem 'slack-ruby-danger', '~> 0.2.0', require: false 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.mock_with :rspec 5 | config.expect_with :rspec 6 | config.raise_errors_for_deprecations! 7 | end 8 | -------------------------------------------------------------------------------- /spec/database_adapters/mongoid/mongoid.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Mongo::Logger.logger.level = Logger::INFO 4 | Mongoid.load!(File.expand_path('mongoid.yml', __dir__), ENV.fetch('RACK_ENV', nil)) 5 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api/endpoints/slack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'slack/actions_endpoint' 4 | require_relative 'slack/commands_endpoint' 5 | require_relative 'slack/events_endpoint' 6 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/requests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'requests/request' 4 | require_relative 'requests/event' 5 | require_relative 'requests/command' 6 | require_relative 'requests/action' 7 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/requests/action.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Requests 6 | class Action < Request 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/requests/event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Requests 6 | class Event < Request 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/requests/command.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Requests 6 | class Command < Request 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/database_adapters/mongoid/rspec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | config.before :suite do 5 | Mongoid::Tasks::Database.create_indexes 6 | end 7 | 8 | config.after :suite do 9 | Mongoid.purge! 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/slack-ruby-bot-server-events/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SlackRubyBotServer::Events do 6 | it 'has a version' do 7 | expect(SlackRubyBotServer::Events::VERSION).not_to be_nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | NewCops: enable 3 | 4 | Metrics: 5 | Enabled: false 6 | 7 | Layout/LineLength: 8 | Max: 500 9 | Enabled: false 10 | 11 | Style/Documentation: 12 | Enabled: false 13 | 14 | Style/ModuleFunction: 15 | EnforcedStyle: extend_self 16 | 17 | plugins: 18 | - rubocop-rake 19 | - rubocop-rspec 20 | 21 | inherit_from: .rubocop_todo.yml 22 | 23 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'slack-ruby-bot-server' 4 | 5 | require_relative 'slack-ruby-bot-server-events/version' 6 | require_relative 'slack-ruby-bot-server-events/config' 7 | require_relative 'slack-ruby-bot-server-events/requests' 8 | require_relative 'slack-ruby-bot-server-events/api' 9 | 10 | require_relative 'slack-ruby-bot-server/api' 11 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: lint 3 | on: [push, pull_request] 4 | jobs: 5 | lint: 6 | name: RuboCop 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Set up Ruby 11 | uses: ruby/setup-ruby@v1 12 | with: 13 | ruby-version: 3.4 14 | bundler-cache: true 15 | - name: Run RuboCop 16 | run: bundle exec rubocop 17 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler/gem_tasks' 5 | 6 | require 'rspec/core' 7 | require 'rspec/core/rake_task' 8 | 9 | RSpec::Core::RakeTask.new(:spec) do |spec| 10 | spec.pattern = FileList['spec/**/*_spec.rb'].exclude(%r{ext/(?!#{ENV.fetch('DATABASE_ADAPTER', nil)})}) 11 | end 12 | 13 | require 'rubocop/rake_task' 14 | RuboCop::RakeTask.new 15 | 16 | task default: %i[rubocop spec] 17 | -------------------------------------------------------------------------------- /spec/database_adapters/mongoid/mongoid.yml: -------------------------------------------------------------------------------- 1 | development: 2 | clients: 3 | default: 4 | database: slack-ruby-bot-server-events_development 5 | hosts: 6 | - 127.0.0.1:27017 7 | options: 8 | raise_not_found_error: false 9 | use_utc: true 10 | 11 | test: 12 | clients: 13 | default: 14 | uri: <%= ENV["DATABASE_URL"] || 'mongodb://localhost' %> 15 | options: 16 | raise_not_found_error: false 17 | use_utc: true 18 | -------------------------------------------------------------------------------- /spec/database_adapters/activerecord/postgresql.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | pool: 10 4 | timeout: 5000 5 | encoding: unicode 6 | 7 | development: 8 | <<: *default 9 | database: slack_ruby_bot_server_events_development 10 | 11 | test: 12 | <<: *default 13 | database: slack_ruby_bot_server_events_test 14 | url: <%= ENV["DATABASE_URL"] %> 15 | 16 | production: 17 | <<: *default 18 | database: slack_ruby_bot_server_events_production 19 | -------------------------------------------------------------------------------- /spec/database_adapters/activerecord/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'activerecord' 4 | 5 | ActiveRecord::Schema.define do 6 | self.verbose = false 7 | 8 | create_table :teams, force: true do |t| 9 | t.string :team_id 10 | t.string :name 11 | t.string :domain 12 | t.string :token 13 | t.string :bot_user_id 14 | t.string :activated_user_id 15 | t.string :activated_user_access_token 16 | t.boolean :active, default: true 17 | t.timestamps 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/requests/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Requests 6 | class Request < ActiveSupport::HashWithIndifferentAccess 7 | attr_reader :request 8 | 9 | def initialize(params, request) 10 | @request = request 11 | super(params) 12 | end 13 | 14 | def logger 15 | SlackRubyBotServer::Api::Middleware.logger 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV['RACK_ENV'] = 'test' 4 | ENV['DATABASE_ADAPTER'] ||= 'mongoid' 5 | 6 | require 'logger' 7 | 8 | Bundler.require 9 | 10 | require 'rack/test' 11 | require 'slack-ruby-bot-server/rspec' 12 | 13 | Dir[File.join(__dir__, 'support', '**/*.rb')].sort.each do |file| 14 | require file 15 | end 16 | 17 | SlackRubyBotServer::Service.logger.level = Logger::WARN 18 | 19 | Dir[File.join(__dir__, 'database_adapters', SlackRubyBotServer::Config.database_adapter.to_s, '**/*.rb')].sort.each do |file| 20 | require file 21 | end 22 | -------------------------------------------------------------------------------- /spec/database_adapters/activerecord/activerecord.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | yml = ERB.new(File.read(File.expand_path('postgresql.yml', __dir__))).result 4 | db_config = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') 5 | YAML.safe_load(yml, aliases: true)[ENV.fetch('RACK_ENV', nil)] 6 | else 7 | YAML.safe_load(yml, [], [], true)[ENV.fetch('RACK_ENV', nil)] 8 | end 9 | ActiveRecord::Tasks::DatabaseTasks.create(db_config) 10 | ActiveRecord::Base.establish_connection(db_config) 11 | ActiveRecord::Base.logger ||= Logger.new(STDOUT) 12 | ActiveRecord::Base.logger.level = :info 13 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: danger 3 | on: [pull_request] 4 | jobs: 5 | danger: 6 | runs-on: ubuntu-latest 7 | env: 8 | BUNDLE_GEMFILE: Gemfile.danger 9 | steps: 10 | - uses: actions/checkout@v2 11 | with: 12 | fetch-depth: 0 13 | - name: Set up Ruby 14 | uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: 2.7 17 | bundler-cache: true 18 | - name: Run Danger 19 | run: | 20 | # the personal token is public, this is ok, base64 encode to avoid tripping Github 21 | TOKEN=$(echo -n Z2hwX0xNQ3VmanBFeTBvYkZVTWh6NVNqVFFBOEUxU25abzBqRUVuaAo= | base64 --decode) 22 | DANGER_GITHUB_API_TOKEN=$TOKEN bundle exec danger --verbose 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | case ENV.fetch('DATABASE_ADAPTER', nil) 6 | when 'mongoid' 7 | gem 'kaminari-mongoid' 8 | gem 'mongoid', ENV['MONGOID_VERSION'] || '~> 7.3.0' 9 | gem 'mongoid-scroll' 10 | gem 'mutex_m' 11 | when 'activerecord' 12 | gem 'activerecord' 13 | gem 'otr-activerecord' 14 | gem 'pagy_cursor' 15 | gem 'pg' 16 | gem 'virtus' 17 | when nil 18 | warn "Missing ENV['DATABASE_ADAPTER']." 19 | else 20 | warn "Invalid ENV['DATABASE_ADAPTER']: #{ENV.fetch('DATABASE_ADAPTER', nil)}." 21 | end 22 | 23 | gemspec 24 | 25 | group :development, :test do 26 | gem 'bundler' 27 | gem 'database_cleaner' 28 | gem 'fabrication' 29 | gem 'faker' 30 | gem 'hyperclient' 31 | gem 'rack-test' 32 | gem 'rake' 33 | gem 'rspec' 34 | gem 'rubocop', '1.80.2' 35 | gem 'rubocop-rake' 36 | gem 'rubocop-rspec' 37 | gem 'vcr' 38 | gem 'webmock' 39 | end 40 | -------------------------------------------------------------------------------- /slack-ruby-bot-server-events.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'slack-ruby-bot-server-events/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'slack-ruby-bot-server-events' 9 | spec.version = SlackRubyBotServer::Events::VERSION 10 | spec.authors = ['Daniel Doubrovkine'] 11 | spec.email = ['dblock@dblock.org'] 12 | 13 | spec.summary = 'Slack commands, interactive buttons, and events extension for slack-ruby-bot-server.' 14 | spec.homepage = 'https://github.com/slack-ruby/slack-ruby-bot-server-events' 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) } 17 | spec.require_paths = ['lib'] 18 | 19 | spec.add_dependency 'slack-ruby-bot-server', '>= 0.12.0' 20 | spec.metadata['rubygems_mfa_required'] = 'true' 21 | end 22 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api/endpoints/slack/events_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Api 6 | module Endpoints 7 | module Slack 8 | class EventsEndpoint < Grape::API 9 | desc 'Handle Slack events.' 10 | params do 11 | requires :token, type: String 12 | requires :type, type: String 13 | optional :challenge, type: String 14 | end 15 | post '/event' do 16 | event = SlackRubyBotServer::Events::Requests::Event.new(params, request) 17 | type = event[:type] 18 | event_type = event[:event][:type] if event.key?(:event) 19 | key = [type, event_type].compact 20 | SlackRubyBotServer::Events.config.run_callbacks(:event, key, event) || body(false) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server/api/endpoints.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Api 5 | module Endpoints 6 | class RootEndpoint 7 | namespace :slack do 8 | format :json 9 | 10 | before do 11 | ::Slack::Events::Request.new( 12 | request, 13 | signing_secret: SlackRubyBotServer::Events.config.signing_secret, 14 | signature_expires_in: SlackRubyBotServer::Events.config.signature_expires_in 15 | ).verify! 16 | rescue ::Slack::Events::Request::TimestampExpired 17 | error!('Invalid Signature', 403) 18 | end 19 | 20 | mount SlackRubyBotServer::Events::Api::Endpoints::Slack::CommandsEndpoint 21 | mount SlackRubyBotServer::Events::Api::Endpoints::Slack::ActionsEndpoint 22 | mount SlackRubyBotServer::Events::Api::Endpoints::Slack::EventsEndpoint 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/slack-ruby-bot-server-events/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SlackRubyBotServer::Events::Config do 6 | it 'defaults signature_expires_in' do 7 | expect(SlackRubyBotServer::Events.config.signature_expires_in).to eq 300 8 | end 9 | 10 | context 'with ENV[SLACK_SIGNING_SECRET] set' do 11 | before do 12 | allow(ENV).to receive(:fetch) { |k| "#{k} was set" } 13 | SlackRubyBotServer::Events.config.reset! 14 | end 15 | 16 | it 'sets signing_secret' do 17 | expect(SlackRubyBotServer::Events.config.signing_secret).to eq 'SLACK_SIGNING_SECRET was set' 18 | end 19 | end 20 | 21 | %i[ 22 | signing_secret 23 | signature_expires_in 24 | ].each do |k| 25 | context "with #{k} set" do 26 | before do 27 | SlackRubyBotServer::Events.configure do |config| 28 | config.send("#{k}=", 'set') 29 | end 30 | end 31 | 32 | it "sets and returns #{k}" do 33 | expect(SlackRubyBotServer::Events.config.send(k)).to eq 'set' 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api/endpoints/slack/commands_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Api 6 | module Endpoints 7 | module Slack 8 | class CommandsEndpoint < Grape::API 9 | desc 'Respond to slash commands.' 10 | params do 11 | requires :command, type: String 12 | requires :text, type: String 13 | requires :token, type: String 14 | requires :user_id, type: String 15 | requires :channel_id, type: String 16 | requires :channel_name, type: String 17 | requires :team_id, type: String 18 | end 19 | post '/command' do 20 | command = SlackRubyBotServer::Events::Requests::Command.new(params, request) 21 | command_name = command[:command] 22 | SlackRubyBotServer::Events.config.run_callbacks(:command, command_name, command) || body(false) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2025 Daniel Doubrovkine & Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spec/support/api/endpoints/endpoint_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'hyperclient' 4 | 5 | module SlackRubyBotServer 6 | module Events 7 | module Api 8 | module Test 9 | module EndpointTest 10 | extend ActiveSupport::Concern 11 | include Rack::Test::Methods 12 | 13 | included do 14 | let(:client) do 15 | Hyperclient.new('http://example.org/api') do |client| 16 | client.headers = { 17 | 'Content-Type' => 'application/json', 18 | 'Accept' => 'application/json,application/hal+json' 19 | } 20 | client.connection(default: false) do |conn| 21 | conn.request :json 22 | conn.response :json 23 | conn.use Faraday::Response::RaiseError 24 | conn.use FaradayMiddleware::FollowRedirects 25 | conn.use Faraday::Adapter::Rack, app 26 | end 27 | end 28 | end 29 | end 30 | 31 | def app 32 | SlackRubyBotServer::Api::Middleware.instance 33 | end 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /.github/workflows/test-mongodb.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test with mongodb 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | entry: 10 | - { ruby: 3.1, mongoid: 6.4.8, mongodb: 4.4 } 11 | - { ruby: 3.1, mongoid: 7.3.0, mongodb: 5.0 } 12 | - { ruby: 3.1, mongoid: 7.3.0, mongodb: 5.0 } 13 | - { ruby: 3.2, mongoid: 8.1.11, mongodb: 6.0 } 14 | - { ruby: 3.4, mongoid: 8.1.11, mongodb: 7.0 } 15 | - { ruby: 3.4, mongoid: 9.0.8, mongodb: 8.0 } 16 | name: test (ruby=${{ matrix.entry.ruby }}, mongoid=${{ matrix.entry.mongoid }}, mongodb=${{ matrix.entry.mongodb }}) 17 | steps: 18 | - uses: actions/checkout@v2 19 | - uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.entry.ruby }} 22 | - uses: supercharge/mongodb-github-action@1.7.0 23 | with: 24 | mongodb-version: ${{ matrix.entry.mongodb }} 25 | - name: Test 26 | run: | 27 | bundle install 28 | bundle exec rake spec 29 | env: 30 | DATABASE_ADAPTER: mongoid 31 | DATABASE_URL: "mongodb://localhost" 32 | MONGOID_VERSION: ${{ matrix.entry.mongoid }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test-postgresql.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test with postgresql 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | entry: 10 | - { ruby: 3.1, postgresql: 11 } 11 | - { ruby: 3.1, postgresql: 14 } 12 | - { ruby: 3.1, postgresql: 14 } 13 | - { ruby: 3.2, postgresql: 15 } 14 | - { ruby: 3.3, postgresql: 16 } 15 | - { ruby: 3.4, postgresql: 17 } 16 | name: test (ruby=${{ matrix.entry.ruby }}, postgresql=${{ matrix.entry.postgresql }}) 17 | services: 18 | postgres: 19 | image: postgres:${{ matrix.entry.postgresql }} 20 | env: 21 | POSTGRES_USER: test 22 | POSTGRES_PASSWORD: password 23 | POSTGRES_DB: slack_ruby_bot_server_events_test 24 | ports: 25 | - 5432:5432 26 | # needed because the postgres container does not provide a healthcheck 27 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: ruby/setup-ruby@v1 31 | with: 32 | ruby-version: ${{ matrix.entry.ruby }} 33 | - name: Test 34 | run: | 35 | bundle install 36 | bundle exec rake spec 37 | env: 38 | DATABASE_ADAPTER: activerecord 39 | DATABASE_URL: postgres://test:password@localhost/slack_ruby_bot_server_events_test 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Changelog 2 | 3 | #### 0.4.1 (Next) 4 | 5 | * Your contribution here. 6 | 7 | #### 0.4.0 (2025/09/22) 8 | 9 | * [#18](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/18): Use pagy_cursor in place of cursor_pagination - [@duffn](https://github.com/duffn). 10 | * [#26](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/26): Upgraded RuboCop to 1.80.2 - [@dblock](https://github.com/dblock). 11 | 12 | #### 0.3.2 (2022/06/09) 13 | 14 | * [#13](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/13): Replace Travis CI with Github Actions - [@CrazyOptimist](https://github.com/CrazyOptimist). 15 | * [#10](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/10): Fix actions endpoint for block dropdowns - [@maths22](https://github.com/maths22). 16 | * [#14](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/14): Added support for Ruby 3.1 - [@CrazyOptimist](https://github.com/CrazyOptimist). 17 | 18 | #### 0.3.1 (2021/02/04) 19 | 20 | * [#9](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/9): Provide default value when actions are missing in payload - [@nijave](https://github.com/nijave). 21 | 22 | #### 0.3.0 (2020/12/30) 23 | 24 | * [#6](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/6): Add support for block_actions interaction type - [@CrazyOptimist](https://github.com/CrazyOptimist). 25 | 26 | #### 0.2.0 (2020/07/19) 27 | 28 | * Include action type in actions callbacks - [@dblock](https://github.com/dblock). 29 | 30 | #### 0.1.0 (2020/07/19) 31 | 32 | * Initial public release - [@dblock](https://github.com/dblock). 33 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing Slack-Ruby-Bot-Server-Events 2 | 3 | There're no hard rules about when to release slack-ruby-bot-server-events. Release bug fixes frequently, features not so frequently and breaking API changes rarely. 4 | 5 | ### Release 6 | 7 | Run tests, check that all tests succeed locally. 8 | 9 | ``` 10 | bundle install 11 | rake 12 | ``` 13 | 14 | Check that the last build succeeded in [Travis CI](https://travis-ci.org/slack-ruby/slack-ruby-bot-server-events) for all supported platforms. 15 | 16 | Change "Next" in [CHANGELOG.md](CHANGELOG.md) to the current date. 17 | 18 | ``` 19 | ### 0.2.2 (7/10/2015) 20 | ``` 21 | 22 | Remove the line with "Your contribution here.", since there will be no more contributions to this release. 23 | 24 | Commit your changes. 25 | 26 | ``` 27 | git add CHANGELOG.md 28 | git commit -m "Preparing for release, 0.2.2." 29 | git push origin master 30 | ``` 31 | 32 | Release. 33 | 34 | ``` 35 | $ rake release 36 | 37 | slack-ruby-bot-server-events 0.2.2 built to pkg/slack-ruby-bot-server-events-0.2.2.gem. 38 | Tagged v0.2.2. 39 | Pushed git commits and tags. 40 | Pushed slack-ruby-bot-server-events 0.2.2 to rubygems.org. 41 | ``` 42 | 43 | ### Prepare for the Next Version 44 | 45 | Add the next release to [CHANGELOG.md](CHANGELOG.md). 46 | 47 | ``` 48 | ### 0.2.3 (Next) 49 | 50 | * Your contribution here. 51 | ``` 52 | 53 | Increment the third version number in [lib/slack-ruby-bot-server-events/version.rb](lib/slack-ruby-bot-server-events/version.rb). 54 | 55 | Commit your changes. 56 | 57 | ``` 58 | git add CHANGELOG.md lib/slack-ruby-bot-server-events/version.rb 59 | git commit -m "Preparing for next development iteration, 0.2.3." 60 | git push origin master 61 | ``` 62 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Config 6 | extend self 7 | 8 | ATTRIBUTES = %i[ 9 | signing_secret 10 | signature_expires_in 11 | callbacks 12 | ].freeze 13 | 14 | attr_accessor(*Config::ATTRIBUTES) 15 | 16 | def reset! 17 | self.callbacks = Hash.new { |h, k| h[k] = [] } 18 | self.signing_secret = ENV.fetch('SLACK_SIGNING_SECRET', nil) 19 | self.signature_expires_in = 5 * 60 20 | 21 | on :event, 'url_verification' do |event| 22 | { challenge: event[:challenge] } 23 | end 24 | end 25 | 26 | def on(type, *values, &block) 27 | value_key = values.compact.join('/') if values.any? 28 | key = [type.to_s, value_key].compact.join('/') 29 | callbacks[key] << block 30 | end 31 | 32 | def run_callbacks(type, value, args) 33 | callbacks = [] 34 | 35 | keys = ([type.to_s] + Array(value)).compact 36 | 37 | # more specific callbacks first 38 | while keys.any? 39 | callbacks += self.callbacks[keys.join('/')] 40 | keys.pop 41 | end 42 | 43 | return nil unless callbacks&.any? 44 | 45 | callbacks.each do |c| 46 | rc = c.call(args || value) 47 | return rc if rc 48 | end 49 | 50 | nil 51 | end 52 | end 53 | 54 | class << self 55 | def configure 56 | block_given? ? yield(Config) : Config 57 | end 58 | 59 | def config 60 | Config 61 | end 62 | end 63 | end 64 | end 65 | 66 | SlackRubyBotServer::Events::Config.reset! 67 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2025-09-22 13:18:33 UTC using RuboCop version 1.80.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: Severity. 11 | Gemspec/RequiredRubyVersion: 12 | Exclude: 13 | - 'slack-ruby-bot-server-events.gemspec' 14 | 15 | # Offense count: 1 16 | # Configuration parameters: ExpectMatchingDefinition, CheckDefinitionPathHierarchy, CheckDefinitionPathHierarchyRoots, Regex, IgnoreExecutableScripts, AllowedAcronyms. 17 | # CheckDefinitionPathHierarchyRoots: lib, spec, test, src 18 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 19 | Naming/FileName: 20 | Exclude: 21 | - 'Rakefile.rb' 22 | - 'lib/slack-ruby-bot-server-events.rb' 23 | 24 | # Offense count: 7 25 | RSpec/AnyInstance: 26 | Exclude: 27 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/actions_endpoint_spec.rb' 28 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/commands_endpoint_spec.rb' 29 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/events_endpoint_spec.rb' 30 | 31 | # Offense count: 2 32 | # Configuration parameters: Prefixes, AllowedPatterns. 33 | # Prefixes: when, with, without 34 | RSpec/ContextWording: 35 | Exclude: 36 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/actions_endpoint_spec.rb' 37 | 38 | # Offense count: 3 39 | # Configuration parameters: CountAsOne. 40 | RSpec/ExampleLength: 41 | Max: 9 42 | 43 | # Offense count: 22 44 | RSpec/MultipleExpectations: 45 | Max: 2 46 | 47 | # Offense count: 5 48 | # Configuration parameters: AllowedGroups. 49 | RSpec/NestedGroups: 50 | Max: 5 51 | 52 | # Offense count: 5 53 | # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata. 54 | RSpec/SpecFilePathFormat: 55 | Exclude: 56 | - '**/spec/routing/**/*' 57 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/actions_endpoint_spec.rb' 58 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/commands_endpoint_spec.rb' 59 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/events_endpoint_spec.rb' 60 | - 'spec/slack-ruby-bot-server-events/config_spec.rb' 61 | - 'spec/slack-ruby-bot-server-events/version_spec.rb' 62 | 63 | # Offense count: 1 64 | # This cop supports unsafe autocorrection (--autocorrect-all). 65 | Style/GlobalStdStream: 66 | Exclude: 67 | - 'spec/database_adapters/activerecord/activerecord.rb' 68 | 69 | # Offense count: 1 70 | # This cop supports safe autocorrection (--autocorrect). 71 | # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. 72 | Style/GuardClause: 73 | Exclude: 74 | - 'spec/slack-ruby-bot-server-events/api/endpoints/slack/events_endpoint_spec.rb' 75 | -------------------------------------------------------------------------------- /spec/slack-ruby-bot-server-events/api/endpoints/slack/commands_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SlackRubyBotServer::Events::Api::Endpoints::Slack::CommandsEndpoint do 6 | include SlackRubyBotServer::Events::Api::Test::EndpointTest 7 | 8 | it 'checks signature' do 9 | post '/api/slack/command' 10 | expect(last_response.status).to eq 403 11 | response = JSON.parse(last_response.body) 12 | expect(response).to eq('error' => 'Invalid Signature') 13 | end 14 | 15 | context 'without signature checks' do 16 | before do 17 | allow_any_instance_of(Slack::Events::Request).to receive(:verify!) 18 | end 19 | 20 | let(:command) do 21 | { 22 | command: '/invalid', 23 | text: 'invalid`', 24 | channel_id: 'channel', 25 | channel_name: 'channel_name', 26 | user_id: 'user_id', 27 | team_id: 'team_id', 28 | token: 'deprecated' 29 | } 30 | end 31 | 32 | it 'returns nothing if command is not handled' do 33 | post '/api/slack/command', command 34 | expect(last_response.status).to eq 204 35 | end 36 | 37 | context 'with a command' do 38 | before do 39 | SlackRubyBotServer::Events.configure do |config| 40 | config.on :command do |command| 41 | case command[:command] 42 | when '/test' 43 | { text: 'Success!' } 44 | else 45 | { text: "Unknown command: #{command[:command]}" } 46 | end 47 | end 48 | end 49 | end 50 | 51 | it 'invokes command' do 52 | post '/api/slack/command', command.merge(command: '/test') 53 | expect(last_response.status).to eq 201 54 | response = JSON.parse(last_response.body) 55 | expect(response).to eq('text' => 'Success!') 56 | end 57 | 58 | it 'handles unknown command' do 59 | post '/api/slack/command', command.merge(command: '/invalid') 60 | expect(last_response.status).to eq 201 61 | response = JSON.parse(last_response.body) 62 | expect(response).to eq('text' => 'Unknown command: /invalid') 63 | end 64 | end 65 | 66 | context 'with a named command' do 67 | before do 68 | SlackRubyBotServer::Events.configure do |config| 69 | config.on :command, '/foo' do |_command| 70 | { text: 'Foo!' } 71 | end 72 | 73 | config.on :command, '/test' do |_command| 74 | { text: 'Test!' } 75 | end 76 | 77 | config.on :command do |command| 78 | { text: "Invoked command #{command[:command]}." } 79 | end 80 | end 81 | end 82 | 83 | it 'invokes command' do 84 | post '/api/slack/command', command.merge(command: '/test') 85 | expect(last_response.status).to eq 201 86 | response = JSON.parse(last_response.body) 87 | expect(response).to eq('text' => 'Test!') 88 | end 89 | 90 | it 'invokes another command' do 91 | post '/api/slack/command', command.merge(command: '/foo') 92 | expect(last_response.status).to eq 201 93 | response = JSON.parse(last_response.body) 94 | expect(response).to eq('text' => 'Foo!') 95 | end 96 | 97 | it 'handles unknown command' do 98 | post '/api/slack/command', command.merge(command: '/invalid') 99 | expect(last_response.status).to eq 201 100 | response = JSON.parse(last_response.body) 101 | expect(response).to eq('text' => 'Invoked command /invalid.') 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/slack-ruby-bot-server-events/api/endpoints/slack/actions_endpoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SlackRubyBotServer 4 | module Events 5 | module Api 6 | module Endpoints 7 | module Slack 8 | class ActionsEndpoint < Grape::API 9 | desc 'Respond to interactive slack buttons and actions.' 10 | params do 11 | requires :payload, type: JSON do 12 | requires :type, type: String 13 | given type: ->(val) { %w[message_action shortcut].include? val } do 14 | requires :token, type: String 15 | requires :callback_id, type: String 16 | optional :trigger_id, type: String 17 | optional :response_url, type: String 18 | optional :channel, type: Hash do 19 | requires :id, type: String 20 | optional :name, type: String 21 | end 22 | requires :user, type: Hash do 23 | requires :id, type: String 24 | optional :name, type: String 25 | end 26 | requires :team, type: Hash do 27 | requires :id, type: String 28 | optional :domain, type: String 29 | end 30 | optional :actions, type: Array do 31 | requires :value, type: String 32 | end 33 | optional :message, type: Hash do 34 | requires :type, type: String 35 | optional :user, type: String 36 | requires :ts, type: String 37 | requires :text, type: String 38 | end 39 | end 40 | 41 | given type: ->(val) { val == 'block_actions' } do 42 | optional :trigger_id, type: String 43 | optional :response_url, type: String 44 | requires :token, type: String 45 | requires :user, type: Hash do 46 | requires :id, type: String 47 | optional :name, type: String 48 | end 49 | requires :team, type: Hash do 50 | requires :id, type: String 51 | optional :domain, type: String 52 | end 53 | requires :actions, type: Array do 54 | requires :action_id, type: String 55 | optional :block_id, type: String 56 | optional :type, type: String 57 | optional :action_ts, type: String 58 | end 59 | optional :message, type: Hash do 60 | requires :type, type: String 61 | optional :user, type: String 62 | requires :ts, type: String 63 | requires :text, type: String 64 | optional :blocks, type: Array do 65 | requires :type, type: String 66 | requires :block_id, type: String 67 | end 68 | end 69 | end 70 | end 71 | end 72 | post '/action' do 73 | action = SlackRubyBotServer::Events::Requests::Action.new(params, request) 74 | payload_type = params[:payload][:type] 75 | callback_id = params[:payload][:callback_id] 76 | action_ids = params[:payload].fetch(:actions, []).map { |entity| entity[:action_id] } 77 | SlackRubyBotServer::Events.config.run_callbacks( 78 | :action, 79 | ([payload_type, callback_id] + action_ids).compact, 80 | action 81 | ) || body(false) 82 | end 83 | end 84 | end 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to SlackRubyBotServer::Events 2 | 3 | This project is work of [many contributors](https://github.com/slack-ruby/slack-ruby-bot-server-events/graphs/contributors). 4 | 5 | You're encouraged to submit [pull requests](https://github.com/slack-ruby/slack-ruby-bot-server-events/pulls), [propose features and discuss issues](https://github.com/slack-ruby/slack-ruby-bot-server-events/issues). 6 | 7 | In the examples below, substitute your Github username for `contributor` in URLs. 8 | 9 | ## Fork the Project 10 | 11 | Fork the [project on Github](https://github.com/slack-ruby/slack-ruby-bot-server-events) and check out your copy. 12 | 13 | ``` 14 | git clone https://github.com/contributor/slack-ruby-bot-server-events.git 15 | cd slack-ruby-bot-server-events 16 | git remote add upstream https://github.com/slack-ruby/slack-ruby-bot-server-events.git 17 | ``` 18 | 19 | ## Create a Topic Branch 20 | 21 | Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. 22 | 23 | ``` 24 | git checkout master 25 | git pull upstream master 26 | git checkout -b my-feature-branch 27 | ``` 28 | 29 | ## Bundle Install and Test 30 | 31 | Ensure that you can build the project and run tests. 32 | 33 | ``` 34 | bundle install 35 | bundle exec rake 36 | ``` 37 | 38 | ## Write Tests 39 | 40 | Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. 41 | Add to [spec](spec). 42 | 43 | We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. 44 | 45 | ## Write Code 46 | 47 | Implement your feature or bug fix. 48 | 49 | Ruby style is enforced with [Rubocop](https://github.com/bbatsov/rubocop). 50 | Run `bundle exec rubocop` and fix any style issues highlighted. 51 | 52 | Make sure that `bundle exec rake` completes without errors. 53 | 54 | ## Write Documentation 55 | 56 | Document any external behavior in the [README](README.md). 57 | 58 | ## Update Changelog 59 | 60 | Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. 61 | Make it look like every other line, including your name and link to your Github account. 62 | 63 | ## Commit Changes 64 | 65 | Make sure git knows your name and email address: 66 | 67 | ``` 68 | git config --global user.name "Your Name" 69 | git config --global user.email "contributor@example.com" 70 | ``` 71 | 72 | Writing good commit logs is important. A commit log should describe what changed and why. 73 | 74 | ``` 75 | git add ... 76 | git commit 77 | ``` 78 | 79 | ## Push 80 | 81 | ``` 82 | git push origin my-feature-branch 83 | ``` 84 | 85 | ## Make a Pull Request 86 | 87 | Go to https://github.com/contributor/slack-ruby-bot-server-events and select your feature branch. 88 | Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. 89 | 90 | ## Rebase 91 | 92 | If you've been working on a change for a while, rebase with upstream/master. 93 | 94 | ``` 95 | git fetch upstream 96 | git rebase upstream/master 97 | git push origin my-feature-branch -f 98 | ``` 99 | 100 | ## Update CHANGELOG Again 101 | 102 | Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows. 103 | 104 | ``` 105 | * [#123](https://github.com/slack-ruby/slack-ruby-bot-server-events/pull/123): Reticulated splines - [@contributor](https://github.com/contributor). 106 | ``` 107 | 108 | Amend your previous commit and force push the changes. 109 | 110 | ``` 111 | git commit --amend 112 | git push origin my-feature-branch -f 113 | ``` 114 | 115 | ## Check on Your Pull Request 116 | 117 | Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. 118 | 119 | ## Be Patient 120 | 121 | It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! 122 | 123 | ## Thank You 124 | 125 | Please do know that we really appreciate and value your time and work. We love you, really. 126 | -------------------------------------------------------------------------------- /spec/slack-ruby-bot-server-events/api/endpoints/slack/events_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SlackRubyBotServer::Events::Api::Endpoints::Slack::EventsEndpoint do 6 | include SlackRubyBotServer::Events::Api::Test::EndpointTest 7 | 8 | it 'checks signature' do 9 | post '/api/slack/event' 10 | expect(last_response.status).to eq 403 11 | response = JSON.parse(last_response.body) 12 | expect(response).to eq('error' => 'Invalid Signature') 13 | end 14 | 15 | context 'without signature checks' do 16 | before do 17 | allow_any_instance_of(Slack::Events::Request).to receive(:verify!) 18 | end 19 | 20 | let(:event) do 21 | { 22 | token: 'deprecated', 23 | team_id: 'team_id', 24 | api_app_id: 'A19GAJ72T', 25 | event: { 26 | type: 'link_shared', 27 | user: 'user_id', 28 | channel: 'C1', 29 | message_ts: '1547842100.001400', 30 | links: [{ 31 | url: 'https://www.example.com', 32 | domain: 'example.com' 33 | }] 34 | }, 35 | type: 'event_callback', 36 | event_id: 'EvFGTNRKLG', 37 | event_time: 1_547_842_101, 38 | authed_users: ['U04KB5WQR'] 39 | } 40 | end 41 | 42 | context 'with an unfurl event' do 43 | before do 44 | SlackRubyBotServer::Events.configure do |config| 45 | config.on :event do |event| 46 | if event[:type] == 'event_callback' && event[:event][:type] == 'link_shared' 47 | event[:event][:links].each do |link| 48 | next unless link[:domain] == 'example.com' 49 | 50 | event.logger.info "UNFURL: #{link[:url]}" 51 | 52 | Slack::Web::Client.new.chat_unfurl( 53 | channel: event[:event][:channel], 54 | ts: event[:event][:message_ts], 55 | unfurls: { 56 | link[:url] => { text: 'unfurl' } 57 | }.to_json 58 | ) 59 | end 60 | else 61 | raise "Event #{event[:type]} is not supported." 62 | end 63 | end 64 | end 65 | end 66 | 67 | it 'performs built-in event challenge' do 68 | post '/api/slack/event', 69 | type: 'url_verification', 70 | challenge: 'challenge', 71 | token: 'deprecated' 72 | expect(last_response.status).to eq 201 73 | response = JSON.parse(last_response.body) 74 | expect(response).to eq('challenge' => 'challenge') 75 | end 76 | 77 | it 'unfurls a URL' do 78 | expect_any_instance_of(Slack::Web::Client).to receive(:chat_unfurl).with( 79 | channel: 'C1', 80 | ts: '1547842100.001400', 81 | unfurls: { 82 | 'https://www.example.com' => { 'text' => 'unfurl' } 83 | }.to_json 84 | ) 85 | 86 | post '/api/slack/event', event 87 | expect(last_response.status).to eq 201 88 | end 89 | end 90 | 91 | context 'with a named event handler' do 92 | before do 93 | SlackRubyBotServer::Events.configure do |config| 94 | config.on :event, 'event_callback/link_shared' do |event| 95 | event.logger.info 'Link shared event called.' 96 | true 97 | end 98 | 99 | config.on :event, 'event_callback' do |event| 100 | event.logger.info "Generic event with event_callback #{event[:event][:type]} called." 101 | true 102 | end 103 | 104 | config.on :event do |event| 105 | event.logger.info "Generic event #{event[:type]} called." 106 | true 107 | end 108 | end 109 | end 110 | 111 | it 'performs built-in event challenge' do 112 | post '/api/slack/event', 113 | type: 'url_verification', 114 | challenge: 'challenge', 115 | token: 'deprecated' 116 | expect(last_response.status).to eq 201 117 | response = JSON.parse(last_response.body) 118 | expect(response).to eq('challenge' => 'challenge') 119 | end 120 | 121 | it 'link shared event callback' do 122 | expect_any_instance_of(Logger).to receive(:info).with('Link shared event called.') 123 | post '/api/slack/event', event 124 | expect(last_response.status).to eq 201 125 | end 126 | 127 | it 'another event callback' do 128 | expect_any_instance_of(Logger).to receive(:info).with('Generic event with event_callback name_of_event called.') 129 | post '/api/slack/event', event.merge(event: { type: 'name_of_event' }) 130 | expect(last_response.status).to eq 201 131 | end 132 | 133 | it 'another event' do 134 | expect_any_instance_of(Logger).to receive(:info).with('Generic event event_type called.') 135 | post '/api/slack/event', event.merge(type: 'event_type') 136 | expect(last_response.status).to eq 201 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Slack Ruby Bot Server Events Extension 2 | ====================================== 3 | 4 | [![Gem Version](https://badge.fury.io/rb/slack-ruby-bot-server-events.svg)](https://badge.fury.io/rb/slack-ruby-bot-server-events) 5 | [![lint](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/rubocop.yml/badge.svg)](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/rubocop.yml) 6 | [![test with mongodb](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/test-mongodb.yml/badge.svg)](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/test-mongodb.yml) 7 | [![test with postgresql](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/test-postgresql.yml/badge.svg)](https://github.com/slack-ruby/slack-ruby-bot-server-events/actions/workflows/test-postgresql.yml) 8 | 9 | An extension to [slack-ruby-bot-server](https://github.com/slack-ruby/slack-ruby-bot-server) that makes it easy to handle Slack slash commands, interactive buttons and events. 10 | 11 | ### Table of Contents 12 | 13 | - [Sample](#sample) 14 | - [Usage](#usage) 15 | - [Gemfile](#gemfile) 16 | - [Configure](#configure) 17 | - [OAuth](#oauth) 18 | - [Events](#events) 19 | - [Implement Callbacks](#implement-callbacks) 20 | - [Events](#events-1) 21 | - [Actions](#actions) 22 | - [Commands](#commands) 23 | - [Copyright & License](#copyright--license) 24 | 25 | ### Sample 26 | 27 | See [slack-ruby/slack-ruby-bot-server-events-sample](https://github.com/slack-ruby/slack-ruby-bot-server-events-sample) for a working sample. 28 | 29 | ### Usage 30 | 31 | #### Gemfile 32 | 33 | Add 'slack-ruby-bot-server-events' to Gemfile. 34 | 35 | ```ruby 36 | gem 'slack-ruby-bot-server-events' 37 | ``` 38 | 39 | #### Configure 40 | 41 | ##### OAuth 42 | 43 | Configure your app's [OAuth version](https://api.slack.com/authentication/oauth-v2) and [scopes](https://api.slack.com/legacy/oauth-scopes) as needed by your application. 44 | 45 | ```ruby 46 | SlackRubyBotServer.configure do |config| 47 | config.oauth_version = :v2 48 | config.oauth_scope = ['users:read', 'channels:read', 'groups:read', 'chat:write', 'commands', 'incoming-webhook'] 49 | end 50 | ``` 51 | 52 | ##### Events 53 | 54 | Configure events-specific settings. 55 | 56 | ```ruby 57 | SlackRubyBotServer::Events.configure do |config| 58 | config.signing_secret = 'secret' 59 | end 60 | ``` 61 | 62 | The following settings are supported. 63 | 64 | setting | description 65 | ----------------------|------------------------------------------------------------------ 66 | signing_secret | Slack signing secret, defaults is `ENV['SLACK_SIGNING_SECRET']`. 67 | signature_expires_in | Signature expiration window in seconds, default is `300`. 68 | 69 | Get the signing secret from [your app's](https://api.slack.com/apps) _Basic Information_ settings. 70 | 71 | #### Implement Callbacks 72 | 73 | This library supports events, actions and commands. When implementing multiple callbacks for each type, the response from the first callback to return a non `nil` value will be used and no further callbacks will be invoked. Callbacks receive subclasses of [SlackRubyBotServer::Events::Requests::Request](lib/slack-ruby-bot-server-events/requests/request.rb). 74 | 75 | #### Events 76 | 77 | Respond to [Slack Events](https://api.slack.com/events-api) by implementing `SlackRubyBotServer::Events::Config#on :event`. 78 | 79 | The following example unfurls URLs. 80 | 81 | ```ruby 82 | SlackRubyBotServer::Events.configure do |config| 83 | config.on :event, 'event_callback', 'link_shared' do |event| 84 | event[:event][:links].each do |link| 85 | Slack::Web::Client.new(token: ...).chat_unfurl( 86 | channel: event[:event][:channel], 87 | ts: event[:event][:message_ts], 88 | unfurls: { 89 | link[:url] => { text: 'Unfurled URL.' } 90 | }.to_json 91 | ) 92 | end 93 | 94 | true # return true to avoid invoking further callbacks 95 | end 96 | 97 | config.on :event, 'event_callback' do |event| 98 | # handle any event callback 99 | false 100 | end 101 | 102 | config.on :event do |event| 103 | # handle any event[:event][:type] 104 | false 105 | end 106 | end 107 | ``` 108 | 109 | 110 | #### Actions 111 | 112 | Respond to [Shortcuts](https://api.slack.com/interactivity/shortcuts) and [Interactive Messages](https://api.slack.com/messaging/interactivity) as well as [Attached Interactive Message Buttons(Outmoded)](https://api.slack.com/legacy/message-buttons) by implementing `SlackRubyBotServer::Events::Config#on :action`. 113 | 114 | The following example posts an ephemeral message that counts the letters in a message shortcut. 115 | 116 | ```ruby 117 | SlackRubyBotServer::Events.configure do |config| 118 | config.on :action, 'interactive_message', 'action_id' do |action| 119 | payload = action[:payload] 120 | message = payload[:message] 121 | 122 | Faraday.post(payload[:response_url], { 123 | text: "The text \"#{message[:text]}\" has #{message[:text].size} letter(s).", 124 | response_type: 'ephemeral' 125 | }.to_json, 'Content-Type' => 'application/json') 126 | 127 | { ok: true } 128 | end 129 | 130 | config.on :action do |action| 131 | # handle any other action 132 | false 133 | end 134 | end 135 | ``` 136 | 137 | The following example responds to an interactive message. 138 | You can compose rich message layouts using [Block Kit Builder](https://app.slack.com/block-kit-builder). 139 | 140 | ```ruby 141 | SlackRubyBotServer::Events.configure do |config| 142 | config.on :action, 'block_actions', 'your_action_id' do |action| 143 | payload = action[:payload] 144 | 145 | Faraday.post(payload[:response_url], { 146 | text: "The action \"your_action_id\" has been invoked.", 147 | response_type: 'ephemeral' 148 | }.to_json, 'Content-Type' => 'application/json') 149 | 150 | { ok: true } 151 | end 152 | end 153 | ``` 154 | 155 | #### Commands 156 | 157 | Respond to [Slash Commands](https://api.slack.com/interactivity/slash-commands) by implementing `SlackRubyBotServer::Events::Config#on :command`. 158 | 159 | The following example responds to `/ping` with `pong`. 160 | 161 | ```ruby 162 | SlackRubyBotServer::Events.configure do |config| 163 | config.on :command, '/ping' do 164 | { text: 'pong' } 165 | end 166 | 167 | config.on :command do |command| 168 | # handle any other command 169 | false 170 | end 171 | end 172 | ``` 173 | 174 | ### Copyright & License 175 | 176 | Copyright [Daniel Doubrovkine](http://code.dblock.org) and Contributors, 2020-2025 177 | 178 | [MIT License](LICENSE) 179 | -------------------------------------------------------------------------------- /spec/slack-ruby-bot-server-events/api/endpoints/slack/actions_endpoint_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe SlackRubyBotServer::Events::Api::Endpoints::Slack::ActionsEndpoint do 6 | include SlackRubyBotServer::Events::Api::Test::EndpointTest 7 | 8 | it 'checks signature' do 9 | post '/api/slack/action' 10 | expect(last_response.status).to eq 403 11 | response = JSON.parse(last_response.body) 12 | expect(response).to eq('error' => 'Invalid Signature') 13 | end 14 | 15 | context 'without signature checks' do 16 | before do 17 | allow_any_instance_of(Slack::Events::Request).to receive(:verify!) 18 | end 19 | 20 | # payload type reference url: https://api.slack.com/reference/interaction-payloads 21 | context 'given payload type is message_actions' do 22 | let(:payload) do 23 | { 24 | type: 'message_action', 25 | channel: { id: 'C12345', name: 'channel' }, 26 | user: { id: 'user_id' }, 27 | team: { id: 'team_id' }, 28 | token: 'deprecated', 29 | callback_id: 'action_id' 30 | } 31 | end 32 | 33 | shared_examples 'message_actions handler' do 34 | it 'performs action' do 35 | post '/api/slack/action', payload: payload.to_json 36 | expect(last_response.status).to eq 201 37 | response = JSON.parse(last_response.body) 38 | expect(response).to eq('text' => 'message_action/action_id') 39 | end 40 | end 41 | 42 | context 'with an action handler' do 43 | before do 44 | SlackRubyBotServer::Events.configure do |config| 45 | config.on :action do |action| 46 | { text: "#{action[:payload][:type]}/#{action[:payload][:callback_id]}" } 47 | end 48 | end 49 | end 50 | 51 | it_behaves_like 'message_actions handler' 52 | 53 | context 'with actions in the payload' do 54 | before do 55 | payload.merge!(actions: [{ name: 'id', value: '43749' }]) 56 | end 57 | 58 | it_behaves_like 'message_actions handler' 59 | end 60 | end 61 | 62 | context 'with a message type action handler' do 63 | before do 64 | SlackRubyBotServer::Events.configure do |config| 65 | config.on :action, 'message_action', 'unique_id' do |_action| 66 | { text: 'message_action: exact match' } 67 | end 68 | 69 | config.on :action, 'message_action' do |_action| 70 | { text: 'message_action: type match' } 71 | end 72 | 73 | config.on :action do |action| 74 | { text: "#{action[:payload][:type]}/#{action[:payload][:callback_id]}" } 75 | end 76 | end 77 | end 78 | 79 | it 'performs specific action' do 80 | post '/api/slack/action', payload: payload.merge(type: 'message_action', callback_id: 'unique_id').to_json 81 | expect(last_response.status).to eq 201 82 | response = JSON.parse(last_response.body) 83 | expect(response).to eq('text' => 'message_action: exact match') 84 | end 85 | 86 | it 'performs default action' do 87 | post '/api/slack/action', payload: payload.merge(type: 'message_action', callback_id: 'updated').to_json 88 | expect(last_response.status).to eq 201 89 | response = JSON.parse(last_response.body) 90 | expect(response).to eq('text' => 'message_action: type match') 91 | end 92 | 93 | it 'performs any action' do 94 | post '/api/slack/action', payload: payload.merge(type: 'global_shortcut', action_id: 'action_id').to_json 95 | expect(last_response.status).to eq 201 96 | response = JSON.parse(last_response.body) 97 | expect(response).to eq('text' => 'global_shortcut/action_id') 98 | end 99 | end 100 | end 101 | 102 | context 'given payload type is block_actions' do 103 | let(:payload) do 104 | { 105 | type: 'block_actions', 106 | response_url: 'https://hooks.slack.com/api/path/to/hook', 107 | user: { id: 'user_id' }, 108 | team: { id: 'team_id' }, 109 | token: 'deprecated', 110 | actions: [ 111 | { type: 'button', action_id: 'action_id' } 112 | ] 113 | } 114 | end 115 | 116 | let(:payload_without_response_url) do 117 | { 118 | type: 'block_actions', 119 | user: { id: 'user_id' }, 120 | team: { id: 'team_id' }, 121 | token: 'deprecated', 122 | actions: [ 123 | { type: 'button', action_id: 'action_id' } 124 | ] 125 | } 126 | end 127 | 128 | context 'with an action handler' do 129 | before do 130 | SlackRubyBotServer::Events.configure do |config| 131 | config.on :action do |action| 132 | { text: "#{action[:payload][:type]}/#{action[:payload][:actions][0][:action_id]}" } 133 | end 134 | end 135 | end 136 | 137 | it 'performs action' do 138 | post '/api/slack/action', payload: payload.to_json 139 | expect(last_response.status).to eq 201 140 | response = JSON.parse(last_response.body) 141 | expect(response).to eq('text' => 'block_actions/action_id') 142 | end 143 | 144 | it 'performs action when payload has no response url' do 145 | post '/api/slack/action', payload: payload_without_response_url.to_json 146 | expect(last_response.status).to eq 201 147 | response = JSON.parse(last_response.body) 148 | expect(response).to eq('text' => 'block_actions/action_id') 149 | end 150 | end 151 | 152 | context 'with a block type actions handler' do 153 | before do 154 | SlackRubyBotServer::Events.configure do |config| 155 | config.on :action, 'block_actions', 'unique_id' do |_action| 156 | { text: 'block_actions: exact match' } 157 | end 158 | 159 | config.on :action, 'block_actions' do |_action| 160 | { text: 'block_actions: type match' } 161 | end 162 | end 163 | end 164 | 165 | it 'performs specific action' do 166 | post '/api/slack/action', payload: payload.merge(type: 'block_actions', actions: [{ action_id: 'unique_id' }]).to_json 167 | expect(last_response.status).to eq 201 168 | response = JSON.parse(last_response.body) 169 | expect(response).to eq('text' => 'block_actions: exact match') 170 | end 171 | 172 | it 'performs default action' do 173 | post '/api/slack/action', payload: payload.merge(type: 'block_actions', actions: [{ action_id: 'updated' }]).to_json 174 | expect(last_response.status).to eq 201 175 | response = JSON.parse(last_response.body) 176 | expect(response).to eq('text' => 'block_actions: type match') 177 | end 178 | end 179 | end 180 | end 181 | end 182 | --------------------------------------------------------------------------------