slack_line 1.1 → 1.3
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 +4 -4
- data/.github/workflows/rspec.yml +1 -1
- data/CHANGELOG.md +12 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/README.md +146 -6
- data/bin/slack_line_stateful_thread +9 -0
- data/lib/slack_line/cli/slack_line_message.rb +6 -2
- data/lib/slack_line/cli/slack_line_stateful_thread.rb +126 -0
- data/lib/slack_line/cli/slack_line_thread.rb +6 -2
- data/lib/slack_line/client.rb +4 -4
- data/lib/slack_line/configuration.rb +23 -2
- data/lib/slack_line/disk_caching.rb +18 -0
- data/lib/slack_line/groups.rb +9 -4
- data/lib/slack_line/message.rb +6 -3
- data/lib/slack_line/sent_thread.rb +6 -1
- data/lib/slack_line/thread.rb +6 -2
- data/lib/slack_line/users.rb +9 -4
- data/lib/slack_line/version.rb +1 -1
- data/slack_line.gemspec +1 -0
- metadata +21 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '091877d65cd3c6b2c21d869608359fbc6c9774f663399eb01b2fec9afc098da3'
|
|
4
|
+
data.tar.gz: 848360f12adbca1f5108764f450f448deac06a4fd61efcc8b994e3a00d166128
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1b2ea474330ad8a53da29f5c74e271bfe17392c578dc8f7e2829767e52d4a7952dfb7929a76571236b83b336e7d254a735d9a7b49302ad9c0192f67b4fced92
|
|
7
|
+
data.tar.gz: 2ab43cc4c5c4e02236cadbcaceac55146d9fc118246224494e9210b7e1e7eb21f0cbb04ffc2e8d5af901a0cd2e563ce721ca748e574ab1ac3226add51e4ee80a
|
data/.github/workflows/rspec.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -9,3 +9,15 @@ can do each of those things directly (for simpler messages).
|
|
|
9
9
|
## Version 1.1
|
|
10
10
|
|
|
11
11
|
Fixed incorrect description/summary on gemspec.
|
|
12
|
+
|
|
13
|
+
## Version 1.2
|
|
14
|
+
|
|
15
|
+
Added disk-caching for the users and groups lists, through the `cache_path` and
|
|
16
|
+
`cache_duration` config options and matching CLI flags. Added the
|
|
17
|
+
`slack_line_stateful_thread` script, and `content` and `update` on `SentThread`.
|
|
18
|
+
|
|
19
|
+
## Version 1.3
|
|
20
|
+
|
|
21
|
+
`message` and `thread` now accept `to:`, as the README showed. Declared `reline`
|
|
22
|
+
as a runtime dependency, which fixes a warning or `LoadError` under Bundler on
|
|
23
|
+
Ruby 4.0. The invalid content error now lists the classes of the bad arguments.
|
data/Gemfile
CHANGED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2026 Eric Mueller
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -80,6 +80,19 @@ 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
|
|
86
|
+
[lightly](https://github.com/DannyBen/lightly)
|
|
87
|
+
gem to cache results to disk, which avoids redundant API calls across
|
|
88
|
+
separate runs. Requires `lightly` to be installed as an optional
|
|
89
|
+
dependency - if it is not available, a `DiskCaching::NoLightly` error
|
|
90
|
+
will be raised at runtime.
|
|
91
|
+
* `cache_duration` or `SLACK_LINE_CACHE_DURATION` - how long cached
|
|
92
|
+
data remains valid (default `"15m"`). Accepts a plain integer number
|
|
93
|
+
of seconds, or a number followed by a unit suffix: `s` (seconds),
|
|
94
|
+
`m` (minutes), `h` (hours), or `d` (days). For example: `"30m"`,
|
|
95
|
+
`"2h"`, `"1d"`, or `"900"`.
|
|
83
96
|
|
|
84
97
|
You can just set those via the environment variables, but you can also
|
|
85
98
|
set them on the singleton configuration object:
|
|
@@ -92,10 +105,12 @@ SlackLine.configure do |config|
|
|
|
92
105
|
config.default_channel = "#ci-flow"
|
|
93
106
|
config.per_message_delay = 0.2
|
|
94
107
|
config.per_thread_delay = 2.0
|
|
108
|
+
config.cache_path = "/tmp/slack_line_cache"
|
|
109
|
+
config.cache_duration = "1h"
|
|
95
110
|
end
|
|
96
111
|
```
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
### Multiple Configurations
|
|
99
114
|
|
|
100
115
|
If you're working in a context where you need to support multiple
|
|
101
116
|
SlackLine configurations, don't worry! The singleton central config is
|
|
@@ -122,15 +137,140 @@ BAR_SLACK.thread("Message 1", "Message 2").post
|
|
|
122
137
|
BAR_SLACK.message("Message 3", to: "#bar-team-3").post
|
|
123
138
|
```
|
|
124
139
|
|
|
140
|
+
## CLI Scripts
|
|
141
|
+
|
|
142
|
+
The gem ships with three executable scripts for sending and managing Slack
|
|
143
|
+
messages from the command line. All three accept `-t`/`--slack-token TOKEN` and
|
|
144
|
+
`-n`/`--bot-name NAME` to override the corresponding environment variables.
|
|
145
|
+
Configuration can also come from `SLACK_LINE_SLACK_TOKEN` and friends as
|
|
146
|
+
described above.
|
|
147
|
+
|
|
148
|
+
### `slack_line_message`
|
|
149
|
+
|
|
150
|
+
Sends, updates, or previews a single Slack message.
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
# Post a simple message
|
|
154
|
+
slack_line_message --post-to "#general" --save /tmp/msg.json "Something happened!"
|
|
155
|
+
|
|
156
|
+
# Preview without posting (prints JSON block kit content)
|
|
157
|
+
slack_line_message "Something happened!"
|
|
158
|
+
|
|
159
|
+
# Post a message using the block-kit DSL on stdin
|
|
160
|
+
echo 'text "Something happened!"' | slack_line_message --post-to "#general" --save /tmp/msg.json
|
|
161
|
+
|
|
162
|
+
# Append a reply to an existing thread (saved from a prior post)
|
|
163
|
+
slack_line_message --append /tmp/msg.json --save /tmp/msg.json "Follow-up!"
|
|
164
|
+
|
|
165
|
+
# Update a previously-sent message in place
|
|
166
|
+
slack_line_message --update /tmp/msg.json "Edited message text"
|
|
167
|
+
|
|
168
|
+
# Update a specific message within a saved thread (0-indexed)
|
|
169
|
+
slack_line_message --update /tmp/msg.json --message-number 2 "Corrected reply"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Options:
|
|
173
|
+
|
|
174
|
+
* `-p`/`--post-to TARGET` - channel or user to post to
|
|
175
|
+
* `-a`/`--append PATH` - append a reply to the thread saved at PATH
|
|
176
|
+
* `-U`/`--update PATH` - update the message (or thread) saved at PATH
|
|
177
|
+
* `-m`/`--message-number N` - which message in a thread to update (0-indexed;
|
|
178
|
+
required with `--update` on a thread)
|
|
179
|
+
* `-s`/`--save PATH` - write the sent/updated result to PATH as JSON
|
|
180
|
+
* `-u`/`--look-up-users` - resolve `@mentions` via the Slack API
|
|
181
|
+
* `--cache-path PATH` / `--cache-duration DURATION` - disk-cache user/group
|
|
182
|
+
lookups
|
|
183
|
+
* `--no-backoff` - disable per-message sleep delays
|
|
184
|
+
|
|
185
|
+
When no content arguments are given and no DSL is piped, the script reads DSL
|
|
186
|
+
interactively from stdin.
|
|
187
|
+
|
|
188
|
+
### `slack_line_thread`
|
|
189
|
+
|
|
190
|
+
Sends or previews a thread (multiple messages posted together).
|
|
191
|
+
|
|
192
|
+
```sh
|
|
193
|
+
# Post a thread from positional string arguments
|
|
194
|
+
slack_line_thread --post-to "#general" --save /tmp/thread.json "First" "Second" "Third"
|
|
195
|
+
|
|
196
|
+
# Preview the thread without posting
|
|
197
|
+
slack_line_thread "First" "Second"
|
|
198
|
+
|
|
199
|
+
# Post a thread from block-kit DSL on stdin
|
|
200
|
+
cat thread.dsl | slack_line_thread --post-to "#general" --save /tmp/thread.json
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A DSL block looks like:
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
message "Simple first message"
|
|
207
|
+
message do
|
|
208
|
+
text "Fancier second message"
|
|
209
|
+
context "with some context"
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Options mirror `slack_line_message`, minus the update/append flags:
|
|
214
|
+
|
|
215
|
+
* `-p`/`--post-to TARGET` - channel or user to post to
|
|
216
|
+
* `-s`/`--save PATH` - write the sent result to PATH as JSON
|
|
217
|
+
* `-u`/`--look-up-users`, `--cache-path`, `--cache-duration`, `--no-backoff` -
|
|
218
|
+
same as above
|
|
219
|
+
|
|
220
|
+
### `slack_line_stateful_thread`
|
|
221
|
+
|
|
222
|
+
Designed for long-running processes that need to post a single status message
|
|
223
|
+
and then keep it updated as state changes - for example, a deployment pipeline
|
|
224
|
+
that posts `[running] Deploy started`, updates it to `[done] Deploy finished`,
|
|
225
|
+
and appends replies along the way. The sent message is persisted to a file;
|
|
226
|
+
subsequent invocations load and re-persist that file to know which Slack message
|
|
227
|
+
to update.
|
|
228
|
+
|
|
229
|
+
Messages are formatted as `[STATE] body`. The `--state` and `--message` flags
|
|
230
|
+
each update their respective part independently; omitting one leaves it
|
|
231
|
+
unchanged on update.
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
# Initial post - creates /tmp/deploy.json and posts "[running] Deploy started"
|
|
235
|
+
slack_line_stateful_thread --path /tmp/deploy.json --post-to "#deploys" \
|
|
236
|
+
--state running --message "Deploy started"
|
|
237
|
+
|
|
238
|
+
# Update the state only - becomes "[done] Deploy started"
|
|
239
|
+
slack_line_stateful_thread --path /tmp/deploy.json --state done
|
|
240
|
+
|
|
241
|
+
# Update the body only - becomes "[done] Deploy finished"
|
|
242
|
+
slack_line_stateful_thread --path /tmp/deploy.json --message "Deploy finished"
|
|
243
|
+
|
|
244
|
+
# Update both state and body - becomes "[failed] Something went wrong"
|
|
245
|
+
slack_line_stateful_thread --path /tmp/deploy.json --state failed --message "Something went wrong"
|
|
246
|
+
|
|
247
|
+
# Append a thread reply without changing the main message
|
|
248
|
+
slack_line_stateful_thread --path /tmp/deploy.json --thread --message "Step 1 complete"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Options:
|
|
252
|
+
|
|
253
|
+
* `--path PATH` - (required) file path used to persist the sent message between
|
|
254
|
+
invocations
|
|
255
|
+
* `-p`/`--post-to TARGET` - channel or user; required on the first call,
|
|
256
|
+
forbidden thereafter
|
|
257
|
+
* `-s`/`--state STATE` - the state label shown in brackets; required on first
|
|
258
|
+
call
|
|
259
|
+
* `-m`/`--message MESSAGE` - the message body; required on first call and when
|
|
260
|
+
using `--thread`
|
|
261
|
+
* `--thread` - append a reply instead of updating the main message (mutually
|
|
262
|
+
exclusive with `--state`)
|
|
263
|
+
|
|
125
264
|
## Slack App Permissions
|
|
126
265
|
|
|
127
266
|
In order to post/update messages, the app behind your `SLACK_LINE_TOKEN` can use
|
|
128
267
|
these permissions:
|
|
129
268
|
|
|
130
269
|
* `chat:write` - send messages at all.
|
|
131
|
-
* `chat:write.public` - send messages to public channels your app _isn't a
|
|
132
|
-
of_ (so you don't need to invite them to the relevant channels to make
|
|
270
|
+
* `chat:write.public` - send messages to public channels your app _isn't a
|
|
271
|
+
member of_ (so you don't need to invite them to the relevant channels to make
|
|
272
|
+
them work).
|
|
133
273
|
* `im:write` - start direct messages with individuals.
|
|
134
|
-
* `users:read` and `usergroups:read` - look up users/groups for (a) messaging
|
|
135
|
-
directly or (b) supporting the `look_up_users` config option (for those
|
|
136
|
-
restrictive workspaces)
|
|
274
|
+
* `users:read` and `usergroups:read` - look up users/groups for (a) messaging
|
|
275
|
+
them directly or (b) supporting the `look_up_users` config option (for those
|
|
276
|
+
more restrictive workspaces)
|
|
@@ -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
|
|
|
@@ -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
|
|
data/lib/slack_line/client.rb
CHANGED
|
@@ -12,12 +12,12 @@ module SlackLine
|
|
|
12
12
|
|
|
13
13
|
memoize def slack_client = Slack::Web::Client.new(token: configuration.slack_token)
|
|
14
14
|
|
|
15
|
-
def message(*text_or_blocks, &dsl_block) = Message.new(*text_or_blocks, client: self, &dsl_block)
|
|
15
|
+
def message(*text_or_blocks, to: nil, &dsl_block) = Message.new(*text_or_blocks, client: self, to:, &dsl_block)
|
|
16
16
|
|
|
17
|
-
def thread(*messages, &dsl_block) = Thread.new(*messages, client: self, &dsl_block)
|
|
17
|
+
def thread(*messages, to: nil, &dsl_block) = Thread.new(*messages, client: self, to:, &dsl_block)
|
|
18
18
|
|
|
19
|
-
memoize def users = Users.new(
|
|
19
|
+
memoize def users = Users.new(client: self)
|
|
20
20
|
|
|
21
|
-
memoize def groups = Groups.new(
|
|
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
|
data/lib/slack_line/groups.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
module SlackLine
|
|
2
2
|
class Groups
|
|
3
3
|
include Memoization
|
|
4
|
+
include DiskCaching
|
|
4
5
|
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
6
|
+
def initialize(client:)
|
|
7
|
+
@client = client
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
memoize def all
|
|
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 :
|
|
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 || []
|
data/lib/slack_line/message.rb
CHANGED
|
@@ -3,14 +3,17 @@ module SlackLine
|
|
|
3
3
|
extend Forwardable
|
|
4
4
|
include Memoization
|
|
5
5
|
|
|
6
|
-
def initialize(*text_or_blocks, client:, &dsl_block)
|
|
6
|
+
def initialize(*text_or_blocks, client:, to: nil, &dsl_block)
|
|
7
7
|
@text_or_blocks = text_or_blocks
|
|
8
8
|
@dsl_block = dsl_block
|
|
9
9
|
@client = client
|
|
10
|
+
@to = to
|
|
10
11
|
|
|
11
12
|
validate!
|
|
12
13
|
end
|
|
13
14
|
|
|
15
|
+
attr_reader :to
|
|
16
|
+
|
|
14
17
|
memoize def content
|
|
15
18
|
if @dsl_block
|
|
16
19
|
MessageContext.new(&@dsl_block).content
|
|
@@ -30,7 +33,7 @@ module SlackLine
|
|
|
30
33
|
"https://app.slack.com/block-kit-builder##{escaped_json}"
|
|
31
34
|
end
|
|
32
35
|
|
|
33
|
-
def post(to: nil, thread_ts: nil) = MessageSender.new(message: self, client:, to
|
|
36
|
+
def post(to: nil, thread_ts: nil) = MessageSender.new(message: self, client:, to: to || self.to, thread_ts:).post
|
|
34
37
|
|
|
35
38
|
private
|
|
36
39
|
|
|
@@ -49,7 +52,7 @@ module SlackLine
|
|
|
49
52
|
|
|
50
53
|
def validate_type!
|
|
51
54
|
unless @text_or_blocks.empty? || blocks?(@text_or_blocks) || strings?(@text_or_blocks)
|
|
52
|
-
raise(ArgumentError, "Invalid content type: #{@text_or_blocks.class}")
|
|
55
|
+
raise(ArgumentError, "Invalid content type: #{@text_or_blocks.map(&:class).uniq.join(", ")}")
|
|
53
56
|
end
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -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..])
|
data/lib/slack_line/thread.rb
CHANGED
|
@@ -4,14 +4,17 @@ module SlackLine
|
|
|
4
4
|
include Enumerable
|
|
5
5
|
include Memoization
|
|
6
6
|
|
|
7
|
-
def initialize(*supplied_messages, client:, &dsl_block)
|
|
7
|
+
def initialize(*supplied_messages, client:, to: nil, &dsl_block)
|
|
8
8
|
@supplied_messages = supplied_messages
|
|
9
9
|
@dsl_block = dsl_block
|
|
10
10
|
@client = client
|
|
11
|
+
@to = to
|
|
11
12
|
|
|
12
13
|
validate!
|
|
13
14
|
end
|
|
14
15
|
|
|
16
|
+
attr_reader :to
|
|
17
|
+
|
|
15
18
|
# an Array of SlackLine::Messages
|
|
16
19
|
memoize def messages = message_contents.map { |mc| convert_supplied_message(mc) }
|
|
17
20
|
|
|
@@ -20,11 +23,12 @@ module SlackLine
|
|
|
20
23
|
memoize def builder_urls = messages.map(&:builder_url)
|
|
21
24
|
|
|
22
25
|
def post(to: nil)
|
|
26
|
+
target = to || self.to || messages.filter_map(&:to).first
|
|
23
27
|
sent_messages = []
|
|
24
28
|
thread_ts = nil
|
|
25
29
|
|
|
26
30
|
messages.each do |message|
|
|
27
|
-
sent = message.post(to
|
|
31
|
+
sent = message.post(to: target, thread_ts:)
|
|
28
32
|
thread_ts ||= sent.ts
|
|
29
33
|
sent_messages << sent
|
|
30
34
|
end
|
data/lib/slack_line/users.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
module SlackLine
|
|
2
2
|
class Users
|
|
3
3
|
include Memoization
|
|
4
|
+
include DiskCaching
|
|
4
5
|
|
|
5
|
-
def initialize(
|
|
6
|
-
@
|
|
6
|
+
def initialize(client:)
|
|
7
|
+
@client = client
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
memoize def all
|
|
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 :
|
|
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}
|
data/lib/slack_line/version.rb
CHANGED
data/slack_line.gemspec
CHANGED
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.
|
|
4
|
+
version: '1.3'
|
|
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-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -164,6 +164,20 @@ dependencies:
|
|
|
164
164
|
- - "~>"
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: 3.1.0
|
|
167
|
+
- !ruby/object:Gem::Dependency
|
|
168
|
+
name: reline
|
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
|
170
|
+
requirements:
|
|
171
|
+
- - ">="
|
|
172
|
+
- !ruby/object:Gem::Version
|
|
173
|
+
version: '0.6'
|
|
174
|
+
type: :runtime
|
|
175
|
+
prerelease: false
|
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0.6'
|
|
167
181
|
description: |
|
|
168
182
|
Sending messages with the Slack API is not that difficult, but I've had
|
|
169
183
|
to solve the same problems a lot of times at different companies. This
|
|
@@ -173,6 +187,7 @@ email:
|
|
|
173
187
|
- nevinera@gmail.com
|
|
174
188
|
executables:
|
|
175
189
|
- slack_line_message
|
|
190
|
+
- slack_line_stateful_thread
|
|
176
191
|
- slack_line_thread
|
|
177
192
|
extensions: []
|
|
178
193
|
extra_rdoc_files: []
|
|
@@ -188,15 +203,19 @@ files:
|
|
|
188
203
|
- ".standard.yml"
|
|
189
204
|
- CHANGELOG.md
|
|
190
205
|
- Gemfile
|
|
206
|
+
- LICENSE
|
|
191
207
|
- README.md
|
|
192
208
|
- bin/slack_line_message
|
|
209
|
+
- bin/slack_line_stateful_thread
|
|
193
210
|
- bin/slack_line_thread
|
|
194
211
|
- lib/slack_line.rb
|
|
195
212
|
- lib/slack_line/cli.rb
|
|
196
213
|
- lib/slack_line/cli/slack_line_message.rb
|
|
214
|
+
- lib/slack_line/cli/slack_line_stateful_thread.rb
|
|
197
215
|
- lib/slack_line/cli/slack_line_thread.rb
|
|
198
216
|
- lib/slack_line/client.rb
|
|
199
217
|
- lib/slack_line/configuration.rb
|
|
218
|
+
- lib/slack_line/disk_caching.rb
|
|
200
219
|
- lib/slack_line/groups.rb
|
|
201
220
|
- lib/slack_line/memoization.rb
|
|
202
221
|
- lib/slack_line/message.rb
|