slack_line 0.1.0 → 1.0
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/README.md +11 -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 +2 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5012be14f249f312a75b1d5cca149fd938eb2d6b66db4b51fb93789b3a4030e9
|
|
4
|
+
data.tar.gz: 6a4244475ce9b64db464f391d6dbfa0c8d6a1dc509854b1b05a6bc1149ad78de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bb9462733e8c15e292606d303c04d2c72855d9d141ebfd917b9f2bf415b964aad0dd34fd0832c1a74064f551272da0dfce670ae43047828ee0c89a8fd86f6c5
|
|
7
|
+
data.tar.gz: 455afd3746d46319fe637e924034655cfa9f42da2c06d8c30a3f578ed70e2245811f6f7a5404d37939360cdb47309a966485d8151ec41e578724fb494c222a18
|
data/README.md
CHANGED
|
@@ -121,3 +121,14 @@ 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 these permissions:
|
|
128
|
+
|
|
129
|
+
* `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).
|
|
132
|
+
* `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)
|
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)
|
|
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
|
@@ -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.0'
|
|
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
|
|
@@ -191,16 +191,23 @@ files:
|
|
|
191
191
|
- bin/slack_line_message
|
|
192
192
|
- bin/slack_line_thread
|
|
193
193
|
- lib/slack_line.rb
|
|
194
|
+
- lib/slack_line/cli.rb
|
|
195
|
+
- lib/slack_line/cli/slack_line_message.rb
|
|
196
|
+
- lib/slack_line/cli/slack_line_thread.rb
|
|
194
197
|
- lib/slack_line/client.rb
|
|
195
198
|
- lib/slack_line/configuration.rb
|
|
199
|
+
- lib/slack_line/groups.rb
|
|
196
200
|
- lib/slack_line/memoization.rb
|
|
197
201
|
- lib/slack_line/message.rb
|
|
198
202
|
- lib/slack_line/message_context.rb
|
|
203
|
+
- lib/slack_line/message_converter.rb
|
|
204
|
+
- lib/slack_line/message_sender.rb
|
|
199
205
|
- lib/slack_line/section_context.rb
|
|
200
206
|
- lib/slack_line/sent_message.rb
|
|
201
207
|
- lib/slack_line/sent_thread.rb
|
|
202
208
|
- lib/slack_line/thread.rb
|
|
203
209
|
- lib/slack_line/thread_context.rb
|
|
210
|
+
- lib/slack_line/users.rb
|
|
204
211
|
- lib/slack_line/version.rb
|
|
205
212
|
- slack_line.gemspec
|
|
206
213
|
homepage: https://github.com/nevinera/slack_line
|