slack_bot-events 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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