slack_interactive_client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3441dda9fe55fa50b7780ddf42a8695425785bd91dce7d3993c84878cc6edee0
4
+ data.tar.gz: eca34dfd218a21d82031db317454ebd53f62c1196a0cdb8c9ebc487a64409da6
5
+ SHA512:
6
+ metadata.gz: 9682852dbf51d73af56389b70816bc50c321a2b61023f3d64abb8082366cfaf2f6b7ed83aef1edf8b9db55d07f16595a2fb77e09bb3a73cba29d7f22d2215c0c
7
+ data.tar.gz: 775ad4048c834105a706cef29ff8fad6a8eea2779ad4270317dbbb9bfb45eb4281f074c9513f967a3dc3c8734b02c35ff7dfd0cc11f333b384753ee4f012f6a4
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'csv'
5
+ require_relative 'concerns/slack_interaction_params'
6
+ require_relative 'concerns/slack_pattern_interaction'
7
+
8
+ module SlackInteractiveClient
9
+ class BaseInteraction
10
+ include ::SlackInteractionParams
11
+ include ::SlackPatternInteraction
12
+
13
+ class << self
14
+ attr_reader :interaction_config, :interaction_channels, :pattern_string
15
+
16
+ # EXAMPLE
17
+ #
18
+ # interaction_options can_interact: { only: ['micah.bowie'] }, channels: []
19
+ def interaction_options(options = {})
20
+ set_interaction_config(options[:can_interact])
21
+ @interaction_channels = options[:channels].empty? ? :all : options[:channels]
22
+ end
23
+
24
+ def interaction_pattern(pattern_string = '')
25
+ @pattern_string = pattern_string.strip.squeeze
26
+ end
27
+
28
+ private
29
+
30
+ def set_interaction_config(options)
31
+ @interaction_config ||= { all: [], only: [], except: [] }
32
+ return unless options.present?
33
+
34
+ if options == :all
35
+ @interaction_config[:all] << :all
36
+ return
37
+ end
38
+
39
+ if options[:only] && options.is_a?(Hash)
40
+ @interaction_config[:only] += Array(options[:only])
41
+ end
42
+
43
+ if options[:except] && options.is_a?(Hash)
44
+ @interaction_config[:except] += Array(options[:except])
45
+ end
46
+ end
47
+ end
48
+
49
+ attr_reader :interaction_params
50
+ def initialize(webhook_params = {})
51
+ @interaction_params = webhook_params
52
+ @pattern = self.class.pattern_string
53
+ @extracted_values = {}
54
+ extract_values(slack_message_text)
55
+ end
56
+
57
+ def call
58
+ unless can_interact?
59
+ send_message('You are not authorized to interact with this command or interaction is in an invalid channel.')
60
+ return
61
+ end
62
+ execute
63
+ end
64
+
65
+ def execute
66
+ puts "Method not reimplemented by child"
67
+ end
68
+
69
+ private
70
+
71
+ def direct_message?
72
+ slack_channel_name == ::SlackInteractionParams::DIRECT_MESSAGE_CHANNEL
73
+ end
74
+
75
+ def can_user_interact?
76
+ # If not configuration is set then we allow any user to interact
77
+ return true if self.class.interaction_config.nil?
78
+
79
+ all_conditions = self.class.interaction_config[:all]
80
+ only_conditions = self.class.interaction_config[:only]
81
+ except_conditions = self.class.interaction_config[:except]
82
+
83
+ return true if all_conditions.empty? && only_conditions.empty? && except_conditions.empty?
84
+ return true if all_conditions.include?(:all)
85
+ return true if only_conditions.include?(slack_user_name)
86
+ return false if except_conditions.include?(slack_user_name)
87
+
88
+ false
89
+ end
90
+
91
+ def interactive_channel?
92
+ # If not configuration is set then we allow any user to interact
93
+ return true if self.class.interaction_channels.nil?
94
+
95
+ interaction_channels = self.class.interaction_channels
96
+
97
+ return true if interaction_channels == :all
98
+ return true if interaction_channels == :direct_message && direct_message?
99
+ return false if interaction_channels == :direct_message && !direct_message?
100
+ return false unless interaction_channels.is_a?(Array)
101
+ return true if direct_message? && interaction_channels.include?(:direct_message)
102
+ return true if interaction_channels.include?(slack_channel_name)
103
+
104
+ false
105
+ end
106
+
107
+ def can_interact?
108
+ can_user_interact? && interactive_channel?
109
+ end
110
+
111
+ def send_message(message)
112
+ ::SlackInteractiveClient::Client.send_message(message, slack_channel_id)
113
+ end
114
+
115
+ def send_csv(csv_data)
116
+ csv_content ||= CSV.generate do |csv|
117
+ csv << csv_data.dig(:headers)
118
+ csv_data.dig(:rows).each { |row| csv << row }
119
+ end
120
+ csv_data[:content] = csv_content
121
+
122
+ ::SlackInteractiveClient::Client.send_csv(csv_data, slack_channel_id)
123
+ end
124
+
125
+ def send_message_with_template(template, args)
126
+ ::SlackInteractiveClient::Client.send_message_with_template(template, args.merge(channel: slack_channel_id))
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EXAMPLE
4
+ #
5
+ # SlackInteractiveClient::Bot.define do
6
+ # interaction :hello_world, HelloWorldInteraction
7
+ # end
8
+ #
9
+ require 'active_support'
10
+
11
+ module SlackInteractiveClient
12
+ class Bot
13
+ class << self
14
+ attr_reader :interactions
15
+
16
+ def define(&block)
17
+ @interactions ||= HashWithIndifferentAccess.new
18
+ instance_exec(&block)
19
+ end
20
+
21
+ def interaction(name, interaction_class)
22
+ @interactions[name] = interaction_class
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'slack-ruby-client'
4
+ require_relative 'configuration'
5
+ require_relative 'message'
6
+ require_relative 'message_stop_guard'
7
+
8
+ module SlackInteractiveClient
9
+ class Client
10
+ class << self
11
+
12
+ attr_writer :configuration
13
+
14
+ def configuration
15
+ @configuration ||= ::SlackInteractiveClient::Configuration.new
16
+ end
17
+
18
+ def reset
19
+ @configuration = nil
20
+ end
21
+
22
+ def configure
23
+ yield configuration
24
+ ::Slack.configure do |config|
25
+ config.token = configuration.auth_token
26
+ config.logger = configuration.logger_instance
27
+ end
28
+ "SlackInteractiveClient::Client Configured!"
29
+ end
30
+
31
+ def send_message_with_template(template_name, args = {})
32
+ compilation = ::SlackInteractiveClient::Message.compile(template_name, args)
33
+
34
+ web_client.chat_postMessage(
35
+ channel: compilation.channel,
36
+ blocks: compilation.blocks,
37
+ as_user: true,
38
+ )
39
+
40
+ if compilation.csv? && compilation.csv_data[:csv][:content]
41
+ web_client.files_upload(
42
+ channels: compilation.channel,
43
+ content: compilation.csv_data[:csv][:content],
44
+ title: compilation.csv_data[:csv][:title],
45
+ filename: compilation.csv_data[:csv][:filename],
46
+ initial_comment: compilation.csv_data[:csv][:comment],
47
+ as_user: true,
48
+ )
49
+ end
50
+ return "Message sent by: SlackInteractiveClient::Client"
51
+ rescue ::Slack::Web::Api::Errors::SlackError => e
52
+ puts "An error occurred when sending the message: #{e.message}"
53
+ end
54
+
55
+ def send_message(message, channel)
56
+ scrubbed = ::SlackInteractiveClient::MessageStopGuard.scrubber.call(message)
57
+ web_client.chat_postMessage(
58
+ channel: channel,
59
+ blocks: [{ type: 'rich_text', elements: [ type: 'rich_text_section', elements: [ { type: 'text', text: scrubbed } ] ] }],
60
+ as_user: true,
61
+ )
62
+ return "Message sent by: SlackInteractiveClient::Client"
63
+ rescue ::Slack::Web::Api::Errors::SlackError => e
64
+ puts "An error occurred when sending the message: #{e.message}"
65
+ end
66
+
67
+ def send_csv(csv_data, channel)
68
+ web_client.files_upload(
69
+ channels: channel,
70
+ content: csv_data[:content],
71
+ title: csv_data[:title],
72
+ filename: csv_data[:filename],
73
+ initial_comment: csv_data[:comment],
74
+ as_user: true,
75
+ )
76
+ return "Message sent by: SlackInteractiveClient::Client"
77
+ rescue ::Slack::Web::Api::Errors::SlackError => e
78
+ puts "An error occurred when sending the message: #{e.message}"
79
+ end
80
+
81
+ private
82
+
83
+ def web_client
84
+ ::Slack::Web::Client.new
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+
5
+ module SlackInteractionParams
6
+ DIRECT_MESSAGE_CHANNEL = "directmessage".freeze
7
+
8
+ def slack_command
9
+ interaction_params[:command]
10
+ end
11
+
12
+ def slack_user_name
13
+ interaction_params[:user_name]
14
+ end
15
+
16
+ def slack_token
17
+ interaction_params[:token]
18
+ end
19
+
20
+ def slack_team_id
21
+ interaction_params[:team_id]
22
+ end
23
+
24
+ def slack_channel_id
25
+ interaction_params[:channel_id]
26
+ end
27
+
28
+ def slack_message_text
29
+ return nil unless interaction_params[:text].present? && interaction_params[:text].is_a?(String)
30
+
31
+ interaction_params[:text].strip.squeeze
32
+ end
33
+
34
+ def slack_user_id
35
+ interaction_params[:user_id]
36
+ end
37
+
38
+ def slack_response_url
39
+ interaction_params[:response_url]
40
+ end
41
+
42
+ def slack_channel_name
43
+ interaction_params[:channel_name]
44
+ end
45
+
46
+ def slack_team_domain
47
+ interaction_params[:team_domain]
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support'
3
+
4
+ module SlackPatternInteraction
5
+ def method_missing(method_name, *arguments, &block)
6
+ if @extracted_values.has_key?(method_name.to_s)
7
+ @extracted_values[method_name.to_s]
8
+ else
9
+ super
10
+ end
11
+ end
12
+
13
+ def respond_to_missing?(method_name, include_private = false)
14
+ @extracted_values.has_key?(method_name.to_s) || super
15
+ end
16
+
17
+ private
18
+
19
+ def extract_values(string)
20
+ return {} if string.blank?
21
+ # Find all dynamic parts of the pattern
22
+ dynamic_parts = @pattern.scan(/\$\[(.*?)\]/).flatten
23
+
24
+ # Create a regex pattern replacing dynamic parts with capture groups
25
+ regex_pattern = @pattern.gsub(/\$\[.*?\]/, '(\w+)')
26
+ regex = Regexp.new(regex_pattern)
27
+
28
+ # Match the string with the regex
29
+ match = string.match(regex)
30
+
31
+ return unless match
32
+
33
+ # Iterate over each capture and store in the hash
34
+ dynamic_parts.each_with_index do |variable_name, index|
35
+ @extracted_values[variable_name] = match[index + 1]
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ class Configuration
5
+ attr_accessor :auth_token, :redis_instance, :logger_instance, :base_controller_class, :signing_secret
6
+
7
+ def initialize
8
+ @base_controller_class = "ApplicationController"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace SlackInteractiveClient
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_interaction"
4
+
5
+ module SlackInteractiveClient
6
+ class UnknownCommandInteraction < BaseInteraction
7
+ interaction_options can_interact: :all, channels: :all
8
+
9
+ def execute
10
+ response = "Hmmm. I don't know that command. If you think I should double check your slack configuration and your interaction definitions"
11
+ send_message(response)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ class Message
5
+ class Compilation
6
+ attr_reader :payload, :csv_message, :csv_data
7
+ def initialize(payload, csv_message = false, csv_data = {})
8
+ @payload = payload
9
+ @csv_message = csv_message
10
+ @csv_data = csv_data
11
+ end
12
+
13
+ def blocks
14
+ payload[:blocks]&.to_json
15
+ end
16
+
17
+ def channel
18
+ if payload[:channel].is_a?(Array)
19
+ payload[:channel].first
20
+ elsif payload[:channel].is_a?(String)
21
+ payload[:channel]
22
+ end
23
+ end
24
+
25
+ alias csv? csv_message
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'slack-ruby-client'
5
+ require_relative './message/compilation'
6
+ require_relative 'message_stop_guard'
7
+
8
+ module SlackInteractiveClient
9
+ class Message
10
+ class << self
11
+ def define(&block)
12
+ @templates ||= {}
13
+ instance_exec(&block)
14
+ end
15
+
16
+ def compile(template_name, args = {})
17
+ template = @templates.fetch(template_name) do
18
+ raise ArgumentError, "Template #{template_name} not defined"
19
+ end
20
+ template.reset_message_blocks
21
+ template.compile(args)
22
+ end
23
+
24
+ def template(name, &block)
25
+ instance = new
26
+ instance.instance_eval(&block)
27
+ @templates[name] = instance
28
+ end
29
+ end
30
+
31
+ attr_reader :name, :blocks, :environment, :channels, :tagged_users
32
+
33
+ def initialize
34
+ @blocks = []
35
+ @environment = nil
36
+ @channels = []
37
+ @tagged_users = []
38
+ @csv_data_block = []
39
+ @list_items = []
40
+ end
41
+
42
+ def template(name, &block)
43
+ @name = name
44
+ instance_eval(&block) if block_given?
45
+ end
46
+
47
+ def text(text = nil , &block)
48
+ @text_block = text.nil? ? block : Proc.new {|x| text }
49
+ end
50
+
51
+ def title(title = nil , &block)
52
+ @title_block = title.nil? ? block : Proc.new {|x| title }
53
+ end
54
+
55
+ def markdown_section(markdown = nil, &block)
56
+ @markdown_block = markdown.nil? ? block : Proc.new {|x| markdown }
57
+ end
58
+
59
+ def list_section(&block)
60
+ @list_block = block
61
+ end
62
+
63
+ def list_item(item)
64
+ @list_items << item
65
+ end
66
+
67
+ def environment(env)
68
+ @environment = env
69
+ end
70
+
71
+ def channel(channel)
72
+ @channels << channel
73
+ end
74
+
75
+ def mention(*users)
76
+ # Format the user mention strings
77
+ @tagged_users = users.flatten
78
+ @user_mentions = users.flatten.join(' ')
79
+ end
80
+
81
+ def csv_data(bool = false)
82
+ @include_csv_data = bool
83
+ end
84
+
85
+ def compile(args = {})
86
+ construct_environment_block if @environment
87
+ construct_title_block(args) if @title_block
88
+ construct_text_block(args) if @text_block
89
+ construct_markdown_block(args) if @markdown_block
90
+ construct_list_block(args) if @list_block
91
+ construct_csv(args) if @include_csv_data
92
+ add_user_mentions if @tagged_users.any?
93
+
94
+ message_payload = {
95
+ blocks: @blocks,
96
+ channel: args[:channel] || @channels,
97
+ }
98
+
99
+ ::SlackInteractiveClient::Message::Compilation.new(
100
+ message_payload,
101
+ @include_csv_data,
102
+ @csv_data
103
+ )
104
+ end
105
+
106
+ def reset_message_blocks
107
+ @blocks = []
108
+ @list_items = []
109
+ end
110
+
111
+ private
112
+
113
+ def construct_environment_block
114
+ env = @environment.upcase
115
+ scrubbed = ::SlackInteractiveClient::MessageStopGuard.scrubber.call(env)
116
+ add_block(type: 'section', text: { type: 'mrkdwn', text: "Environment: [#{scrubbed}] \n" })
117
+ end
118
+
119
+ def construct_title_block(args)
120
+ text_content = @title_block.call(args)
121
+ scrubbed = ::SlackInteractiveClient::MessageStopGuard.scrubber.call(text_content)
122
+ add_block(type: 'header', text: { type: 'plain_text', text: scrubbed })
123
+ end
124
+
125
+ def construct_text_block(args)
126
+ text_content = @text_block.call(args)
127
+ scrubbed = ::SlackInteractiveClient::MessageStopGuard.scrubber.call(text_content)
128
+ add_block(type: 'rich_text', elements: [ type: 'rich_text_section', elements: [ { type: 'text', text: scrubbed } ] ])
129
+ end
130
+
131
+ def construct_markdown_block(args)
132
+ markdown_content = @markdown_block.call(args)
133
+ scrubbed = MessageStopGuard.scrubber.call(markdown_content)
134
+ add_block(type: 'section', text: { type: 'mrkdwn', text: "```#{scrubbed}```" })
135
+ end
136
+
137
+ def construct_list_block(args)
138
+ @list_block.call(args)
139
+ list_elements_content = construct_list_items_block
140
+ add_block(type: 'rich_text', elements: [{ type: "rich_text_list", style: "bullet", elements: list_elements_content }])
141
+ end
142
+
143
+ def construct_list_items_block
144
+ @list_items.map do |list_item|
145
+ scrubbed = ::SlackInteractiveClient::MessageStopGuard.scrubber.call(list_item)
146
+ {
147
+ "type": "rich_text_section",
148
+ "elements": [
149
+ {
150
+ "type": "text",
151
+ "text": scrubbed
152
+ },
153
+ ]
154
+ }
155
+ end
156
+ end
157
+
158
+ def add_user_mentions
159
+ unless @tagged_users.empty?
160
+ # Format a string that includes all user mentions with Slack mention syntax
161
+ user_mentions = @tagged_users.map { |user| "<#{user}>" }.join(' ')
162
+
163
+ # Create a context block with all user mentions in one line
164
+ add_block({
165
+ type: 'context',
166
+ elements: [{ type: 'mrkdwn', text: "CC: #{user_mentions}" }]
167
+ })
168
+ end
169
+ end
170
+
171
+ def construct_csv(args)
172
+ return unless @include_csv_data
173
+
174
+ data = args.slice(:csv)
175
+ @csv_content ||= CSV.generate do |csv|
176
+ csv << args.dig(:csv, :headers)
177
+ args.dig(:csv, :rows).each { |row| csv << row }
178
+ end
179
+ data[:csv][:content] = @csv_content
180
+ @csv_data = data
181
+ end
182
+
183
+ def add_block(block)
184
+ @blocks << block
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ class MessageStopGuard
5
+ FILTERED_SUBSTITUTION = '[FILTERED]'
6
+
7
+ SENSITIVE_KEY_SUFFIXES = %w[
8
+ number numbers ssn email date_of_birth secret token salt identity identity_fingerprint
9
+ ].freeze
10
+
11
+ # Builds a list of regex patterns for sensitive values
12
+ def self.build_sensitive_value_patterns
13
+ SENSITIVE_KEY_SUFFIXES.map do |key_suffix|
14
+ /(?<key>#{key_suffix}[:\\'"=>\s]*)(?<value>[\w-]+)/i
15
+ end
16
+ end
17
+
18
+ # Returns a lambda that scrubs sensitive information from a message
19
+ def self.scrubber
20
+ pii_patterns = build_sensitive_value_patterns
21
+
22
+ lambda do |raw_msg|
23
+ pii_patterns.reduce(raw_msg) do |msg, pii_regex|
24
+ msg.gsub(pii_regex) { |m| $~[:key] + FILTERED_SUBSTITUTION }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ class Railtie < Rails::Railtie
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'httparty'
4
+
5
+ module SlackInteractiveClient
6
+ class ResponseClient
7
+ class << self
8
+ # { "response_type": "in_channel", "response_type": "ephemeral" }
9
+ # { "text": "text" }
10
+ # { "blocks": [ ] }
11
+ # { "replace_original": false" },
12
+ def respond(response_url, body)
13
+ ::HTTParty.post(response_url,
14
+ body: body.to_json,
15
+ headers: { 'Content-Type' => 'application/json' }
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackInteractiveClient
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/all'
5
+
6
+ require_relative "slack_interactive_client/configuration"
7
+ require_relative "slack_interactive_client/message_stop_guard"
8
+ require_relative "slack_interactive_client/client"
9
+ require_relative "slack_interactive_client/message/compilation"
10
+ require_relative "slack_interactive_client/message"
11
+ require_relative "slack_interactive_client/bot"
12
+ require_relative "slack_interactive_client/railtie"
13
+ require_relative "slack_interactive_client/response_client"
14
+ require_relative "slack_interactive_client/interactions/unknown_command_interaction"
15
+ require_relative "slack_interactive_client/base_interaction"
16
+ require_relative "slack_interactive_client/engine"
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SlackInteractiveClient::BaseInteraction do
4
+ class TestInteraction < ::SlackInteractiveClient::BaseInteraction
5
+ interaction_pattern "to $[name] the state $[state_name]"
6
+
7
+ def execute
8
+ return "hey #{name} welcome to #{state_name}"
9
+ end
10
+ end
11
+ let(:interaction_params) { { text: "to micah the state TEXAS"} }
12
+ let(:interaction_class) { TestInteraction.new(interaction_params) }
13
+
14
+ describe '#call' do
15
+ it 'stores the interactions' do
16
+ expect(interaction_class.call).to eq("hey micah welcome to TEXAS")
17
+ end
18
+ end
19
+ end
data/spec/bot_spec.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SlackInteractiveClient::Bot do
4
+ let(:message) { described_class.new }
5
+
6
+ class HelloWorldInteraction; end
7
+
8
+ describe '#define' do
9
+ before do
10
+ described_class.define do
11
+ interaction :hello_world, HelloWorldInteraction
12
+ end
13
+ end
14
+
15
+ it 'stores the interactions' do
16
+ expect(described_class.interactions.size).to eq(1)
17
+ expect(described_class.interactions[:hello_world]).to eq(HelloWorldInteraction)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe SlackInteractiveClient::Client do
4
+ after do
5
+ described_class.reset
6
+ end
7
+
8
+ describe '#configuration' do
9
+ it 'creates a new instance of Configuration' do
10
+ expect(described_class.configuration).to be_instance_of(SlackInteractiveClient::Configuration)
11
+ end
12
+ end
13
+
14
+ describe '#reset' do
15
+ it 'creates and assigns a new instance of Configuration' do
16
+ configuration = described_class.configuration
17
+ described_class.reset
18
+ expect(described_class.configuration).not_to eq(configuration)
19
+ end
20
+ end
21
+
22
+ describe '#configure' do
23
+ let(:configuration_params) do
24
+ { auth_token: "a-token", logger_instance: 'Rails logger' }
25
+ end
26
+
27
+ it 'creates a new instance of Configuration from block' do
28
+ described_class.configure do |config|
29
+ config.auth_token = configuration_params[:auth_token]
30
+ config.logger_instance = configuration_params[:logger_instance]
31
+ end
32
+
33
+ expect(described_class.configuration.auth_token).to eq(configuration_params[:auth_token])
34
+ expect(described_class.configuration.logger_instance).to eq(configuration_params[:logger_instance])
35
+ end
36
+ end
37
+
38
+ describe '#version' do
39
+ it 'has a version number' do
40
+ expect(SlackInteractiveClient::VERSION).not_to be nil
41
+ end
42
+ end
43
+
44
+ describe '#send_message' do
45
+ let(:template_name) { :test_template }
46
+ let(:args) { { some: 'argument' } }
47
+ let(:compilation) do
48
+ instance_double("SlackInteractiveClient::Message::Compilation",
49
+ channel: 'C1234567890',
50
+ blocks: [],
51
+ csv?: false)
52
+ end
53
+
54
+ before do
55
+ allow(SlackInteractiveClient::Message).to receive(:compile).and_return(compilation)
56
+ allow_any_instance_of(Slack::Web::Client).to receive(:chat_postMessage)
57
+ allow_any_instance_of(Slack::Web::Client).to receive(:files_upload)
58
+ end
59
+
60
+ it 'compiles the message and sends it to Slack' do
61
+ expect(SlackInteractiveClient::Message).to receive(:compile).with(template_name, args)
62
+ expect_any_instance_of(Slack::Web::Client).to receive(:chat_postMessage).with(hash_including(:channel, :blocks, :as_user))
63
+ described_class.send_message(template_name, args)
64
+ end
65
+
66
+ context 'when the compilation includes csv data' do
67
+ let(:csv_data) do
68
+ {
69
+ csv: {
70
+ content: 'id,name\n1,Test',
71
+ title: 'Test CSV',
72
+ filename: 'test.csv',
73
+ comment: 'Here is your CSV!'
74
+ }
75
+ }
76
+ end
77
+
78
+ let(:compilation_with_csv) do
79
+ instance_double("SlackInteractiveClient::Message::Compilation",
80
+ channel: 'C1234567890',
81
+ blocks: [],
82
+ csv?: true,
83
+ csv_data: csv_data)
84
+ end
85
+
86
+ it 'uploads a CSV file along with the message' do
87
+ allow(SlackInteractiveClient::Message).to receive(:compile).and_return(compilation_with_csv)
88
+ expect_any_instance_of(Slack::Web::Client).to receive(:files_upload).with(hash_including(:channels, :content, :title, :filename, :initial_comment, :as_user))
89
+ described_class.send_message(template_name, args)
90
+ end
91
+ end
92
+
93
+ context 'when Slack::Web::Api::Errors::SlackError is raised' do
94
+ it 'rescues from Slack::Web::Api::Errors::SlackError' do
95
+ allow(SlackInteractiveClient::Message).to receive(:compile).and_raise(Slack::Web::Api::Errors::SlackError.new('Error'))
96
+ expect { described_class.send_message(template_name, args) }.to_not raise_error
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SlackInteractiveClient::Message::Compilation do
6
+ let(:payload) { { blocks: [{ type: 'section', text: { type: 'mrkdwn', text: 'Test message' } }], channel: ['#general'] } }
7
+ let(:csv_message) { true }
8
+ let(:csv_data) { { header: ['Column1', 'Column2'], rows: [['Data1', 'Data2'], ['Data3', 'Data4']] } }
9
+
10
+ subject(:compilation) { described_class.new(payload, csv_message, csv_data) }
11
+
12
+ describe '#initialize' do
13
+ it 'initializes with payload, csv_message, and csv_data' do
14
+ expect(compilation.payload).to eq(payload)
15
+ expect(compilation.csv_message).to be(csv_message)
16
+ expect(compilation.csv_data).to eq(csv_data)
17
+ end
18
+ end
19
+
20
+ describe '#blocks' do
21
+ context 'when blocks are present in payload' do
22
+ it 'returns a JSON representation of blocks' do
23
+ expect(compilation.blocks).to eq(payload[:blocks].to_json)
24
+ end
25
+ end
26
+
27
+ context 'when blocks are absent in payload' do
28
+ let(:payload) { {} }
29
+
30
+ it 'returns nil' do
31
+ expect(compilation.blocks).to be_nil
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#channel' do
37
+ it 'returns the first channel from the payload' do
38
+ expect(compilation.channel).to eq(payload[:channel].first)
39
+ end
40
+ end
41
+
42
+ describe '#csv?' do
43
+ context 'when csv_message is true' do
44
+ it 'returns true' do
45
+ expect(compilation.csv?).to be_truthy
46
+ end
47
+ end
48
+
49
+ context 'when csv_message is false' do
50
+ let(:csv_message) { false }
51
+
52
+ it 'returns false' do
53
+ expect(compilation.csv?).to be_falsey
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,173 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SlackInteractiveClient::Message do
4
+ let(:message) { described_class.new }
5
+
6
+ describe '#define' do
7
+ it 'defines a new template with the given block' do
8
+ block_called = false
9
+ described_class.define { block_called = true }
10
+ expect(block_called).to be(true)
11
+ end
12
+ end
13
+
14
+ describe '#compile' do
15
+ before do
16
+ described_class.define do
17
+ template :test_template do
18
+ text 'Test message'
19
+ end
20
+ end
21
+ end
22
+
23
+ it 'compiles the given template with arguments' do
24
+ expect { described_class.compile(:test_template) }.not_to raise_error
25
+ end
26
+
27
+ it 'raises an error for an undefined template' do
28
+ expect { described_class.compile(:undefined_template) }.to raise_error(ArgumentError)
29
+ end
30
+ end
31
+
32
+ describe '#template' do
33
+ it 'creates a new template with the given name and block' do
34
+ block_called = false
35
+ described_class.template(:new_template) { block_called = true }
36
+ expect(block_called).to be(true)
37
+ end
38
+ end
39
+
40
+ describe '#template' do
41
+ it 'assigns a name and evaluates the block' do
42
+ message.template(:test) { text 'Testing' }
43
+ expect(message.name).to eq(:test)
44
+ end
45
+ end
46
+
47
+ describe '#text' do
48
+ it 'sets a text block' do
49
+ message.text 'Hello world'
50
+ compiled_message = message.compile
51
+
52
+ expect(compiled_message.blocks).to include('Hello world')
53
+ end
54
+ end
55
+
56
+ describe '#title' do
57
+ it 'sets a title block' do
58
+ message.title 'Title'
59
+ compiled_message = message.compile
60
+
61
+ expect(compiled_message.blocks).to include('*Title*')
62
+ end
63
+ end
64
+
65
+ describe '#markdown_section' do
66
+ it 'sets a markdown section block' do
67
+ message.markdown_section 'Some *markdown* content'
68
+ compiled_message = message.compile
69
+
70
+ expect(compiled_message.blocks).to include('Some *markdown* content')
71
+ end
72
+ end
73
+
74
+ describe '#environment' do
75
+ it 'sets the environment' do
76
+ message.environment('development')
77
+
78
+ expect(message.instance_variable_get(:@environment)).to eq('development')
79
+ end
80
+ end
81
+
82
+ describe '#channel' do
83
+ it 'adds channels to the message' do
84
+ message.channel('#general')
85
+ message.channel('#random')
86
+
87
+ expect(message.channels).to contain_exactly('#general', '#random')
88
+ end
89
+ end
90
+
91
+ describe '#mention' do
92
+ it 'formats user mentions' do
93
+ message.mention('user1', 'user2')
94
+ compiled_message = message.compile
95
+
96
+ expect(compiled_message.payload[:blocks][0][:elements][0][:text]).to include('<user1> <user2>')
97
+ end
98
+ end
99
+
100
+ describe '#csv_data' do
101
+ it 'includes csv data when set to true' do
102
+ message.csv_data(true)
103
+ compiled_message = message.compile(csv: { headers: ['one', 'two'], rows: [['1', '2'], ['3', '4']] })
104
+
105
+ expect(compiled_message.csv_data).to include(:csv)
106
+ end
107
+ end
108
+
109
+ describe '#compile' do
110
+ it 'compiles the message with blocks, channels, and tagged users' do
111
+ message.text 'Hello world'
112
+ message.channel '#general'
113
+ message.mention 'user1'
114
+
115
+ compiled_message = message.compile
116
+
117
+ expect(compiled_message).to be_a(SlackInteractiveClient::Message::Compilation)
118
+ expect(compiled_message.channel).to eq('#general')
119
+ end
120
+ end
121
+
122
+ describe '#reset_message_blocks' do
123
+ it 'clears all message blocks' do
124
+ message.text 'Hello world'
125
+ message.compile
126
+
127
+ expect(message.blocks).not_to be_empty
128
+
129
+ message.reset_message_blocks
130
+
131
+ expect(message.blocks).to be_empty
132
+ end
133
+ end
134
+
135
+ describe '#compile' do
136
+ subject(:message) { described_class.new }
137
+
138
+ let(:args) do
139
+ {
140
+ text: 'Hello, World!',
141
+ title: 'Test Message',
142
+ markdown: 'Some *markdown* content',
143
+ channel: '#general',
144
+ users: ['@user1', '@user2'],
145
+ csv: {
146
+ headers: ['header1', 'header2'],
147
+ rows: [['row1data1', 'row1data2'], ['row2data1', 'row2data2']],
148
+ }
149
+ }
150
+ end
151
+
152
+ before do
153
+ message.environment('test')
154
+ message.channel(args[:channel])
155
+ message.mention(args[:users])
156
+ message.text(args[:text])
157
+ message.title(args[:title])
158
+ message.markdown_section(args[:markdown])
159
+ message.csv_data(true)
160
+ end
161
+
162
+ it 'compiles the message payload with given arguments' do
163
+ compilation = message.compile(args)
164
+
165
+ expect(compilation).to be_an_instance_of(SlackInteractiveClient::Message::Compilation)
166
+ expect(compilation.blocks).to include('Hello, World!')
167
+ expect(compilation.blocks).to include('Test Message')
168
+ expect(compilation.blocks).to include('Some *markdown* content')
169
+ expect(compilation.csv_data).to match(hash_including(:csv))
170
+ expect(compilation.channel).to eq(args[:channel])
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SlackInteractiveClient::MessageStopGuard do
4
+ let(:interaction_params) { { text: "to micah the state TEXAS"} }
5
+ let(:interaction_class) { TestInteraction.new(interaction_params) }
6
+
7
+ describe '#scrubber' do
8
+ it 'filters out the value' do
9
+ scrubbed = described_class.scrubber.call(%(ssn: 1234))
10
+
11
+ expect(scrubbed).to include("[FILTERED]")
12
+ expect(scrubbed).not_to include("1234")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ require "bundler/setup"
2
+ require "byebug"
3
+ require 'rails'
4
+
5
+ require "slack_interactive_client"
6
+
7
+ ENV['RAILS_ENV'] ||= 'test'
8
+
9
+ RSpec.configure do |config|
10
+ config.example_status_persistence_file_path = ".rspec_status"
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,137 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: slack_interactive_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Micah Bowie
8
+ - Code Cowboy
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2023-12-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: slack-ruby-client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: rails
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: httparty
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: rspec-rails
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ description: Interact with your Rails app through Slack by defining dynamic Slack
85
+ slash commands, using a reusable message DSL, slack client, and more.
86
+ email:
87
+ - micahbowie20@gmail.com
88
+ executables: []
89
+ extensions: []
90
+ extra_rdoc_files: []
91
+ files:
92
+ - lib/slack_interactive_client.rb
93
+ - lib/slack_interactive_client/base_interaction.rb
94
+ - lib/slack_interactive_client/bot.rb
95
+ - lib/slack_interactive_client/client.rb
96
+ - lib/slack_interactive_client/concerns/slack_interaction_params.rb
97
+ - lib/slack_interactive_client/concerns/slack_pattern_interaction.rb
98
+ - lib/slack_interactive_client/configuration.rb
99
+ - lib/slack_interactive_client/engine.rb
100
+ - lib/slack_interactive_client/interactions/unknown_command_interaction.rb
101
+ - lib/slack_interactive_client/message.rb
102
+ - lib/slack_interactive_client/message/compilation.rb
103
+ - lib/slack_interactive_client/message_stop_guard.rb
104
+ - lib/slack_interactive_client/railtie.rb
105
+ - lib/slack_interactive_client/response_client.rb
106
+ - lib/slack_interactive_client/version.rb
107
+ - spec/base_interaction_spec.rb
108
+ - spec/bot_spec.rb
109
+ - spec/client_spec.rb
110
+ - spec/message/compilation_spec.rb
111
+ - spec/message_spec.rb
112
+ - spec/message_stop_guard_spec.rb
113
+ - spec/spec_helper.rb
114
+ homepage:
115
+ licenses: []
116
+ metadata: {}
117
+ post_install_message:
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 2.7.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 3.0.3.1
133
+ signing_key:
134
+ specification_version: 4
135
+ summary: Interact with your Rails app through Slack by defining dynamic Slack slash
136
+ commands, using a reusable message DSL, slack client, and more.
137
+ test_files: []