slack_bot-events 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e221d5ba2b7822f18b4079b73bc35005488510570b1c6bf1121b29ae3a38a85f
4
- data.tar.gz: 7c6fd667337ad3eaefc080abac2d53295b6226c41570dcebc118eb321e218a0b
3
+ metadata.gz: 653594899c76b05d93054ceb20520d84a487a38451ecee7d561ab5b9e6590bf7
4
+ data.tar.gz: 3f722375d06dce4ca2a3f4151ed221948b9e1d5e6d87842cf966ff067c54c31b
5
5
  SHA512:
6
- metadata.gz: 9545811f097ea137483b27bfd4b93cfb9d64040487051fd9f906de4d67e09503768fa4341faad9eed36348edf644ad33be7f9109f3c5a9d5741d59fc9c8ca17b
7
- data.tar.gz: 2afc096dd076de82f0f3443a7ac437be405f6b8613db994072a94cb05623797cc631b178e163c756ddfbd7f2c29e337bbb42db5fe71e42aed0b5b148c3bfd5ea
6
+ metadata.gz: eb0fb66fded40a580f042bd45e9ba139cf3753d2960461265d07394a5c182a35c9f35d33105e7ca5536b6e279c44335baf6b8ba645e531186ffbf3c4d9790dcc
7
+ data.tar.gz: 02f478db61cabfce5f8e66b7303f47f9ed548d08dafded39d364e579f68ebf448f17ddf851e9717bfea0e755ab41e0821d450ab4b4e3353823a3b161e9ad0dea
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slack_bot-events (0.1.0)
4
+ slack_bot-events (0.4.0)
5
5
  class_composer (~> 1.0)
6
6
  faraday
7
7
  faye-websocket
@@ -3,7 +3,7 @@
3
3
  require "eventmachine"
4
4
  require "faraday"
5
5
  require "faye/websocket"
6
- require "pry"
6
+ require "slack_bot/events/schematize"
7
7
 
8
8
  module SlackBot
9
9
  module Events
@@ -11,80 +11,62 @@ module SlackBot
11
11
  BASE_API = "https://slack.com/api"
12
12
 
13
13
  def start!
14
- EM.run do
15
- websocket.on :open do |event|
16
- event_tracer(:open)
14
+ EventMachine.run do
15
+ websocket.on :open do |socket_event|
16
+ process(type: :open, socket_event: socket_event) { |**| }
17
17
  end
18
18
 
19
- websocket.on :message do |event|
20
- event_tracer(:message) do
21
- parsed_data = JSON.parse(event.data)
19
+ websocket.on :message do |socket_event|
20
+ process_message(socket_event: socket_event) do |listener:, parsed_data:, schema: nil|
22
21
  case parsed_data["type"]
23
22
  when "events_api"
24
- events_api(parsed_data)
25
- when "app_rate_limited"
26
- # https://api.slack.com/apis/rate-limits#events
27
- # Total allowed workspace events are 30,000 per hour
28
- # This message type is received once you have gone beyond that
29
- params = {
30
- minute_rate_limited: parsed_data["minute_rate_limited"],
31
- team_id: parsed_data["team_id"],
32
- api_app_id: parsed_data["api_app_id"],
33
- }
34
- event_tracer("message:app_rate_limited", **params)
35
- when "hello"
36
- params = {
37
- num_connections: parsed_data["num_connections"],
38
- debug_info_host: parsed_data["debug_info"]["host"],
39
- debug_info_connect_time: parsed_data["debug_info"]["approximate_connection_time"],
40
- }
41
- event_tracer("message:hello", **params)
23
+ events_api(handler: listener[:handler], schema: schema, parsed_data: parsed_data)
42
24
  end
43
25
  end
44
26
  end
45
27
 
46
- websocket.on :close do |event|
47
- event_tracer(:close, code: event.code, reason: event.reason)
28
+ websocket.on :close do |socket_event|
29
+ process(type: :close, socket_event: socket_event) { |**| }
48
30
  @websocket = nil
31
+
32
+ # The websocket is closed, explcitly stop the event machine to to end the loop and return to the parent
33
+ EventMachine.stop
49
34
  end
50
35
  end
51
36
  end
52
37
 
53
- def events_api(parsed_data)
54
- schematized = SlackBot::Events::Schemas::SocketPayload.new(parsed_data)
55
- Events.logger.info(schematized.tldr) if Events.config.print_tldr
56
- object = Events.config.listeners[schematized.type.to_sym]
57
- if object
58
- safe_handler(type: schematized.type.to_sym, object: object, schema: schematized, parsed_data: parsed_data)
38
+ def process_message(socket_event:)
39
+ schema_data = Schematize.call(data: socket_event.data)
40
+
41
+ listener = find_listener(schema: schema_data[:schema]) # Events.config.listeners[schema_data[:schema]&.type.to_sym]
42
+
43
+ SlackBot::Events.message_middleware.invoke_message(websocket: websocket, listener: listener, type: :message, socket_event: socket_event, **schema_data) do
44
+ yield(listener: listener, **schema_data) if block_given?
59
45
  end
60
- websocket.send("#{{ envelope_id: schematized.envelope_id }.to_json}")
61
46
  end
62
47
 
63
- def event_tracer(type, **params)
64
- stringify = params.map { |k,v| "#{k}:#{v}" }.join("; ")
65
- Events.logger.info "[Event Received] #{type} #{stringify}"
66
- if block_given?
67
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
68
- yield
69
- elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
70
- Events.logger.info "[Event completed] [#{elapsed_time.round(3)}s] #{type} #{stringify}"
48
+ def process(type:, socket_event:)
49
+ SlackBot::Events.public_send(:"#{type}_middleware").invoke(type: type, socket_event: socket_event) do
50
+ yield if block_given?
71
51
  end
72
52
  end
73
53
 
54
+ def events_api(handler:, schema:, parsed_data:)
55
+ return if listener.nil?
56
+
57
+ Events.logger.info { schema.tldr } if Events.config.print_tldr
58
+
59
+ # This gets rescued in the MessageHandler middleware
60
+ # on_success and on_failure happens there as well
61
+ handler.call(schema: schema, raw_data: parsed_data)
62
+ end
63
+
74
64
  private
75
65
 
76
- def safe_handler(type:, object:, schema:, parsed_data:)
77
- Events.logger.info "Received Handler for #{type}"
78
- object[:handler].call(schema: schema, raw_data: parsed_data)
79
- object[:on_success]&.call(schema)
80
- rescue => error
81
- Events.logger.error("#{object[:handler]} returned #{error.class} => #{error.message}. Safely returning to websocket. #{error.backtrace[0...10]}")
82
-
83
- begin
84
- object[:on_failure]&.call(schema, error)
85
- rescue => on_failure_error
86
- Events.logger.error("Failure occured during on_failure block. #{on_failure_error}")
87
- end
66
+ def find_listener(schema:)
67
+ return nil if schema.nil?
68
+
69
+ Events.config.listeners[schema.type.to_sym]
88
70
  end
89
71
 
90
72
  def websocket
@@ -103,9 +85,7 @@ module SlackBot
103
85
  end
104
86
 
105
87
  def faraday_headers
106
- {
107
- 'Authorization' => "Bearer #{SlackBot::Events.config.client_socket_token}"
108
- }
88
+ { 'Authorization' => "Bearer #{SlackBot::Events.config.client_socket_token}" }
109
89
  end
110
90
  end
111
91
  end
@@ -2,17 +2,35 @@
2
2
 
3
3
  require "class_composer"
4
4
  require "logger"
5
+ require "slack_bot/events/middleware/chain"
5
6
 
6
7
  module SlackBot
7
8
  module Events
8
9
  class Configuration
9
10
  include ClassComposer::Generator
11
+
12
+ ALLOWED_ACKNOWLEDGE = [
13
+ DEFAULT_ACKNOWLEDGE = :on_complete,
14
+ :on_success,
15
+ :on_receive,
16
+ ]
17
+
10
18
  add_composer :client_id, allowed: String, default: ENV["SLACK_CLIENT_ID"]
11
19
  add_composer :client_secret, allowed: String, default: ENV["SLACK_CLIENT_SECRET"]
12
20
  add_composer :client_signing_secret, allowed: String, default: ENV["SLACK_SIGNING_SECRET"]
13
21
  add_composer :client_socket_token, allowed: String, default: ENV["SLACK_SOCKET_TOKEN"]
14
22
  add_composer :client_verification_token, allowed: String, default: ENV["SLACK_VERIFICATION_TOKEN"]
15
23
  add_composer :print_tldr, allowed: [true.class, false.class], default: true
24
+ add_composer :message_middleware, allowed: Middleware::Chain, default: Middleware::Chain.new(type: :message)
25
+ add_composer :open_middleware, allowed: Middleware::Chain, default: Middleware::Chain.new(type: :open)
26
+ add_composer :close_middleware, allowed: Middleware::Chain, default: Middleware::Chain.new(type: :close)
27
+ add_composer :envelope_acknowledge, allowed: Symbol, default: DEFAULT_ACKNOWLEDGE, validator: ->(val) { ALLOWED_ACKNOWLEDGE.include?(val) }, invalid_message: ->(_) { "Must by a Symbol in #{ALLOWED_ACKNOWLEDGE}" }
28
+
29
+ ALLOWED_ACKNOWLEDGE.each do |ack|
30
+ define_method :"acknowledge_#{ack}?" do
31
+ envelope_acknowledge == ack
32
+ end
33
+ end
16
34
 
17
35
  def register_listener(name:, handler:, on_success: nil, on_failure: nil)
18
36
  if event_handler = listeners[name.to_sym]
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "slack_bot/events/middleware/event_tracer"
4
+ require "slack_bot/events/middleware/message_handler"
5
+
6
+ ####################
7
+ #
8
+ # Adapted from Sidekiq:
9
+ # https://github.com/sidekiq/sidekiq/blob/main/lib/sidekiq/middleware/chain.rb
10
+ #
11
+ ####################
12
+
13
+ module SlackBot
14
+ module Events
15
+ module Middleware
16
+ class Chain
17
+ include Enumerable
18
+
19
+ attr_reader :type
20
+
21
+ DEFAULT_ENTRIES = {
22
+ message: [Middleware::EventTracer, Middleware::MessageHandler],
23
+ open: [Middleware::EventTracer],
24
+ close: [Middleware::EventTracer],
25
+ }
26
+
27
+ def initialize(type:)
28
+ @type = type
29
+ end
30
+
31
+ def self.default_entry(type)
32
+ DEFAULT_ENTRIES[type].map { Entry.new(_1) }
33
+ end
34
+
35
+ def entries
36
+ @entries ||= self.class.default_entry(type)
37
+ end
38
+
39
+ def remove(klass)
40
+ raise ArgumentError, "Unable to remove default Middleware #{klass}" if self.class.default_entry.map(:klass).include?(klass)
41
+
42
+ entries.delete_if { |entry| entry.klass == klass }
43
+ end
44
+
45
+ def add(klass, *args)
46
+ remove(klass)
47
+ entries << Entry.new(klass, args)
48
+ end
49
+
50
+ def prepend(klass, *args)
51
+ remove(klass)
52
+ entries.insert(0, Entry.new(klass, args))
53
+ end
54
+
55
+ def insert_before(oldklass, newklass, *args)
56
+ i = entries.index { |entry| entry.klass == newklass }
57
+ new_entry = i.nil? ? Entry.new(klass, args) : entries.delete_at(i)
58
+ i = entries.index { |entry| entry.klass == oldklass } || 0
59
+ entries.insert(i, new_entry)
60
+ end
61
+
62
+ def insert_after(oldklass, newklass, *args)
63
+ i = entries.index { |entry| entry.klass == newklass }
64
+ new_entry = i.nil? ? Entry.new(klass, args) : entries.delete_at(i)
65
+ i = entries.index { |entry| entry.klass == oldklass } || entries.count - 1
66
+ entries.insert(i + 1, new_entry)
67
+ end
68
+
69
+ def exists?(klass)
70
+ any? { |entry| entry.klass == klass }
71
+ end
72
+
73
+ def empty?
74
+ entries.nil? || entries.empty?
75
+ end
76
+
77
+ def retrieve
78
+ map(&:instance!)
79
+ end
80
+
81
+ def each(&block)
82
+ entries.each(&block)
83
+ end
84
+
85
+ def clear
86
+ @entries = nil
87
+ end
88
+
89
+ def invoke_message(type:, socket_event:, parsed_data:, websocket:, listener:, schema: nil)
90
+ return yield if empty?
91
+
92
+ chain = retrieve
93
+ traverse_chain = proc do
94
+ if chain.empty?
95
+ yield
96
+ else
97
+ params = {
98
+ parsed_data: parsed_data,
99
+ schema: schema,
100
+ socket_event: socket_event,
101
+ listener: listener,
102
+ type: type,
103
+ websocket: websocket,
104
+ }
105
+ chain.shift.call(**params, &traverse_chain)
106
+ end
107
+ end
108
+ traverse_chain.call
109
+ end
110
+
111
+ def invoke(type:, socket_event:)
112
+ return yield if empty?
113
+
114
+ chain = retrieve
115
+ traverse_chain = proc do
116
+ if chain.empty?
117
+ yield
118
+ else
119
+ chain.shift.call(type: type, socket_event: socket_event, &traverse_chain)
120
+ end
121
+ end
122
+ traverse_chain.call
123
+ end
124
+
125
+ def inspect_me
126
+ entries.map do |e|
127
+ [
128
+ e.klass,
129
+ e.args
130
+ ]
131
+ end
132
+ end
133
+ end
134
+
135
+ class Entry
136
+ attr_reader :klass
137
+ attr_reader :args
138
+
139
+ def initialize(klass, args = [])
140
+ @klass = klass
141
+ @args = args
142
+ end
143
+
144
+ def instance!
145
+ klass.new(*args)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackBot
4
+ module Events
5
+ module Middleware
6
+ class EventTracer
7
+ def call(type:, socket_event:, schema: nil, parsed_data: nil, **params)
8
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
9
+ temp_type = type.dup.to_s
10
+ case type
11
+ when :close
12
+ additional_info = "code: #{socket_event.code} reason:#{socket_event.reason}"
13
+ when :message
14
+ accurate_type = parsed_data.dig("payload","event","subtype") || parsed_data.dig("payload","event","type")
15
+ p_type = [
16
+ parsed_data.dig("type"),
17
+ accurate_type
18
+ ].compact.join(":")
19
+ case p_type
20
+ when "app_rate_limited"
21
+ # https://api.slack.com/apis/rate-limits#events
22
+ # Total allowed workspace events are 30,000 per hour
23
+ # This message type is received once you have gone beyond that
24
+ temp_type += ":#{p_type}"
25
+ additional_info = "minute_rate_limited:#{parsed_data["minute_rate_limited"]} " \
26
+ "team_id:#{parsed_data["team_id"]} " \
27
+ "api_app_id:#{parsed_data["api_app_id"]}"
28
+ else
29
+ # Expected other types are `events_api` and `hello`
30
+ temp_type += ":#{p_type}"
31
+ end
32
+ end
33
+
34
+ Events.logger.info { "[Event Received] #{temp_type} #{additional_info}" }
35
+
36
+ yield
37
+
38
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
39
+ Events.logger.info { "[Event Finished] #{temp_type} [#{(elapsed_time * 1000).round(2)}ms]" }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackBot
4
+ module Events
5
+ module Middleware
6
+ class MessageHandler
7
+ def call(schema:, websocket:, listener:, **params)
8
+ if SlackBot::Events.config.acknowledge_on_receive?
9
+ acknowledge!(websocket: websocket, schema: schema)
10
+ end
11
+
12
+ yield
13
+
14
+ listener[:on_success]&.call(schema) if listener
15
+
16
+ if SlackBot::Events.config.acknowledge_on_success? || SlackBot::Events.config.acknowledge_on_complete?
17
+ acknowledge!(websocket: websocket, schema: schema)
18
+ end
19
+ rescue StandardError => error
20
+ puts error.message
21
+ puts error.backtrace
22
+ Events.logger.error do
23
+ "#{listener[:handler]} returned #{error.class} => #{error.message}. #{error.backtrace[0...10]}"
24
+ end
25
+
26
+ begin
27
+ listener[:on_failure]&.call(schema, error) if listener
28
+ rescue StandardError => on_failure_error
29
+ Events.logger.error("Failure occured during on_failure block. #{on_failure_error}")
30
+ end
31
+
32
+ if SlackBot::Events.config.acknowledge_on_complete?
33
+ acknowledge!(websocket: websocket, schema: schema)
34
+ elsif SlackBot::Events.config.acknowledge_on_success?
35
+ Events.logger.debug do
36
+ "Envelope acknowledgment skipped. Ackowledgment on success only. Slack may send a duplicate message"
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def acknowledge!(websocket:, schema:)
44
+ return if schema.nil?
45
+
46
+ websocket.send("#{{ envelope_id: schema.envelope_id }.to_json}")
47
+ Events.logger.debug { "Envelope acknowledgment completed [#{SlackBot::Events.config.envelope_acknowledge}]" }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -28,7 +28,7 @@ module SlackBot
28
28
  def tldr
29
29
  retry_language = "#{retry_attempt}"
30
30
  retry_language += ":#{retry_reason}" if retry_attempt > 0
31
- "tldr:#{payload.event.tldr}; retry:#{retry_language}"
31
+ "#{payload.event.tldr}; retry:#{retry_language}"
32
32
  end
33
33
  end
34
34
  end
@@ -39,7 +39,7 @@ module SlackBot
39
39
  end
40
40
 
41
41
  def tldr
42
- "type: #{type}; channel:#{channel}; raw_text:#{text}"
42
+ "type:#{type}; user:#{user}; channel:#{channel}; ts_id:#{combined_id}"
43
43
  end
44
44
 
45
45
  def thread_ts
@@ -52,6 +52,10 @@ module SlackBot
52
52
 
53
53
  private
54
54
 
55
+ def combined_id
56
+ thread_ts ? "#{ts}:#{thread_ts}" : ts
57
+ end
58
+
55
59
  def return_nil?(val)
56
60
  JsonSchematize::EmptyValue === val || val.nil?
57
61
  end
@@ -28,7 +28,7 @@ module SlackBot
28
28
  end
29
29
 
30
30
  def tldr
31
- "type: #{type}; channel:#{channel}; reaction:#{reaction}"
31
+ "type:#{type}; user:#{item_user}; channel:#{channel}; reaction:#{reaction}"
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackBot
4
+ module Events
5
+ module Schematize
6
+ def self.message(parsed_data)
7
+ case parsed_data["type"]
8
+ when "events_api"
9
+ return SlackBot::Events::Schemas::SocketPayload
10
+ when "app_rate_limited"
11
+ when "hello"
12
+ end
13
+ end
14
+
15
+ def self.call(data:)
16
+ parsed_data = JSON.parse(data)
17
+ return { parsed_data: parsed_data } unless schema_klass = message(parsed_data)
18
+
19
+ if schema_klass.respond_to?(:call)
20
+ { schema: schema_klass.call(parsed_data).new(parsed_data), parsed_data: parsed_data }
21
+ else
22
+ { schema: schema_klass.new(parsed_data), parsed_data: parsed_data }
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SlackBot
4
4
  module Events
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "pry"
4
+
3
5
  require "slack_bot/events/configuration"
4
6
  require "slack_bot/events/client"
7
+ require "slack_bot/events/middleware/chain"
5
8
  require "slack_bot/events/schemas/socket_payload"
6
9
 
7
10
  module SlackBot
@@ -40,5 +43,17 @@ module SlackBot
40
43
  def self.logger
41
44
  config.logger
42
45
  end
46
+
47
+ def self.message_middleware
48
+ config.message_middleware
49
+ end
50
+
51
+ def self.open_middleware
52
+ config.open_middleware
53
+ end
54
+
55
+ def self.close_middleware
56
+ config.close_middleware
57
+ end
43
58
  end
44
59
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack_bot-events
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Taylor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-18 00:00:00.000000000 Z
11
+ date: 2024-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -134,6 +134,9 @@ files:
134
134
  - lib/slack_bot/events.rb
135
135
  - lib/slack_bot/events/client.rb
136
136
  - lib/slack_bot/events/configuration.rb
137
+ - lib/slack_bot/events/middleware/chain.rb
138
+ - lib/slack_bot/events/middleware/event_tracer.rb
139
+ - lib/slack_bot/events/middleware/message_handler.rb
137
140
  - lib/slack_bot/events/schemas/authorization.rb
138
141
  - lib/slack_bot/events/schemas/data_payload.rb
139
142
  - lib/slack_bot/events/schemas/socket_payload.rb
@@ -144,6 +147,7 @@ files:
144
147
  - lib/slack_bot/events/schemas/type/item.rb
145
148
  - lib/slack_bot/events/schemas/type/message.rb
146
149
  - lib/slack_bot/events/schemas/type/reaction_modified.rb
150
+ - lib/slack_bot/events/schematize.rb
147
151
  - lib/slack_bot/events/version.rb
148
152
  - slack_bot-events.gemspec
149
153
  homepage: https://github.com/matt-taylor/slack_bot-events