slack_line 0.1.0 → 1.1
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/linters.yml +5 -4
- data/CHANGELOG.md +11 -0
- data/README.md +13 -0
- data/bin/slack_line_message +4 -44
- data/bin/slack_line_thread +4 -50
- data/lib/slack_line/cli/slack_line_message.rb +154 -0
- data/lib/slack_line/cli/slack_line_thread.rb +96 -0
- data/lib/slack_line/cli.rb +8 -0
- data/lib/slack_line/client.rb +4 -0
- data/lib/slack_line/configuration.rb +9 -2
- data/lib/slack_line/groups.rb +25 -0
- data/lib/slack_line/message.rb +1 -5
- data/lib/slack_line/message_converter.rb +49 -0
- data/lib/slack_line/message_sender.rb +75 -0
- data/lib/slack_line/sent_message.rb +30 -2
- data/lib/slack_line/sent_thread.rb +15 -0
- data/lib/slack_line/thread.rb +1 -2
- data/lib/slack_line/users.rb +44 -0
- data/lib/slack_line/version.rb +1 -1
- data/lib/slack_line.rb +9 -0
- data/slack_line.gemspec +7 -6
- metadata +15 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63f2270ed13b9bf4d14585497109923c70b8e7ec8a34af3ebb46306afdfaf703
|
|
4
|
+
data.tar.gz: 677793b2b72ef6f1d58673b9c658151b3b22504e85cb34678d6e0973d23f00a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 939add34dd7ecd05b44828f53bfd20d62b4c0df9a4deb2b2b2097f736b6a3666173f098e81451416865c0ff66765320b2bff16c8558230eb445a54a3673e9be1
|
|
7
|
+
data.tar.gz: 9dc08564bfb998fe4abe612f546a4f9327924ea70e01a153d5daaa751baae3698296b14cb13bc7c50ff612e31f9f1f54dcf6086a74bd5579a17257307027f2d6
|
|
@@ -15,10 +15,11 @@ jobs:
|
|
|
15
15
|
|
|
16
16
|
- name: Cache gems
|
|
17
17
|
uses: actions/cache@v3
|
|
18
|
-
with:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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/README.md
CHANGED
|
@@ -121,3 +121,16 @@ BAR_SLACK = SlackLine::Client.new(default_channel: "#team-bar", bot_name: "BarBo
|
|
|
121
121
|
BAR_SLACK.thread("Message 1", "Message 2").post
|
|
122
122
|
BAR_SLACK.message("Message 3", to: "#bar-team-3").post
|
|
123
123
|
```
|
|
124
|
+
|
|
125
|
+
## Slack App Permissions
|
|
126
|
+
|
|
127
|
+
In order to post/update messages, the app behind your `SLACK_LINE_TOKEN` can use
|
|
128
|
+
these permissions:
|
|
129
|
+
|
|
130
|
+
* `chat:write` - send messages at all.
|
|
131
|
+
* `chat:write.public` - send messages to public channels your app _isn't a member
|
|
132
|
+
of_ (so you don't need to invite them to the relevant channels to make them work).
|
|
133
|
+
* `im:write` - start direct messages with individuals.
|
|
134
|
+
* `users:read` and `usergroups:read` - look up users/groups for (a) messaging them
|
|
135
|
+
directly or (b) supporting the `look_up_users` config option (for those more
|
|
136
|
+
restrictive workspaces)
|
data/bin/slack_line_message
CHANGED
|
@@ -1,49 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require_relative "../lib/slack_line"
|
|
4
|
-
require "optparse"
|
|
5
|
-
require "reline"
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
configuration = SlackLine::Configuration.new(SlackLine.configuration)
|
|
14
|
-
|
|
15
|
-
OptionParser.new do |opts|
|
|
16
|
-
opts.banner = "Usage: build_slack_line_message [options] [content]"
|
|
17
|
-
|
|
18
|
-
opts.on("-t", "--slack-token TOKEN", "Slack API token") { |t| configuration.slack_token = t }
|
|
19
|
-
opts.on("-u", "--look-up-users", "Enable user look-up") { configuration.look_up_users = true }
|
|
20
|
-
opts.on("-n", "--bot-name NAME", "Bot name to use") { |n| configuration.bot_name = n }
|
|
21
|
-
opts.on("-p", "--post-to TARGET", "Channel or user post the message to") { |t| options[:post_to] = t }
|
|
22
|
-
end.parse!
|
|
23
|
-
|
|
24
|
-
if ARGV.empty?
|
|
25
|
-
warn "No content provided, reading (as dsl) from stdin. Control+D to finish:\n\n"
|
|
26
|
-
options[:dsl] = ""
|
|
27
|
-
while (line = Reline.readline("MSG> ", true))
|
|
28
|
-
options[:dsl] += line + "\n"
|
|
29
|
-
end
|
|
30
|
-
else
|
|
31
|
-
options[:content] = ARGV.dup
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
client = SlackLine::Client.new(configuration)
|
|
35
|
-
|
|
36
|
-
message =
|
|
37
|
-
if options[:content]
|
|
38
|
-
SlackLine::Message.new(*options[:content], client:)
|
|
39
|
-
else
|
|
40
|
-
SlackLine::Message.new(client:) { eval(options[:dsl]) }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
if options[:post_to]
|
|
44
|
-
message.post(to: options[:post_to])
|
|
45
|
-
warn "Posted message to #{options[:post_to]}"
|
|
46
|
-
else
|
|
47
|
-
warn "Preview message at #{message.builder_url}\n\n"
|
|
48
|
-
puts JSON.pretty_generate(message.content.as_json)
|
|
5
|
+
begin
|
|
6
|
+
SlackLine::Cli::SlackLineMessage.new(argv: ARGV).run
|
|
7
|
+
rescue SlackLine::Cli::ExitException => e
|
|
8
|
+
abort e.message
|
|
49
9
|
end
|
data/bin/slack_line_thread
CHANGED
|
@@ -1,55 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
3
|
require_relative "../lib/slack_line"
|
|
4
|
-
require "optparse"
|
|
5
|
-
require "reline"
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
configuration = SlackLine::Configuration.new(SlackLine.configuration)
|
|
14
|
-
|
|
15
|
-
OptionParser.new do |opts|
|
|
16
|
-
opts.banner = "Usage: build_slack_line_message [options] [content]"
|
|
17
|
-
|
|
18
|
-
opts.on("-t", "--slack-token TOKEN", "Slack API token") { |t| configuration.slack_token = t }
|
|
19
|
-
opts.on("-u", "--look-up-users", "Enable user look-up") { configuration.look_up_users = true }
|
|
20
|
-
opts.on("-n", "--bot-name NAME", "Bot name to use") { |n| configuration.bot_name = n }
|
|
21
|
-
opts.on("-p", "--post-to TARGET", "Channel or user post the message to") { |t| options[:post_to] = t }
|
|
22
|
-
end.parse!
|
|
23
|
-
|
|
24
|
-
if ARGV.empty?
|
|
25
|
-
warn "No content provided, reading (as dsl) from stdin. Control+D to finish:\n\n"
|
|
26
|
-
options[:dsl] = ""
|
|
27
|
-
while (line = Reline.readline("THD> ", true))
|
|
28
|
-
options[:dsl] += line + "\n"
|
|
29
|
-
end
|
|
30
|
-
else
|
|
31
|
-
options[:content] = ARGV.dup
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
client = SlackLine::Client.new(configuration)
|
|
35
|
-
|
|
36
|
-
thread =
|
|
37
|
-
if options[:content]
|
|
38
|
-
SlackLine::Thread.new(*options[:content], client:)
|
|
39
|
-
else
|
|
40
|
-
SlackLine::Thread.new(client:) { eval(options[:dsl]) }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
if options[:post_to]
|
|
44
|
-
thread.post_to(options[:post_to])
|
|
45
|
-
warn "Posted thread to #{options[:post_to]}"
|
|
46
|
-
else
|
|
47
|
-
warn "Preview messages at:"
|
|
48
|
-
thread.builder_urls.each { |url| warn " #{url}" }
|
|
49
|
-
|
|
50
|
-
thread.each do |message|
|
|
51
|
-
puts "\n\n--------------------- Message ---------------------\n"
|
|
52
|
-
puts "Preview at: #{message.builder_url}\n\n"
|
|
53
|
-
puts JSON.pretty_generate(message.content.as_json)
|
|
54
|
-
end
|
|
5
|
+
begin
|
|
6
|
+
SlackLine::Cli::SlackLineThread.new(argv: ARGV).run
|
|
7
|
+
rescue SlackLine::Cli::ExitException => e
|
|
8
|
+
abort e.message
|
|
55
9
|
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require "reline"
|
|
3
|
+
|
|
4
|
+
module SlackLine
|
|
5
|
+
module Cli
|
|
6
|
+
class SlackLineMessage
|
|
7
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
8
|
+
@argv = argv
|
|
9
|
+
@stdout = stdout
|
|
10
|
+
@stderr = stderr
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
if options[:append]
|
|
15
|
+
run_append
|
|
16
|
+
elsif options[:update]
|
|
17
|
+
run_update
|
|
18
|
+
elsif options[:post_to]
|
|
19
|
+
run_post
|
|
20
|
+
else
|
|
21
|
+
run_preview
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def options
|
|
26
|
+
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}
|
|
28
|
+
remaining = option_parser(opts).parse(@argv.dup)
|
|
29
|
+
if remaining.empty?
|
|
30
|
+
opts[:dsl] = read_stdin
|
|
31
|
+
else
|
|
32
|
+
opts[:content] = remaining
|
|
33
|
+
end
|
|
34
|
+
validate_options!(opts)
|
|
35
|
+
@options = opts
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def configuration
|
|
39
|
+
return @configuration if defined?(@configuration)
|
|
40
|
+
cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff).compact
|
|
41
|
+
@configuration = Configuration.new(nil, **cfg_opts)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
attr_reader :stdout, :stderr
|
|
47
|
+
|
|
48
|
+
def option_parser(opts) # rubocop:disable Metrics/AbcSize
|
|
49
|
+
OptionParser.new do |parser|
|
|
50
|
+
parser.on("-t", "--slack-token TOKEN") { |t| opts[:slack_token] = t }
|
|
51
|
+
parser.on("-u", "--look-up-users") { opts[:look_up_users] = true }
|
|
52
|
+
parser.on("-n", "--bot-name NAME") { |n| opts[:bot_name] = n }
|
|
53
|
+
parser.on("-p", "--post-to TARGET") { |t| opts[:post_to] = t }
|
|
54
|
+
parser.on("-a", "--append PATH") { |p| opts[:append] = p }
|
|
55
|
+
parser.on("-U", "--update PATH") { |p| opts[:update] = p }
|
|
56
|
+
parser.on("-m", "--message-number N", Integer) { |n| opts[:message_number] = n }
|
|
57
|
+
parser.on("-s", "--save PATH") { |p| opts[:save] = p }
|
|
58
|
+
parser.on("--no-backoff") { opts[:backoff] = false }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def validate_options!(opts)
|
|
63
|
+
if [opts[:post_to], opts[:append], opts[:update]].compact.size > 1
|
|
64
|
+
raise(ExitException, "Only one of --post-to, --append, or --update can be used at a time")
|
|
65
|
+
end
|
|
66
|
+
raise ExitException, "--message-number requires --update" if opts[:message_number] && !opts[:update]
|
|
67
|
+
raise ExitException, "--message-number cannot be less than 0" if opts[:message_number] && opts[:message_number] < 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read_stdin
|
|
71
|
+
stderr.puts "No content provided, reading (as dsl) from stdin. Control+D to finish:\n\n"
|
|
72
|
+
lines = []
|
|
73
|
+
while (line = Reline.readline("MSG> ", true))
|
|
74
|
+
lines << line
|
|
75
|
+
end
|
|
76
|
+
lines.join("\n")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def client = @client ||= Client.new(configuration)
|
|
80
|
+
|
|
81
|
+
def load_json(path, client:) = SlackLine.from_json(JSON.parse(File.read(path)), client:)
|
|
82
|
+
|
|
83
|
+
def save(result) = File.write(options[:save], JSON.pretty_generate(result.as_json))
|
|
84
|
+
|
|
85
|
+
def message
|
|
86
|
+
@message ||=
|
|
87
|
+
if options[:content]
|
|
88
|
+
Message.new(*options[:content], client:)
|
|
89
|
+
else
|
|
90
|
+
dsl = options[:dsl]
|
|
91
|
+
Message.new(client:) { eval(dsl) } # standard:disable Security/Eval
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def append_target = @append_target ||= load_json(options[:append], client:)
|
|
96
|
+
|
|
97
|
+
def run_append
|
|
98
|
+
sent_thread = append_target.append(message)
|
|
99
|
+
stderr.puts "Appended to thread in #{sent_thread.channel}"
|
|
100
|
+
save(sent_thread) if options[:save]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def update_target = @update_target ||= load_json(options[:update], client:)
|
|
104
|
+
|
|
105
|
+
def validate_update_flags!
|
|
106
|
+
message_number = options[:message_number]
|
|
107
|
+
if update_target.is_a?(SentMessage)
|
|
108
|
+
raise(ExitException, "--message-number cannot be used when updating a single message") if message_number
|
|
109
|
+
elsif message_number.nil?
|
|
110
|
+
raise(ExitException, "--message-number is required when updating a thread")
|
|
111
|
+
elsif message_number >= update_target.size
|
|
112
|
+
raise(ExitException, "--message-number #{message_number} is out of range (thread has #{update_target.size} messages)")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def message_to_update
|
|
117
|
+
return @message_to_update if defined?(@message_to_update)
|
|
118
|
+
|
|
119
|
+
validate_update_flags!
|
|
120
|
+
if update_target.is_a?(SentMessage)
|
|
121
|
+
@message_to_update = update_target
|
|
122
|
+
else
|
|
123
|
+
msg_num = options[:message_number]
|
|
124
|
+
@message_to_update = update_target.sent_messages[msg_num]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run_update
|
|
129
|
+
updated_msg = message_to_update.update(message)
|
|
130
|
+
result = update_target.is_a?(SentThread) ? rebuilt_thread(updated_msg) : updated_msg
|
|
131
|
+
type = update_target.is_a?(SentThread) ? "thread" : "message"
|
|
132
|
+
stderr.puts "Updated #{type} in #{result.channel}"
|
|
133
|
+
save(result) if options[:save]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def rebuilt_thread(updated_msg)
|
|
137
|
+
idx = options[:message_number]
|
|
138
|
+
msgs = update_target.sent_messages
|
|
139
|
+
SentThread.new(*msgs[0...idx], updated_msg, *msgs[(idx + 1)..])
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def run_post
|
|
143
|
+
sent = message.post(to: options[:post_to])
|
|
144
|
+
stderr.puts "Posted message to #{options[:post_to]}"
|
|
145
|
+
save(sent) if options[:save]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def run_preview
|
|
149
|
+
stderr.puts "Preview message at #{message.builder_url}\n\n"
|
|
150
|
+
stdout.puts JSON.pretty_generate(message.content.as_json)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
require "reline"
|
|
3
|
+
|
|
4
|
+
module SlackLine
|
|
5
|
+
module Cli
|
|
6
|
+
class SlackLineThread
|
|
7
|
+
def initialize(argv:, stdout: $stdout, stderr: $stderr)
|
|
8
|
+
@argv = argv
|
|
9
|
+
@stdout = stdout
|
|
10
|
+
@stderr = stderr
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run
|
|
14
|
+
if options[:post_to]
|
|
15
|
+
run_post
|
|
16
|
+
else
|
|
17
|
+
run_preview
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def options
|
|
22
|
+
return @options if defined?(@options)
|
|
23
|
+
opts = {post_to: nil, save: nil, slack_token: nil, look_up_users: nil, bot_name: nil, backoff: nil}
|
|
24
|
+
remaining = option_parser(opts).parse(@argv.dup)
|
|
25
|
+
if remaining.empty?
|
|
26
|
+
opts[:dsl] = read_stdin
|
|
27
|
+
else
|
|
28
|
+
opts[:content] = remaining
|
|
29
|
+
end
|
|
30
|
+
@options = opts
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def configuration
|
|
34
|
+
return @configuration if defined?(@configuration)
|
|
35
|
+
cfg_opts = options.slice(:slack_token, :look_up_users, :bot_name, :backoff).compact
|
|
36
|
+
@configuration = Configuration.new(nil, **cfg_opts)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :stdout, :stderr
|
|
42
|
+
|
|
43
|
+
def option_parser(opts) # rubocop:disable Metrics/AbcSize
|
|
44
|
+
OptionParser.new do |parser|
|
|
45
|
+
parser.on("-t", "--slack-token TOKEN") { |t| opts[:slack_token] = t }
|
|
46
|
+
parser.on("-u", "--look-up-users") { opts[:look_up_users] = true }
|
|
47
|
+
parser.on("-n", "--bot-name NAME") { |n| opts[:bot_name] = n }
|
|
48
|
+
parser.on("-p", "--post-to TARGET") { |t| opts[:post_to] = t }
|
|
49
|
+
parser.on("-s", "--save PATH") { |p| opts[:save] = p }
|
|
50
|
+
parser.on("--no-backoff") { opts[:backoff] = false }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read_stdin
|
|
55
|
+
stderr.puts "No content provided, reading (as dsl) from stdin. Control+D to finish:\n\n"
|
|
56
|
+
lines = []
|
|
57
|
+
while (line = Reline.readline("THD> ", true))
|
|
58
|
+
lines << line
|
|
59
|
+
end
|
|
60
|
+
lines.join("\n")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def client = @client ||= Client.new(configuration)
|
|
64
|
+
|
|
65
|
+
def save(result) = File.write(options[:save], JSON.pretty_generate(result.as_json))
|
|
66
|
+
|
|
67
|
+
def thread
|
|
68
|
+
@thread ||=
|
|
69
|
+
if options[:content]
|
|
70
|
+
Thread.new(*options[:content], client:)
|
|
71
|
+
else
|
|
72
|
+
dsl = options[:dsl]
|
|
73
|
+
Thread.new(client:) { eval(dsl) } # standard:disable Security/Eval
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_post
|
|
78
|
+
sent = thread.post(to: options[:post_to])
|
|
79
|
+
stderr.puts "Posted thread to #{options[:post_to]}"
|
|
80
|
+
save(sent) if options[:save]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def preview_message(message)
|
|
84
|
+
stdout.puts "\n\n--------------------- Message ---------------------\n"
|
|
85
|
+
stdout.puts "Preview at: #{message.builder_url}\n\n"
|
|
86
|
+
stdout.puts JSON.pretty_generate(message.content.as_json)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_preview
|
|
90
|
+
stderr.puts "Preview messages at:"
|
|
91
|
+
thread.builder_urls.each { |url| stderr.puts " #{url}" }
|
|
92
|
+
thread.each { |message| preview_message(message) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/slack_line/client.rb
CHANGED
|
@@ -15,5 +15,9 @@ module SlackLine
|
|
|
15
15
|
def message(*text_or_blocks, &dsl_block) = Message.new(*text_or_blocks, client: self, &dsl_block)
|
|
16
16
|
|
|
17
17
|
def thread(*messages, &dsl_block) = Thread.new(*messages, client: self, &dsl_block)
|
|
18
|
+
|
|
19
|
+
memoize def users = Users.new(slack_client:)
|
|
20
|
+
|
|
21
|
+
memoize def groups = Groups.new(slack_client:)
|
|
18
22
|
end
|
|
19
23
|
end
|
|
@@ -2,7 +2,10 @@ module SlackLine
|
|
|
2
2
|
class Configuration
|
|
3
3
|
attr_accessor :slack_token,
|
|
4
4
|
:look_up_users, :bot_name, :default_channel,
|
|
5
|
-
:per_message_delay, :per_thread_delay
|
|
5
|
+
:per_message_delay, :per_thread_delay,
|
|
6
|
+
:backoff
|
|
7
|
+
|
|
8
|
+
alias_method :look_up_users?, :look_up_users
|
|
6
9
|
|
|
7
10
|
DEFAULTS = {
|
|
8
11
|
slack_token: nil,
|
|
@@ -10,7 +13,8 @@ module SlackLine
|
|
|
10
13
|
bot_name: nil,
|
|
11
14
|
default_channel: nil,
|
|
12
15
|
per_message_delay: 0.0,
|
|
13
|
-
per_thread_delay: 0.0
|
|
16
|
+
per_thread_delay: 0.0,
|
|
17
|
+
backoff: true
|
|
14
18
|
}.freeze
|
|
15
19
|
|
|
16
20
|
def initialize(base_config = nil, **overrides)
|
|
@@ -23,6 +27,7 @@ module SlackLine
|
|
|
23
27
|
@default_channel = cascade(:default_channel, "SLACK_LINE_DEFAULT_CHANNEL", :string)
|
|
24
28
|
@per_message_delay = cascade(:per_message_delay, "SLACK_LINE_PER_MESSAGE_DELAY", :float)
|
|
25
29
|
@per_thread_delay = cascade(:per_thread_delay, "SLACK_LINE_PER_THREAD_DELAY", :float)
|
|
30
|
+
@backoff = cascade(:backoff, "SLACK_LINE_NO_BACKOFF", :inverse_boolean)
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
private
|
|
@@ -44,6 +49,8 @@ module SlackLine
|
|
|
44
49
|
|
|
45
50
|
if env_type == :boolean
|
|
46
51
|
%w[1 true yes].include?(value.downcase)
|
|
52
|
+
elsif env_type == :inverse_boolean
|
|
53
|
+
!%w[1 true yes].include?(value.downcase)
|
|
47
54
|
elsif env_type == :float
|
|
48
55
|
value.to_f
|
|
49
56
|
else
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module SlackLine
|
|
2
|
+
class Groups
|
|
3
|
+
include Memoization
|
|
4
|
+
|
|
5
|
+
def initialize(slack_client:)
|
|
6
|
+
@slack_client = slack_client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
memoize def all = fetch_groups
|
|
10
|
+
|
|
11
|
+
def find(handle:)
|
|
12
|
+
groups_by_handle[handle.downcase]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :slack_client
|
|
18
|
+
|
|
19
|
+
memoize def fetch_groups
|
|
20
|
+
slack_client.usergroups_list.usergroups || []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
memoize def groups_by_handle = all.map { |g| [g.handle.downcase, g] }.to_h
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/slack_line/message.rb
CHANGED
|
@@ -30,11 +30,7 @@ module SlackLine
|
|
|
30
30
|
"https://app.slack.com/block-kit-builder##{escaped_json}"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def post(to: nil, thread_ts: nil)
|
|
34
|
-
target = to || configuration.default_channel || raise(ConfigurationError, "No target channel specified and no default_channel configured.")
|
|
35
|
-
response = slack_client.chat_postMessage(channel: target, blocks: content_data, thread_ts:, username: configuration.bot_name)
|
|
36
|
-
SentMessage.new(content: content_data, response:, client:)
|
|
37
|
-
end
|
|
33
|
+
def post(to: nil, thread_ts: nil) = MessageSender.new(message: self, client:, to:, thread_ts:).post
|
|
38
34
|
|
|
39
35
|
private
|
|
40
36
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module SlackLine
|
|
2
|
+
class MessageConverter
|
|
3
|
+
def initialize(blocks, client:)
|
|
4
|
+
@blocks = blocks
|
|
5
|
+
@client = client
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def convert = resolve_mentions(@blocks)
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_reader :client
|
|
13
|
+
|
|
14
|
+
def resolve_mentions(json)
|
|
15
|
+
case json
|
|
16
|
+
when Array then resolve_mentions_in_array(json)
|
|
17
|
+
when Hash then resolve_mentions_in_hash(json)
|
|
18
|
+
else json
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resolve_mentions_in_array(array)
|
|
23
|
+
array.map { |item| resolve_mentions(item) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def resolve_mentions_in_hash(hash)
|
|
27
|
+
text_key = hash.key?(:text) ? :text : "text"
|
|
28
|
+
type_key = hash.key?(:type) ? :type : "type"
|
|
29
|
+
if hash[type_key] == "mrkdwn" && hash[text_key].is_a?(String)
|
|
30
|
+
hash.merge(text_key => substitute_mentions(hash[text_key]))
|
|
31
|
+
else
|
|
32
|
+
hash.transform_values { |v| resolve_mentions(v) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def substitute_mentions(text)
|
|
37
|
+
text.gsub(/@([\w-]+)/) do |match|
|
|
38
|
+
token = $1
|
|
39
|
+
if (user = client.users.find(display_name: token))
|
|
40
|
+
"<@#{user.id}>"
|
|
41
|
+
elsif (group = client.groups.find(handle: token))
|
|
42
|
+
"<!subteam^#{group.id}>"
|
|
43
|
+
else
|
|
44
|
+
match
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module SlackLine
|
|
2
|
+
class MessageSender
|
|
3
|
+
extend Forwardable
|
|
4
|
+
include Memoization
|
|
5
|
+
|
|
6
|
+
def initialize(message:, client:, to: nil, thread_ts: nil)
|
|
7
|
+
@message = message
|
|
8
|
+
@client = client
|
|
9
|
+
@to = to
|
|
10
|
+
@thread_ts = thread_ts
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def post = SentMessage.new(content: content_data, response: api_response, client:)
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
memoize def supplied_target
|
|
18
|
+
to ||
|
|
19
|
+
configuration.default_channel ||
|
|
20
|
+
raise(ConfigurationError, "No target channel specified and no default_channel configured.")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
memoize def target
|
|
24
|
+
if supplied_target.start_with?("@")
|
|
25
|
+
name = supplied_target[1..]
|
|
26
|
+
client.users.find(display_name: name)&.id ||
|
|
27
|
+
raise(UserNotFoundError, "User with display name '#{name}' was not found.")
|
|
28
|
+
else
|
|
29
|
+
supplied_target
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
MAX_RETRIES = 2
|
|
34
|
+
|
|
35
|
+
memoize def api_response
|
|
36
|
+
if configuration.backoff
|
|
37
|
+
with_rate_limit_backoff { call_api }
|
|
38
|
+
else
|
|
39
|
+
call_api
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def call_api
|
|
44
|
+
slack_client.chat_postMessage(
|
|
45
|
+
channel: target,
|
|
46
|
+
blocks: content_data,
|
|
47
|
+
thread_ts: thread_ts,
|
|
48
|
+
username: configuration.bot_name
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_rate_limit_backoff
|
|
53
|
+
retries = 0
|
|
54
|
+
begin
|
|
55
|
+
yield
|
|
56
|
+
rescue Slack::Web::Api::Errors::TooManyRequestsError => e
|
|
57
|
+
raise if retries >= MAX_RETRIES
|
|
58
|
+
retries += 1
|
|
59
|
+
sleep(e.retry_after)
|
|
60
|
+
retry
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
attr_reader :message, :to, :thread_ts, :client
|
|
65
|
+
def_delegators :client, :configuration, :slack_client
|
|
66
|
+
def_delegators :message, :content
|
|
67
|
+
def_delegators :configuration, :look_up_users?
|
|
68
|
+
|
|
69
|
+
memoize def raw_content_data = content.as_json
|
|
70
|
+
|
|
71
|
+
memoize def converter = MessageConverter.new(raw_content_data, client:)
|
|
72
|
+
|
|
73
|
+
memoize def content_data = look_up_users? ? converter.convert : raw_content_data
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -12,11 +12,33 @@ module SlackLine
|
|
|
12
12
|
attr_reader :content, :priorly, :response
|
|
13
13
|
def_delegators :response, :ts, :channel
|
|
14
14
|
|
|
15
|
+
def thread_ts = response.thread_ts || ts
|
|
16
|
+
|
|
15
17
|
def inspect = "#<#{self.class} channel=#{channel.inspect} ts=#{ts.inspect}>"
|
|
16
18
|
|
|
19
|
+
def as_json
|
|
20
|
+
{"type" => "message", "ts" => ts, "channel" => channel,
|
|
21
|
+
"thread_ts" => response.thread_ts, "content" => content, "priorly" => priorly}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.from_json(data, client:)
|
|
25
|
+
raise ArgumentError, "Expected type 'message', got #{data["type"].inspect}" unless data["type"] == "message"
|
|
26
|
+
|
|
27
|
+
response_hash = {ts: data["ts"], channel: data["channel"]}
|
|
28
|
+
response_hash[:thread_ts] = data["thread_ts"] if data["thread_ts"]
|
|
29
|
+
response = Slack::Messages::Message.new(response_hash)
|
|
30
|
+
new(response:, client:, content: data["content"], priorly: data["priorly"])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def append(*text_or_blocks, &dsl_block)
|
|
34
|
+
appended = Thread.new(*text_or_blocks, client:, &dsl_block)
|
|
35
|
+
new_sent = appended.messages.map { |m| m.post(to: channel, thread_ts:) }
|
|
36
|
+
SentThread.new(self, *new_sent)
|
|
37
|
+
end
|
|
38
|
+
|
|
17
39
|
def update(*text_or_blocks, &dsl_block)
|
|
18
|
-
|
|
19
|
-
new_content =
|
|
40
|
+
replacement = replacement_message(*text_or_blocks, &dsl_block)
|
|
41
|
+
new_content = replacement.content.as_json
|
|
20
42
|
response = slack_client.chat_update(channel:, ts:, blocks: new_content)
|
|
21
43
|
SentMessage.new(content: new_content, priorly: content, response:, client:)
|
|
22
44
|
end
|
|
@@ -25,5 +47,11 @@ module SlackLine
|
|
|
25
47
|
|
|
26
48
|
attr_reader :client
|
|
27
49
|
def_delegators :client, :slack_client
|
|
50
|
+
|
|
51
|
+
def replacement_message(*text_or_blocks, &dsl_block)
|
|
52
|
+
return text_or_blocks.first if text_or_blocks.size == 1 && text_or_blocks.first.is_a?(Message)
|
|
53
|
+
|
|
54
|
+
Message.new(*text_or_blocks, client:, &dsl_block)
|
|
55
|
+
end
|
|
28
56
|
end
|
|
29
57
|
end
|
|
@@ -13,6 +13,21 @@ module SlackLine
|
|
|
13
13
|
def_delegators :first, :channel, :ts
|
|
14
14
|
alias_method :thread_ts, :ts
|
|
15
15
|
|
|
16
|
+
def append(*text_or_blocks, &dsl_block)
|
|
17
|
+
extended = first.append(*text_or_blocks, &dsl_block)
|
|
18
|
+
SentThread.new(*sent_messages, *extended.sent_messages[1..])
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
def inspect = "#<#{self.class} channel=#{channel.inspect} size=#{size} thread_ts=#{thread_ts.inspect}>"
|
|
22
|
+
|
|
23
|
+
def as_json
|
|
24
|
+
{"type" => "thread", "messages" => sent_messages.map(&:as_json)}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_json(data, client:)
|
|
28
|
+
raise ArgumentError, "Expected type 'thread', got #{data["type"].inspect}" unless data["type"] == "thread"
|
|
29
|
+
|
|
30
|
+
new(*data["messages"].map { |m| SentMessage.from_json(m, client:) })
|
|
31
|
+
end
|
|
17
32
|
end
|
|
18
33
|
end
|
data/lib/slack_line/thread.rb
CHANGED
|
@@ -20,12 +20,11 @@ module SlackLine
|
|
|
20
20
|
memoize def builder_urls = messages.map(&:builder_url)
|
|
21
21
|
|
|
22
22
|
def post(to: nil)
|
|
23
|
-
target = to || client.configuration.default_channel || raise(ConfigurationError, "No target channel specified and no default_channel configured.")
|
|
24
23
|
sent_messages = []
|
|
25
24
|
thread_ts = nil
|
|
26
25
|
|
|
27
26
|
messages.each do |message|
|
|
28
|
-
sent = message.post(to
|
|
27
|
+
sent = message.post(to:, thread_ts:)
|
|
29
28
|
thread_ts ||= sent.ts
|
|
30
29
|
sent_messages << sent
|
|
31
30
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module SlackLine
|
|
2
|
+
class Users
|
|
3
|
+
include Memoization
|
|
4
|
+
|
|
5
|
+
def initialize(slack_client:)
|
|
6
|
+
@slack_client = slack_client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
memoize def all = all_users.reject(&:deleted).reject(&:is_bot)
|
|
10
|
+
|
|
11
|
+
def find(display_name:)
|
|
12
|
+
users_by_display_name[display_name.downcase]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :slack_client
|
|
18
|
+
|
|
19
|
+
def fetch_page(cursor: nil)
|
|
20
|
+
params = {limit: 200}
|
|
21
|
+
params[:cursor] = cursor if cursor
|
|
22
|
+
|
|
23
|
+
response = slack_client.users_list(params)
|
|
24
|
+
users = response.members || []
|
|
25
|
+
next_cursor = response.dig("response_metadata", "next_cursor")
|
|
26
|
+
|
|
27
|
+
[users, next_cursor]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
memoize def all_users
|
|
31
|
+
all_users, cursor = fetch_page
|
|
32
|
+
|
|
33
|
+
while cursor
|
|
34
|
+
users, cursor = fetch_page(cursor:)
|
|
35
|
+
all_users.concat(users)
|
|
36
|
+
break if cursor.nil? || cursor.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
all_users
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
memoize def users_by_display_name = all.map { |u| [u.profile.display_name.downcase, u] }.to_h
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/slack_line/version.rb
CHANGED
data/lib/slack_line.rb
CHANGED
|
@@ -10,6 +10,7 @@ module SlackLine
|
|
|
10
10
|
Error = Class.new(StandardError)
|
|
11
11
|
ConfigurationError = Class.new(Error)
|
|
12
12
|
PostMessageError = Class.new(Error)
|
|
13
|
+
UserNotFoundError = Class.new(Error)
|
|
13
14
|
|
|
14
15
|
class << self
|
|
15
16
|
extend Forwardable
|
|
@@ -24,6 +25,14 @@ module SlackLine
|
|
|
24
25
|
memoize def client = Client.new(configuration)
|
|
25
26
|
|
|
26
27
|
def_delegators(:client, :message, :thread)
|
|
28
|
+
|
|
29
|
+
TYPES = {"message" => ->(data, client:) { SentMessage.from_json(data, client:) },
|
|
30
|
+
"thread" => ->(data, client:) { SentThread.from_json(data, client:) }}.freeze
|
|
31
|
+
|
|
32
|
+
def from_json(data, client:)
|
|
33
|
+
loader = TYPES[data["type"]] or raise(ArgumentError, "Unknown type #{data["type"].inspect}")
|
|
34
|
+
loader.call(data, client:)
|
|
35
|
+
end
|
|
27
36
|
end
|
|
28
37
|
end
|
|
29
38
|
|
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 = "
|
|
9
|
+
spec.summary = "A gem to send and extend Slack threads and messages"
|
|
10
10
|
spec.description = <<~DESC
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
and
|
|
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"
|
|
@@ -25,11 +25,12 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
26
|
`git ls-files -z`
|
|
27
27
|
.split("\x0")
|
|
28
|
-
.reject { |f| f.start_with?("spec") }
|
|
28
|
+
.reject { |f| f.start_with?("spec", "bin/qa") }
|
|
29
29
|
end
|
|
30
30
|
spec.executables = Dir.chdir(File.expand_path(__dir__)) do
|
|
31
31
|
`git ls-files -z bin/`
|
|
32
32
|
.split("\x0")
|
|
33
|
+
.reject { |f| f.start_with?("bin/qa") }
|
|
33
34
|
.map { |path| path.sub(/^bin\//, "") }
|
|
34
35
|
end
|
|
35
36
|
|
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:
|
|
4
|
+
version: '1.1'
|
|
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-03-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -165,10 +165,10 @@ dependencies:
|
|
|
165
165
|
- !ruby/object:Gem::Version
|
|
166
166
|
version: 3.1.0
|
|
167
167
|
description: |
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
and
|
|
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:
|
|
@@ -186,21 +186,29 @@ files:
|
|
|
186
186
|
- ".rspec"
|
|
187
187
|
- ".rubocop.yml"
|
|
188
188
|
- ".standard.yml"
|
|
189
|
+
- CHANGELOG.md
|
|
189
190
|
- Gemfile
|
|
190
191
|
- README.md
|
|
191
192
|
- bin/slack_line_message
|
|
192
193
|
- bin/slack_line_thread
|
|
193
194
|
- lib/slack_line.rb
|
|
195
|
+
- lib/slack_line/cli.rb
|
|
196
|
+
- lib/slack_line/cli/slack_line_message.rb
|
|
197
|
+
- lib/slack_line/cli/slack_line_thread.rb
|
|
194
198
|
- lib/slack_line/client.rb
|
|
195
199
|
- lib/slack_line/configuration.rb
|
|
200
|
+
- lib/slack_line/groups.rb
|
|
196
201
|
- lib/slack_line/memoization.rb
|
|
197
202
|
- lib/slack_line/message.rb
|
|
198
203
|
- lib/slack_line/message_context.rb
|
|
204
|
+
- lib/slack_line/message_converter.rb
|
|
205
|
+
- lib/slack_line/message_sender.rb
|
|
199
206
|
- lib/slack_line/section_context.rb
|
|
200
207
|
- lib/slack_line/sent_message.rb
|
|
201
208
|
- lib/slack_line/sent_thread.rb
|
|
202
209
|
- lib/slack_line/thread.rb
|
|
203
210
|
- lib/slack_line/thread_context.rb
|
|
211
|
+
- lib/slack_line/users.rb
|
|
204
212
|
- lib/slack_line/version.rb
|
|
205
213
|
- slack_line.gemspec
|
|
206
214
|
homepage: https://github.com/nevinera/slack_line
|
|
@@ -227,5 +235,5 @@ requirements: []
|
|
|
227
235
|
rubygems_version: 3.5.22
|
|
228
236
|
signing_key:
|
|
229
237
|
specification_version: 4
|
|
230
|
-
summary:
|
|
238
|
+
summary: A gem to send and extend Slack threads and messages
|
|
231
239
|
test_files: []
|