slack_line 1.0 → 1.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: 5012be14f249f312a75b1d5cca149fd938eb2d6b66db4b51fb93789b3a4030e9
4
- data.tar.gz: 6a4244475ce9b64db464f391d6dbfa0c8d6a1dc509854b1b05a6bc1149ad78de
3
+ metadata.gz: bc23e7f575bc68df1c9824aacbde9b74050c14f82f765fc6929dc33ae909eccb
4
+ data.tar.gz: 45e54647313e4c71a27de6c8bda77826deaf7704ac8a5680c1439c5015f9b50e
5
5
  SHA512:
6
- metadata.gz: 1bb9462733e8c15e292606d303c04d2c72855d9d141ebfd917b9f2bf415b964aad0dd34fd0832c1a74064f551272da0dfce670ae43047828ee0c89a8fd86f6c5
7
- data.tar.gz: 455afd3746d46319fe637e924034655cfa9f42da2c06d8c30a3f578ed70e2245811f6f7a5404d37939360cdb47309a966485d8151ec41e578724fb494c222a18
6
+ metadata.gz: 5c9d9e4c86c4ae41adac7a71a1044fe02aef53eaa823bc286862178b9fce6de8716d51a7833ed6ee118a550e29726358dd7b2a54df3bc78526074be65a125af4
7
+ data.tar.gz: fc217ed772fbbcbf1a27aa0911d24f2ef716c4b6ffed127f2c0959329b5358e510654a4d313c9d42e8a0168a7b88fe3c5d253d46545775175bd3671c3e593a65
@@ -15,10 +15,11 @@ jobs:
15
15
 
16
16
  - name: Cache gems
17
17
  uses: actions/cache@v3
18
- with: path: vendor/bundle
19
- key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
20
- restore-keys:
21
- ${{ runner.os }}-linters-
18
+ with:
19
+ path: vendor/bundle
20
+ key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
21
+ restore-keys:
22
+ ${{ runner.os }}-linters-
22
23
 
23
24
  - name: Install gems
24
25
  run: bundle install --jobs 4 --retry 3
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## Version 1.0
4
+
5
+ Initially published gem. Can send messages and threads, update messages, extend
6
+ threads, persist those things to disk and reload them. Convenience scripts that
7
+ can do each of those things directly (for simpler messages).
8
+
9
+ ## Version 1.1
10
+
11
+ Fixed incorrect description/summary on gemspec.
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source "https://gem.coop"
2
2
 
3
3
  gemspec
4
+
5
+ gem "lightly"
data/README.md CHANGED
@@ -80,6 +80,18 @@ be set via ENV or `SlackLine.configure`:
80
80
  * `per_thread_delay` or `SLACK_LINE_PER_THREAD_DELAY` is a float as
81
81
  well - SlackLine will `sleep` for this duration after each _thread_
82
82
  is posted, and after each non-thread message is posted.
83
+ * `cache_path` or `SLACK_LINE_CACHE_PATH` - a directory path for
84
+ disk-caching the users and groups lists fetched from the Slack API.
85
+ When set, SlackLine will use the [lightly](https://github.com/DannyBen/lightly)
86
+ gem to cache results to disk, which avoids redundant API calls across
87
+ separate runs. Requires `lightly` to be installed as an optional
88
+ dependency - if it is not available, a `DiskCaching::NoLightly` error
89
+ will be raised at runtime.
90
+ * `cache_duration` or `SLACK_LINE_CACHE_DURATION` - how long cached
91
+ data remains valid (default `"15m"`). Accepts a plain integer number
92
+ of seconds, or a number followed by a unit suffix: `s` (seconds),
93
+ `m` (minutes), `h` (hours), or `d` (days). For example: `"30m"`,
94
+ `"2h"`, `"1d"`, or `"900"`.
83
95
 
84
96
  You can just set those via the environment variables, but you can also
85
97
  set them on the singleton configuration object:
@@ -92,10 +104,12 @@ SlackLine.configure do |config|
92
104
  config.default_channel = "#ci-flow"
93
105
  config.per_message_delay = 0.2
94
106
  config.per_thread_delay = 2.0
107
+ config.cache_path = "/tmp/slack_line_cache"
108
+ config.cache_duration = "1h"
95
109
  end
96
110
  ```
97
111
 
98
- ## Multiple Configurations
112
+ ### Multiple Configurations
99
113
 
100
114
  If you're working in a context where you need to support multiple
101
115
  SlackLine configurations, don't worry! The singleton central config is
@@ -122,13 +136,135 @@ BAR_SLACK.thread("Message 1", "Message 2").post
122
136
  BAR_SLACK.message("Message 3", to: "#bar-team-3").post
123
137
  ```
124
138
 
139
+ ## CLI Scripts
140
+
141
+ The gem ships with three executable scripts for sending and managing Slack messages
142
+ from the command line. All three accept `-t`/`--slack-token TOKEN` and
143
+ `-n`/`--bot-name NAME` to override the corresponding environment variables.
144
+ Configuration can also come from `SLACK_LINE_SLACK_TOKEN` and friends as described
145
+ above.
146
+
147
+ ### `slack_line_message`
148
+
149
+ Sends, updates, or previews a single Slack message.
150
+
151
+ ```sh
152
+ # Post a simple message
153
+ slack_line_message --post-to "#general" --save /tmp/msg.json "Something happened!"
154
+
155
+ # Preview without posting (prints JSON block kit content)
156
+ slack_line_message "Something happened!"
157
+
158
+ # Post a message using the block-kit DSL on stdin
159
+ echo 'text "Something happened!"' | slack_line_message --post-to "#general" --save /tmp/msg.json
160
+
161
+ # Append a reply to an existing thread (saved from a prior post)
162
+ slack_line_message --append /tmp/msg.json --save /tmp/msg.json "Follow-up!"
163
+
164
+ # Update a previously-sent message in place
165
+ slack_line_message --update /tmp/msg.json "Edited message text"
166
+
167
+ # Update a specific message within a saved thread (0-indexed)
168
+ slack_line_message --update /tmp/msg.json --message-number 2 "Corrected reply"
169
+ ```
170
+
171
+ Options:
172
+
173
+ * `-p`/`--post-to TARGET` - channel or user to post to
174
+ * `-a`/`--append PATH` - append a reply to the thread saved at PATH
175
+ * `-U`/`--update PATH` - update the message (or thread) saved at PATH
176
+ * `-m`/`--message-number N` - which message in a thread to update (0-indexed;
177
+ required with `--update` on a thread)
178
+ * `-s`/`--save PATH` - write the sent/updated result to PATH as JSON
179
+ * `-u`/`--look-up-users` - resolve `@mentions` via the Slack API
180
+ * `--cache-path PATH` / `--cache-duration DURATION` - disk-cache user/group lookups
181
+ * `--no-backoff` - disable per-message sleep delays
182
+
183
+ When no content arguments are given and no DSL is piped, the script reads DSL
184
+ interactively from stdin.
185
+
186
+ ### `slack_line_thread`
187
+
188
+ Sends or previews a thread (multiple messages posted together).
189
+
190
+ ```sh
191
+ # Post a thread from positional string arguments
192
+ slack_line_thread --post-to "#general" --save /tmp/thread.json "First" "Second" "Third"
193
+
194
+ # Preview the thread without posting
195
+ slack_line_thread "First" "Second"
196
+
197
+ # Post a thread from block-kit DSL on stdin
198
+ cat thread.dsl | slack_line_thread --post-to "#general" --save /tmp/thread.json
199
+ ```
200
+
201
+ A DSL block looks like:
202
+
203
+ ```ruby
204
+ message "Simple first message"
205
+ message do
206
+ text "Fancier second message"
207
+ context "with some context"
208
+ end
209
+ ```
210
+
211
+ Options mirror `slack_line_message`, minus the update/append flags:
212
+
213
+ * `-p`/`--post-to TARGET` - channel or user to post to
214
+ * `-s`/`--save PATH` - write the sent result to PATH as JSON
215
+ * `-u`/`--look-up-users`, `--cache-path`, `--cache-duration`, `--no-backoff` -
216
+ same as above
217
+
218
+ ### `slack_line_stateful_thread`
219
+
220
+ Designed for long-running processes that need to post a single status message
221
+ and then keep it updated as state changes - for example, a deployment pipeline
222
+ that posts `[running] Deploy started`, updates it to `[done] Deploy finished`,
223
+ and appends replies along the way. The sent message is persisted to a file;
224
+ subsequent invocations load and re-persist that file to know which Slack message
225
+ to update.
226
+
227
+ Messages are formatted as `[STATE] body`. The `--state` and `--message` flags each
228
+ update their respective part independently; omitting one leaves it unchanged on update.
229
+
230
+ ```sh
231
+ # Initial post - creates /tmp/deploy.json and posts "[running] Deploy started"
232
+ slack_line_stateful_thread --path /tmp/deploy.json --post-to "#deploys" \
233
+ --state running --message "Deploy started"
234
+
235
+ # Update the state only - becomes "[done] Deploy started"
236
+ slack_line_stateful_thread --path /tmp/deploy.json --state done
237
+
238
+ # Update the body only - becomes "[done] Deploy finished"
239
+ slack_line_stateful_thread --path /tmp/deploy.json --message "Deploy finished"
240
+
241
+ # Update both state and body - becomes "[failed] Something went wrong"
242
+ slack_line_stateful_thread --path /tmp/deploy.json --state failed --message "Something went wrong"
243
+
244
+ # Append a thread reply without changing the main message
245
+ slack_line_stateful_thread --path /tmp/deploy.json --thread --message "Step 1 complete"
246
+ ```
247
+
248
+ Options:
249
+
250
+ * `--path PATH` - (required) file path used to persist the sent message between invocations
251
+ * `-p`/`--post-to TARGET` - channel or user; required on the first call, forbidden
252
+ thereafter
253
+ * `-s`/`--state STATE` - the state label shown in brackets; required on first call
254
+ * `-m`/`--message MESSAGE` - the message body; required on first call and when using
255
+ `--thread`
256
+ * `--thread` - append a reply instead of updating the main message (mutually exclusive
257
+ with `--state`)
258
+
125
259
  ## Slack App Permissions
126
260
 
127
- In order to post/update messages, the app behind your `SLACK_LINE_TOKEN` can use these permissions:
261
+ In order to post/update messages, the app behind your `SLACK_LINE_TOKEN` can use
262
+ these permissions:
128
263
 
129
264
  * `chat:write` - send messages at all.
130
- * `chat:write.public` - send messages to public channels your app _isn't a member of_ (so you don't
131
- need to invite them to the relevant channels to make them work).
265
+ * `chat:write.public` - send messages to public channels your app _isn't a member
266
+ of_ (so you don't need to invite them to the relevant channels to make them work).
132
267
  * `im:write` - start direct messages with individuals.
133
- * `users:read` and `usergroups:read` - look up users/groups for (a) messaging them directly or
134
- (b) supporting the `look_up_users` config option (for those more restrictive workspaces)
268
+ * `users:read` and `usergroups:read` - look up users/groups for (a) messaging them
269
+ directly or (b) supporting the `look_up_users` config option (for those more
270
+ restrictive workspaces)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/slack_line"
4
+
5
+ begin
6
+ SlackLine::Cli::SlackLineStatefulThread.new(argv: ARGV).run
7
+ rescue SlackLine::Cli::ExitException => e
8
+ abort e.message
9
+ end
@@ -20,11 +20,13 @@ module SlackLine
20
20
  else
21
21
  run_preview
22
22
  end
23
+ rescue DiskCaching::NoLightly, Configuration::InvalidValue => e
24
+ raise ExitException, e.message
23
25
  end
24
26
 
25
27
  def options
26
28
  return @options if defined?(@options)
27
- opts = {post_to: nil, append: nil, update: nil, message_number: nil, save: nil, slack_token: nil, look_up_users: nil, bot_name: nil, backoff: nil}
29
+ opts = {post_to: nil, append: nil, update: nil, message_number: nil, save: nil, slack_token: nil, look_up_users: nil, bot_name: nil, backoff: nil, cache_path: nil, cache_duration: nil}
28
30
  remaining = option_parser(opts).parse(@argv.dup)
29
31
  if remaining.empty?
30
32
  opts[:dsl] = read_stdin
@@ -37,7 +39,7 @@ module SlackLine
37
39
 
38
40
  def configuration
39
41
  return @configuration if defined?(@configuration)
40
- cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff).compact
42
+ cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff, :cache_path, :cache_duration).compact
41
43
  @configuration = Configuration.new(nil, **cfg_opts)
42
44
  end
43
45
 
@@ -56,6 +58,8 @@ module SlackLine
56
58
  parser.on("-m", "--message-number N", Integer) { |n| opts[:message_number] = n }
57
59
  parser.on("-s", "--save PATH") { |p| opts[:save] = p }
58
60
  parser.on("--no-backoff") { opts[:backoff] = false }
61
+ parser.on("--cache-path PATH") { |p| opts[:cache_path] = p }
62
+ parser.on("--cache-duration DURATION") { |d| opts[:cache_duration] = d }
59
63
  end
60
64
  end
61
65
 
@@ -0,0 +1,126 @@
1
+ require "optparse"
2
+
3
+ module SlackLine
4
+ module Cli
5
+ class SlackLineStatefulThread
6
+ def initialize(argv:, stdout: $stdout, stderr: $stderr)
7
+ @argv = argv
8
+ @stdout = stdout
9
+ @stderr = stderr
10
+ end
11
+
12
+ def run
13
+ validate_options!
14
+ if path_exists?
15
+ run_subsequent
16
+ else
17
+ run_initial
18
+ end
19
+ rescue DiskCaching::NoLightly, Configuration::InvalidValue => e
20
+ raise ExitException, e.message
21
+ end
22
+
23
+ def options
24
+ return @options if defined?(@options)
25
+ opts = {path: nil, post_to: nil, state: nil, message: nil, thread: nil, slack_token: nil, bot_name: nil}
26
+ option_parser(opts).parse(@argv.dup)
27
+ @options = opts
28
+ end
29
+
30
+ def configuration
31
+ return @configuration if defined?(@configuration)
32
+ cfg_opts = options.slice(:slack_token, :bot_name).compact
33
+ @configuration = Configuration.new(nil, **cfg_opts)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :stdout, :stderr
39
+
40
+ def option_parser(opts) # rubocop:disable Metrics/AbcSize
41
+ OptionParser.new do |parser|
42
+ parser.on("-t", "--slack-token TOKEN") { |t| opts[:slack_token] = t }
43
+ parser.on("-n", "--bot-name NAME") { |n| opts[:bot_name] = n }
44
+ parser.on("--path PATH") { |p| opts[:path] = p }
45
+ parser.on("-p", "--post-to TARGET") { |t| opts[:post_to] = t }
46
+ parser.on("-s", "--state STATE") { |s| opts[:state] = s }
47
+ parser.on("-m", "--message MESSAGE") { |m| opts[:message] = m }
48
+ parser.on("--thread") { opts[:thread] = true }
49
+ end
50
+ end
51
+
52
+ def validate_options!
53
+ raise ExitException, "--path is required" unless options[:path]
54
+ path_exists? ? validate_subsequent_options! : validate_initial_options!
55
+ end
56
+
57
+ def validate_initial_options!
58
+ raise ExitException, "--thread cannot be used on initial post" if options[:thread]
59
+ raise ExitException, "--post-to is required for initial post" unless options[:post_to]
60
+ raise ExitException, "--state is required for initial post" unless options[:state]
61
+ raise ExitException, "--message is required for initial post" unless options[:message]
62
+ end
63
+
64
+ def validate_subsequent_options!
65
+ raise ExitException, "--post-to cannot be used after initial post" if options[:post_to]
66
+ options[:thread] ? validate_thread_options! : validate_update_options!
67
+ end
68
+
69
+ def validate_thread_options!
70
+ raise ExitException, "--thread cannot be used with --state" if options[:state]
71
+ raise ExitException, "--thread requires --message" unless options[:message]
72
+ end
73
+
74
+ def validate_update_options!
75
+ raise ExitException, "One of --state or --message is required" unless options[:state] || options[:message]
76
+ end
77
+
78
+ def path_exists? = options[:path] && File.exist?(options[:path])
79
+
80
+ def client = @client ||= Client.new(configuration)
81
+
82
+ def load_sent
83
+ @load_sent ||= SlackLine.from_json(JSON.parse(File.read(options[:path])), client:)
84
+ end
85
+
86
+ def save_sent(result) = File.write(options[:path], JSON.pretty_generate(result.as_json))
87
+
88
+ def run_initial
89
+ text = "[#{options[:state]}] #{options[:message]}"
90
+ sent = Message.new(text, client:).post(to: options[:post_to])
91
+ save_sent(sent)
92
+ stderr.puts "Posted stateful thread to #{options[:post_to]}"
93
+ end
94
+
95
+ def run_subsequent
96
+ options[:thread] ? run_thread_message : run_update_message
97
+ end
98
+
99
+ def parse_state_message(text)
100
+ match = text&.match(/\A\[([^\]]*)\] (.+)\z/m)
101
+ raise ExitException, "Cannot parse state and body from stored message content" unless match
102
+ [match[1], match[2]]
103
+ end
104
+
105
+ def current_state_and_body(sent)
106
+ parse_state_message(sent.content.dig(0, "text", "text"))
107
+ end
108
+
109
+ def run_update_message
110
+ sent = load_sent
111
+ current_state, current_body = current_state_and_body(sent)
112
+ new_text = "[#{options[:state] || current_state}] #{options[:message] || current_body}"
113
+ updated = sent.update(Message.new(new_text, client:))
114
+ save_sent(updated)
115
+ stderr.puts "Updated message in #{updated.channel}"
116
+ end
117
+
118
+ def run_thread_message
119
+ sent = load_sent
120
+ sent_thread = sent.append(options[:message])
121
+ save_sent(sent_thread)
122
+ stderr.puts "Threaded message in #{sent_thread.channel}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -16,11 +16,13 @@ module SlackLine
16
16
  else
17
17
  run_preview
18
18
  end
19
+ rescue DiskCaching::NoLightly, Configuration::InvalidValue => e
20
+ raise ExitException, e.message
19
21
  end
20
22
 
21
23
  def options
22
24
  return @options if defined?(@options)
23
- opts = {post_to: nil, save: nil, slack_token: nil, look_up_users: nil, bot_name: nil, backoff: nil}
25
+ opts = {post_to: nil, save: nil, slack_token: nil, look_up_users: nil, bot_name: nil, backoff: nil, cache_path: nil, cache_duration: nil}
24
26
  remaining = option_parser(opts).parse(@argv.dup)
25
27
  if remaining.empty?
26
28
  opts[:dsl] = read_stdin
@@ -32,7 +34,7 @@ module SlackLine
32
34
 
33
35
  def configuration
34
36
  return @configuration if defined?(@configuration)
35
- cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff).compact
37
+ cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff, :cache_path, :cache_duration).compact
36
38
  @configuration = Configuration.new(nil, **cfg_opts)
37
39
  end
38
40
 
@@ -40,7 +42,7 @@ module SlackLine
40
42
 
41
43
  attr_reader :stdout, :stderr
42
44
 
43
- def option_parser(opts)
45
+ def option_parser(opts) # rubocop:disable Metrics/AbcSize
44
46
  OptionParser.new do |parser|
45
47
  parser.on("-t", "--slack-token TOKEN") { |t| opts[:slack_token] = t }
46
48
  parser.on("-u", "--look-up-users") { opts[:look_up_users] = true }
@@ -48,6 +50,8 @@ module SlackLine
48
50
  parser.on("-p", "--post-to TARGET") { |t| opts[:post_to] = t }
49
51
  parser.on("-s", "--save PATH") { |p| opts[:save] = p }
50
52
  parser.on("--no-backoff") { opts[:backoff] = false }
53
+ parser.on("--cache-path PATH") { |p| opts[:cache_path] = p }
54
+ parser.on("--cache-duration DURATION") { |d| opts[:cache_duration] = d }
51
55
  end
52
56
  end
53
57
 
@@ -16,8 +16,8 @@ module SlackLine
16
16
 
17
17
  def thread(*messages, &dsl_block) = Thread.new(*messages, client: self, &dsl_block)
18
18
 
19
- memoize def users = Users.new(slack_client:)
19
+ memoize def users = Users.new(client: self)
20
20
 
21
- memoize def groups = Groups.new(slack_client:)
21
+ memoize def groups = Groups.new(client: self)
22
22
  end
23
23
  end
@@ -1,12 +1,19 @@
1
1
  module SlackLine
2
2
  class Configuration
3
+ include Memoization
4
+
5
+ InvalidValue = Class.new(Error)
6
+
3
7
  attr_accessor :slack_token,
4
8
  :look_up_users, :bot_name, :default_channel,
5
9
  :per_message_delay, :per_thread_delay,
6
- :backoff
10
+ :backoff, :cache_path
11
+ attr_writer :cache_duration
7
12
 
8
13
  alias_method :look_up_users?, :look_up_users
9
14
 
15
+ memoize def cache_duration = parse_duration(@cache_duration.to_s)
16
+
10
17
  DEFAULTS = {
11
18
  slack_token: nil,
12
19
  look_up_users: false,
@@ -14,7 +21,9 @@ module SlackLine
14
21
  default_channel: nil,
15
22
  per_message_delay: 0.0,
16
23
  per_thread_delay: 0.0,
17
- backoff: true
24
+ backoff: true,
25
+ cache_path: nil,
26
+ cache_duration: "15m"
18
27
  }.freeze
19
28
 
20
29
  def initialize(base_config = nil, **overrides)
@@ -28,6 +37,8 @@ module SlackLine
28
37
  @per_message_delay = cascade(:per_message_delay, "SLACK_LINE_PER_MESSAGE_DELAY", :float)
29
38
  @per_thread_delay = cascade(:per_thread_delay, "SLACK_LINE_PER_THREAD_DELAY", :float)
30
39
  @backoff = cascade(:backoff, "SLACK_LINE_NO_BACKOFF", :inverse_boolean)
40
+ @cache_path = cascade(:cache_path, "SLACK_LINE_CACHE_PATH", :string)
41
+ @cache_duration = cascade(:cache_duration, "SLACK_LINE_CACHE_DURATION", :string)
31
42
  end
32
43
 
33
44
  private
@@ -57,5 +68,15 @@ module SlackLine
57
68
  value
58
69
  end
59
70
  end
71
+
72
+ DURATION_MULTIPLIERS = {s: 1, m: 60, h: 3600, d: 86400}.freeze
73
+
74
+ def parse_duration(value)
75
+ match = value.match(/\A(\d+)([smhd])?\z/)
76
+ raise(InvalidValue, "Invalid duration: #{value.inspect}") unless match
77
+
78
+ digits, unit = match.captures
79
+ digits.to_i * DURATION_MULTIPLIERS.fetch(unit&.to_sym, 1)
80
+ end
60
81
  end
61
82
  end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require "lightly"
3
+ rescue LoadError
4
+ # optional dependency
5
+ end
6
+
7
+ module SlackLine
8
+ module DiskCaching
9
+ NoLightly = Class.new(Error)
10
+
11
+ def cached(config:, key:, &block)
12
+ return yield if config.cache_path.nil?
13
+ raise(NoLightly, "The 'lightly' gem is required for disk caching") unless defined?(Lightly)
14
+
15
+ Lightly.new(dir: config.cache_path, life: config.cache_duration).get(key) { yield }
16
+ end
17
+ end
18
+ end
@@ -1,12 +1,15 @@
1
1
  module SlackLine
2
2
  class Groups
3
3
  include Memoization
4
+ include DiskCaching
4
5
 
5
- def initialize(slack_client:)
6
- @slack_client = slack_client
6
+ def initialize(client:)
7
+ @client = client
7
8
  end
8
9
 
9
- memoize def all = fetch_groups
10
+ memoize def all
11
+ cached(config: client.configuration, key: "groups_all") { fetch_groups }
12
+ end
10
13
 
11
14
  def find(handle:)
12
15
  groups_by_handle[handle.downcase]
@@ -14,7 +17,9 @@ module SlackLine
14
17
 
15
18
  private
16
19
 
17
- attr_reader :slack_client
20
+ attr_reader :client
21
+
22
+ def slack_client = client.slack_client
18
23
 
19
24
  memoize def fetch_groups
20
25
  slack_client.usergroups_list.usergroups || []
@@ -10,9 +10,14 @@ module SlackLine
10
10
  attr_reader :sent_messages
11
11
  alias_method :messages, :sent_messages
12
12
  def_delegators :sent_messages, :each, :map, :size, :first, :last, :empty?
13
- def_delegators :first, :channel, :ts
13
+ def_delegators :first, :channel, :ts, :content
14
14
  alias_method :thread_ts, :ts
15
15
 
16
+ def update(*text_or_blocks, &dsl_block)
17
+ updated_root = first.update(*text_or_blocks, &dsl_block)
18
+ SentThread.new(updated_root, *sent_messages[1..])
19
+ end
20
+
16
21
  def append(*text_or_blocks, &dsl_block)
17
22
  extended = first.append(*text_or_blocks, &dsl_block)
18
23
  SentThread.new(*sent_messages, *extended.sent_messages[1..])
@@ -1,12 +1,15 @@
1
1
  module SlackLine
2
2
  class Users
3
3
  include Memoization
4
+ include DiskCaching
4
5
 
5
- def initialize(slack_client:)
6
- @slack_client = slack_client
6
+ def initialize(client:)
7
+ @client = client
7
8
  end
8
9
 
9
- memoize def all = all_users.reject(&:deleted).reject(&:is_bot)
10
+ memoize def all
11
+ cached(config: client.configuration, key: "users_all") { all_users.reject(&:deleted).reject(&:is_bot) }
12
+ end
10
13
 
11
14
  def find(display_name:)
12
15
  users_by_display_name[display_name.downcase]
@@ -14,7 +17,9 @@ module SlackLine
14
17
 
15
18
  private
16
19
 
17
- attr_reader :slack_client
20
+ attr_reader :client
21
+
22
+ def slack_client = client.slack_client
18
23
 
19
24
  def fetch_page(cursor: nil)
20
25
  params = {limit: 200}
@@ -1,3 +1,3 @@
1
1
  module SlackLine
2
- VERSION = "1.0".freeze
2
+ VERSION = "1.2".freeze
3
3
  end
data/slack_line.gemspec CHANGED
@@ -6,12 +6,12 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Eric Mueller"]
7
7
  spec.email = ["nevinera@gmail.com"]
8
8
 
9
- spec.summary = "Build CLIs that are configured via args, file, and/or environment"
9
+ spec.summary = "A gem to send and extend Slack threads and messages"
10
10
  spec.description = <<~DESC
11
- We've written code that merges/cascades default configuration, config-files,
12
- environment variables, and cli-passed arguments _too many times_. This gem
13
- intends to distill that into a configuration hash describing those controls
14
- and relationships, so that users can supply values in multiple ways.
11
+ Sending messages with the Slack API is not that difficult, but I've had
12
+ to solve the same problems a lot of times at different companies. This
13
+ gem attempts to make those solutions irrelevant by providing a simple
14
+ interface and scripts to send and update messages, and build/extend threads.
15
15
  DESC
16
16
  spec.homepage = "https://github.com/nevinera/slack_line"
17
17
  spec.license = "MIT"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slack_line
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.0'
4
+ version: '1.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Mueller
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-02 00:00:00.000000000 Z
11
+ date: 2026-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -165,14 +165,15 @@ dependencies:
165
165
  - !ruby/object:Gem::Version
166
166
  version: 3.1.0
167
167
  description: |
168
- We've written code that merges/cascades default configuration, config-files,
169
- environment variables, and cli-passed arguments _too many times_. This gem
170
- intends to distill that into a configuration hash describing those controls
171
- and relationships, so that users can supply values in multiple ways.
168
+ Sending messages with the Slack API is not that difficult, but I've had
169
+ to solve the same problems a lot of times at different companies. This
170
+ gem attempts to make those solutions irrelevant by providing a simple
171
+ interface and scripts to send and update messages, and build/extend threads.
172
172
  email:
173
173
  - nevinera@gmail.com
174
174
  executables:
175
175
  - slack_line_message
176
+ - slack_line_stateful_thread
176
177
  - slack_line_thread
177
178
  extensions: []
178
179
  extra_rdoc_files: []
@@ -186,16 +187,20 @@ files:
186
187
  - ".rspec"
187
188
  - ".rubocop.yml"
188
189
  - ".standard.yml"
190
+ - CHANGELOG.md
189
191
  - Gemfile
190
192
  - README.md
191
193
  - bin/slack_line_message
194
+ - bin/slack_line_stateful_thread
192
195
  - bin/slack_line_thread
193
196
  - lib/slack_line.rb
194
197
  - lib/slack_line/cli.rb
195
198
  - lib/slack_line/cli/slack_line_message.rb
199
+ - lib/slack_line/cli/slack_line_stateful_thread.rb
196
200
  - lib/slack_line/cli/slack_line_thread.rb
197
201
  - lib/slack_line/client.rb
198
202
  - lib/slack_line/configuration.rb
203
+ - lib/slack_line/disk_caching.rb
199
204
  - lib/slack_line/groups.rb
200
205
  - lib/slack_line/memoization.rb
201
206
  - lib/slack_line/message.rb
@@ -234,5 +239,5 @@ requirements: []
234
239
  rubygems_version: 3.5.22
235
240
  signing_key:
236
241
  specification_version: 4
237
- summary: Build CLIs that are configured via args, file, and/or environment
242
+ summary: A gem to send and extend Slack threads and messages
238
243
  test_files: []