slack_socket_mode_bot 0.9.1 → 0.9.2

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: 47ada769275576b47617b518f5edfe68f9e868b5542bf54a6aff1b8c61cf7f12
4
- data.tar.gz: d746a2d083f4b3aab6e249a564f83f36810c2c3db44f9e43531cb880a67ea905
3
+ metadata.gz: ecdae100a7f27d51751fe1fd77c772ca8a88cfab323266c874b996ffaed909c8
4
+ data.tar.gz: 92b4827386e87485f3adf26e93de16e910a2c4f9d320ba4cc683768d067b130f
5
5
  SHA512:
6
- metadata.gz: 2fccbca8913960ce106189139af617f904f02038cb15a4575e81a3a7dd02c8e95cd613d90527286f437a9d73cbc85aebdbea3096386580deaeb118ca72b666bf
7
- data.tar.gz: d756bea2c709da78cde7ec431cb0fe58cf05c9e06d25523e20fc99f9f01e71765a0c4adf90882eef1fc1056c3bc1fecce2567ec97a91337517b25c117aaaaae8
6
+ metadata.gz: 497006f3d3854a431e29f12f6463c57ed35290e5d722de7fe7bab24a886f667483821553da14ecc904d64b6de45c3d48ae446f90ba4424c3fbcbf7c59362ec38
7
+ data.tar.gz: a0e94a4812aaf022a7e62e02f0a56718ecb38cfd49002fc0dbdc7e9833fcb6520dfe799ff3d2279d9239520a615215240337261a4116c4c4ab1b7496d4239af5
data/README.md CHANGED
@@ -64,22 +64,18 @@ bot.run
64
64
  ```
65
65
 
66
66
  ```
67
- $ ruby example/echo_bot.rb
67
+ $ ruby examples/echo_bot.rb
68
68
  I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] websocket open
69
69
  I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] websocket open
70
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] slack hello
71
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] active connection count: 4
72
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] slack hello
73
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] active connection count: 4
74
70
  I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] websocket open
75
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] slack hello
76
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] active connection count: 4
77
71
  I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] websocket open
78
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack hello
79
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] active connection count: 4
80
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
81
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
82
- I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] slack events_api (event_callback)
72
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2560] hello (active connections: 4)
73
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2600] hello (active connections: 4)
74
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2640] hello (active connections: 4)
75
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] hello (active connections: 4)
76
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RABCDE
77
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RFGHIJ
78
+ I, [20XX-XX-XXTXX:XX:XX.XXXXXX #XXXXXX] INFO -- : [ws:2680] events_api app_mention Ev08H3RKLMNO
83
79
  ...
84
80
  ```
85
81
 
@@ -110,6 +106,11 @@ This method returns the response as a JSON data.
110
106
 
111
107
  Starts the main loop of communication with Slack. This method does not return.
112
108
 
109
+ On an unrecoverable error (for example, losing every connection and being unable
110
+ to reopen any), this method raises an exception instead of silently continuing in
111
+ a degraded state. Running your bot under a process supervisor such as systemd is
112
+ recommended so that it is restarted when this happens.
113
+
113
114
  ### `SlackSocketModeBot#step`
114
115
 
115
116
  Proceeds with the communication one step.
data/Rakefile CHANGED
@@ -1,4 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "lib" << "test"
8
+ t.test_files = FileList["test/test_*.rb"]
9
+ t.warning = false
10
+ end
11
+
12
+ task default: %i[test]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class SlackSocketModeBot
4
- VERSION = "0.9.1"
4
+ VERSION = "0.9.2"
5
5
  end
@@ -12,6 +12,21 @@ require_relative "slack_socket_mode_bot/simple_web_socket"
12
12
  class SlackSocketModeBot
13
13
  class Error < StandardError; end
14
14
 
15
+ API_BASE = "https://slack.com/api/"
16
+
17
+ # Internal signal: retry after `wait` seconds. Always caught in #call.
18
+ class Retry < StandardError
19
+ attr_reader :wait
20
+ def initialize(wait)
21
+ @wait = wait
22
+ super("retry after #{ wait }s")
23
+ end
24
+ end
25
+ private_constant :Retry
26
+
27
+ # Max seconds to sleep on a 429 Retry-After; #call may run in the event loop.
28
+ RETRY_AFTER_CAP = 60
29
+
15
30
  #: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
16
31
  def initialize(token:, app_token: nil, num_of_connections: 4, debug: false, logger: nil, &callback)
17
32
  @token = token
@@ -20,28 +35,64 @@ class SlackSocketModeBot
20
35
  @debug = debug
21
36
  @logger = logger
22
37
  @events = {}
23
- num_of_connections.times { add_connection(callback) } if app_token
38
+ @callback = callback
39
+ # No app token: Web API calls only, no Socket Mode connections.
40
+ @num_of_connections = app_token ? num_of_connections : 0
41
+ replenish_connections
24
42
  end
25
43
 
26
44
  #: (String method, untyped data, ?token: String) -> untyped
27
45
  def call(method, data, token: @token)
28
- count = 0
46
+ url = URI(API_BASE + method)
47
+ body = JSON.generate(data)
48
+ headers = {
49
+ "Content-type" => "application/json; charset=utf-8",
50
+ "Authorization" => "Bearer " + token,
51
+ }
52
+
53
+ retries = 0
29
54
  begin
30
- url = URI("https://slack.com/api/" + method)
31
- res = Net::HTTP.post(
32
- url, JSON.generate(data),
33
- "Content-type" => "application/json; charset=utf-8",
34
- "Authorization" => "Bearer " + token,
35
- )
36
- json = JSON.parse(res.body, symbolize_names: true)
37
- raise Error, json[:error] unless json[:ok]
38
- json
39
- rescue Socket::ResolutionError
55
+ res = Net::HTTP.post(url, body, headers)
56
+
57
+ case res
58
+ when Net::HTTPSuccess
59
+ json = JSON.parse(res.body, symbolize_names: true)
60
+ raise Error, json[:error] unless json[:ok]
61
+ json
62
+ when Net::HTTPTooManyRequests
63
+ # Rejected, not processed: safe to retry after Retry-After seconds.
64
+ raise Retry, Integer(res["retry-after"] || retries + 1)
65
+ else
66
+ # 5xx etc.: may already be processed, so don't retry; just don't crash.
67
+ raise Error, "HTTP #{ res.code } #{ res.message }"
68
+ end
69
+ rescue Socket::ResolutionError, Net::OpenTimeout
70
+ # Never sent (DNS / connect failure): safe to retry.
71
+ retries += 1
72
+ raise if retries >= 3
40
73
  sleep 1
41
- count += 1
42
- retry if count < 3
43
- raise
74
+ retry
75
+ rescue Retry => e
76
+ # Don't block the event loop on an absurdly long wait.
77
+ retries += 1
78
+ raise Error, "rate limited (retry-after: #{ e.wait }s)" if retries >= 3 || e.wait > RETRY_AFTER_CAP
79
+ sleep e.wait
80
+ retry
81
+ end
82
+ end
83
+
84
+ private def replenish_connections
85
+ # Reopen from the main loop, tolerating a single failure; #step retries.
86
+ while @conns.size < @num_of_connections
87
+ begin
88
+ add_connection(@callback)
89
+ rescue => e
90
+ @logger.warn("[reconnect] failed: #{ e.message }") if @logger
91
+ break
92
+ end
44
93
  end
94
+ # Fail loud rather than degrade silently once every connection is gone.
95
+ raise Error, "all socket connections lost" if @num_of_connections > 0 && @conns.empty?
45
96
  end
46
97
 
47
98
  private def add_connection(callback)
@@ -54,13 +105,14 @@ class SlackSocketModeBot
54
105
  when :open
55
106
  @logger.info("[ws:#{ ws.object_id }] websocket open") if @logger
56
107
  when :close
108
+ # #step drops the dead connection; #replenish_connections reopens it.
57
109
  @logger.info("[ws:#{ ws.object_id }] websocket closed") if @logger
58
- add_connection(callback)
59
110
  when :message
60
111
  begin
61
112
  json = JSON.parse(data, symbolize_names: true)
62
113
  rescue JSON::ParserError
63
- add_connection(callback)
114
+ # A stray non-JSON frame: skip it, don't open a spurious connection.
115
+ @logger.warn("[ws:#{ ws.object_id }] received a non-JSON message; ignored") if @logger
64
116
  next
65
117
  end
66
118
 
@@ -77,31 +129,34 @@ class SlackSocketModeBot
77
129
  else
78
130
  payload = json[:payload]
79
131
  if @logger
80
- msg = "[ws:#{ ws.object_id }] #{ json[:type] } [##{ json[:retry_attempt] + 1 }] (#{
81
- {
82
- event_id: payload[:event_id],
83
- event_time: Time.at(payload[:event_time]).strftime("%FT%T"),
84
- type: payload[:type],
85
- }.map {|k, v| "#{ k }: #{ v }" }.join(", ")
86
- })"
87
- @logger.info(msg)
132
+ # Log a per-type identifier; event_id/retry only apply to events_api.
133
+ detail =
134
+ case json[:type]
135
+ when "events_api" then [payload.dig(:event, :type), payload[:event_id]].compact.join(" ")
136
+ when "slash_commands" then payload[:command]
137
+ else payload[:type]
138
+ end
139
+ retry_n = json[:retry_attempt].to_i
140
+ line = "[ws:#{ ws.object_id }] #{ json[:type] }"
141
+ line += " #{ detail }" if detail && !detail.empty?
142
+ line += " (retry ##{ retry_n })" if retry_n > 0
143
+ @logger.info(line)
88
144
  end
145
+ # Only events_api has an event_id; dedup just those (others have none).
146
+ event_id = payload[:event_id]
89
147
  expired = Time.now.to_i - 600
90
148
  @events.reject! {|_, timestamp| timestamp < expired }
91
149
 
92
- if @events[json[:payload][:event_id]]
93
- # ignore
94
- else
95
- @events[json[:payload][:event_id]] = json[:payload][:event_time]
96
-
97
- response = { envelope_id: json[:envelope_id] }
98
- if json[:accepts_response_payload]
99
- response[:payload] = callback.call(json)
100
- else
101
- callback.call(json)
102
- end
103
- ws.send(JSON.generate(response))
150
+ # ACK every message; only skip the handler for a duplicate, else Slack resends.
151
+ duplicate = event_id && @events[event_id]
152
+ @events[event_id] = payload[:event_time] if event_id
153
+
154
+ response = { envelope_id: json[:envelope_id] }
155
+ unless duplicate
156
+ result = callback.call(json)
157
+ response[:payload] = result if json[:accepts_response_payload]
104
158
  end
159
+ ws.send(JSON.generate(response))
105
160
  end
106
161
  end
107
162
  end
@@ -113,6 +168,7 @@ class SlackSocketModeBot
113
168
  def step
114
169
  read_ios, write_ios = [], []
115
170
  @conns.select! {|ws| ws.step(read_ios, write_ios) }
171
+ replenish_connections
116
172
  return read_ios, write_ios
117
173
  end
118
174
 
@@ -1,13 +1,14 @@
1
- module Slack
2
- class SocketMode
3
- VERSION: String
4
-
5
- def initialize: (token: String, ?app_token: String, ?num_of_connections: Integer, ?debug: boolean, ?logger: Logger) { (untyped) -> untyped } -> void
6
-
7
- def call: (String method, untyped data, ?token: String) -> untyped
8
-
9
- def step: -> [Array[IO], Array[IO]]
10
-
11
- def run: -> bot
1
+ class SlackSocketModeBot
2
+ VERSION: String
3
+
4
+ class Error < StandardError
12
5
  end
6
+
7
+ def initialize: (token: String, ?app_token: String?, ?num_of_connections: Integer, ?debug: bool, ?logger: Logger?) { (untyped) -> untyped } -> void
8
+
9
+ def call: (String method, untyped data, ?token: String) -> untyped
10
+
11
+ def step: () -> [ Array[IO], Array[IO] ]
12
+
13
+ def run: () -> void
13
14
  end
metadata CHANGED
@@ -1,62 +1,66 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack_socket_mode_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.9.2
5
5
  platform: ruby
6
6
  authors:
7
- - Yusuke Endoh
7
+ - Yusuke Endoh
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2024-06-22 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: websocket
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '1.2'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '1.2'
12
+ - !ruby/object:Gem::Dependency
13
+ name: websocket
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ -
17
+ - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: "1.2"
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ -
25
+ - ~>
26
+ - !ruby/object:Gem::Version
27
+ version: "1.2"
26
28
  email:
27
- - mame@ruby-lang.org
29
+ - mame@ruby-lang.org
28
30
  executables: []
29
31
  extensions: []
30
32
  extra_rdoc_files: []
31
33
  files:
32
- - LICENSE
33
- - README.md
34
- - Rakefile
35
- - lib/slack_socket_mode_bot.rb
36
- - lib/slack_socket_mode_bot/simple_web_socket.rb
37
- - lib/slack_socket_mode_bot/version.rb
38
- - sig/slack_socket_mode.rbs
39
- homepage: https://github.com/mame/slack_socket_mode_bot
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/slack_socket_mode_bot.rb
38
+ - lib/slack_socket_mode_bot/simple_web_socket.rb
39
+ - lib/slack_socket_mode_bot/version.rb
40
+ - sig/slack_socket_mode.rbs
41
+ homepage: "https://github.com/mame/slack_socket_mode_bot"
40
42
  licenses:
41
- - MIT
43
+ - MIT
42
44
  metadata:
43
- homepage_uri: https://github.com/mame/slack_socket_mode_bot
44
- source_code_uri: https://github.com/mame/slack_socket_mode_bot
45
+ homepage_uri: "https://github.com/mame/slack_socket_mode_bot"
46
+ source_code_uri: "https://github.com/mame/slack_socket_mode_bot"
45
47
  rdoc_options: []
46
48
  require_paths:
47
- - lib
49
+ - lib
48
50
  required_ruby_version: !ruby/object:Gem::Requirement
49
51
  requirements:
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: 3.0.0
52
+ -
53
+ - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 3.3.0
53
56
  required_rubygems_version: !ruby/object:Gem::Requirement
54
57
  requirements:
55
- - - ">="
56
- - !ruby/object:Gem::Version
57
- version: '0'
58
+ -
59
+ - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
58
62
  requirements: []
59
- rubygems_version: 3.6.0.dev
63
+ rubygems_version: 4.1.0.dev
60
64
  specification_version: 4
61
65
  summary: A simple wrapper library for Slack's Socket Mode API
62
66
  test_files: []