slk 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 +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/bin/slk +7 -0
- data/lib/slack_cli/api/activity.rb +28 -0
- data/lib/slack_cli/api/bots.rb +32 -0
- data/lib/slack_cli/api/client.rb +49 -0
- data/lib/slack_cli/api/conversations.rb +52 -0
- data/lib/slack_cli/api/dnd.rb +40 -0
- data/lib/slack_cli/api/emoji.rb +21 -0
- data/lib/slack_cli/api/threads.rb +44 -0
- data/lib/slack_cli/api/usergroups.rb +25 -0
- data/lib/slack_cli/api/users.rb +101 -0
- data/lib/slack_cli/cli.rb +118 -0
- data/lib/slack_cli/commands/activity.rb +292 -0
- data/lib/slack_cli/commands/base.rb +175 -0
- data/lib/slack_cli/commands/cache.rb +116 -0
- data/lib/slack_cli/commands/catchup.rb +484 -0
- data/lib/slack_cli/commands/config.rb +159 -0
- data/lib/slack_cli/commands/dnd.rb +143 -0
- data/lib/slack_cli/commands/emoji.rb +412 -0
- data/lib/slack_cli/commands/help.rb +76 -0
- data/lib/slack_cli/commands/messages.rb +317 -0
- data/lib/slack_cli/commands/presence.rb +107 -0
- data/lib/slack_cli/commands/preset.rb +239 -0
- data/lib/slack_cli/commands/status.rb +194 -0
- data/lib/slack_cli/commands/thread.rb +62 -0
- data/lib/slack_cli/commands/unread.rb +312 -0
- data/lib/slack_cli/commands/workspaces.rb +151 -0
- data/lib/slack_cli/formatters/duration_formatter.rb +28 -0
- data/lib/slack_cli/formatters/emoji_replacer.rb +143 -0
- data/lib/slack_cli/formatters/mention_replacer.rb +154 -0
- data/lib/slack_cli/formatters/message_formatter.rb +429 -0
- data/lib/slack_cli/formatters/output.rb +89 -0
- data/lib/slack_cli/models/channel.rb +52 -0
- data/lib/slack_cli/models/duration.rb +85 -0
- data/lib/slack_cli/models/message.rb +217 -0
- data/lib/slack_cli/models/preset.rb +73 -0
- data/lib/slack_cli/models/reaction.rb +54 -0
- data/lib/slack_cli/models/status.rb +57 -0
- data/lib/slack_cli/models/user.rb +56 -0
- data/lib/slack_cli/models/workspace.rb +52 -0
- data/lib/slack_cli/runner.rb +123 -0
- data/lib/slack_cli/services/api_client.rb +149 -0
- data/lib/slack_cli/services/cache_store.rb +198 -0
- data/lib/slack_cli/services/configuration.rb +74 -0
- data/lib/slack_cli/services/encryption.rb +51 -0
- data/lib/slack_cli/services/preset_store.rb +112 -0
- data/lib/slack_cli/services/reaction_enricher.rb +87 -0
- data/lib/slack_cli/services/token_store.rb +117 -0
- data/lib/slack_cli/support/error_logger.rb +28 -0
- data/lib/slack_cli/support/help_formatter.rb +139 -0
- data/lib/slack_cli/support/inline_images.rb +62 -0
- data/lib/slack_cli/support/slack_url_parser.rb +78 -0
- data/lib/slack_cli/support/user_resolver.rb +114 -0
- data/lib/slack_cli/support/xdg_paths.rb +37 -0
- data/lib/slack_cli/version.rb +5 -0
- data/lib/slack_cli.rb +91 -0
- metadata +103 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
class CLI
|
|
5
|
+
COMMANDS = {
|
|
6
|
+
"status" => Commands::Status,
|
|
7
|
+
"presence" => Commands::Presence,
|
|
8
|
+
"dnd" => Commands::Dnd,
|
|
9
|
+
"messages" => Commands::Messages,
|
|
10
|
+
"thread" => Commands::Thread,
|
|
11
|
+
"unread" => Commands::Unread,
|
|
12
|
+
"catchup" => Commands::Catchup,
|
|
13
|
+
"activity" => Commands::Activity,
|
|
14
|
+
"preset" => Commands::Preset,
|
|
15
|
+
"workspaces" => Commands::Workspaces,
|
|
16
|
+
"cache" => Commands::Cache,
|
|
17
|
+
"emoji" => Commands::Emoji,
|
|
18
|
+
"config" => Commands::Config,
|
|
19
|
+
"help" => Commands::Help
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(argv, output: nil)
|
|
23
|
+
@argv = argv.dup
|
|
24
|
+
@output = output || Formatters::Output.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
command_name, *args = @argv
|
|
29
|
+
|
|
30
|
+
# Handle version flags
|
|
31
|
+
if command_name.nil? || command_name == "--help" || command_name == "-h"
|
|
32
|
+
return run_command("help", [])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if command_name == "--version" || command_name == "-V" || command_name == "version"
|
|
36
|
+
@output.puts "slk v#{VERSION}"
|
|
37
|
+
return 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Look up command
|
|
41
|
+
if (command_class = COMMANDS[command_name])
|
|
42
|
+
run_command(command_name, args)
|
|
43
|
+
elsif preset_exists?(command_name)
|
|
44
|
+
# Treat as preset shortcut
|
|
45
|
+
run_command("preset", [command_name] + args)
|
|
46
|
+
else
|
|
47
|
+
@output.error("Unknown command: #{command_name}")
|
|
48
|
+
@output.puts
|
|
49
|
+
@output.puts "Run 'slk help' for available commands."
|
|
50
|
+
1
|
|
51
|
+
end
|
|
52
|
+
rescue ConfigError => e
|
|
53
|
+
@output.error(e.message)
|
|
54
|
+
log_error(e)
|
|
55
|
+
1
|
|
56
|
+
rescue EncryptionError => e
|
|
57
|
+
@output.error("Encryption error: #{e.message}")
|
|
58
|
+
log_error(e)
|
|
59
|
+
1
|
|
60
|
+
rescue ApiError => e
|
|
61
|
+
@output.error("API error: #{e.message}")
|
|
62
|
+
log_error(e)
|
|
63
|
+
1
|
|
64
|
+
rescue Interrupt
|
|
65
|
+
@output.puts
|
|
66
|
+
@output.puts "Interrupted."
|
|
67
|
+
130
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
@output.error("Unexpected error: #{e.message}")
|
|
70
|
+
log_path = log_error(e)
|
|
71
|
+
@output.puts "Details logged to: #{log_path}" if log_path
|
|
72
|
+
1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def run_command(name, args)
|
|
78
|
+
command_class = COMMANDS[name]
|
|
79
|
+
return 1 unless command_class
|
|
80
|
+
|
|
81
|
+
verbose = args.include?("-v") || args.include?("--verbose")
|
|
82
|
+
|
|
83
|
+
# Create output with verbose flag
|
|
84
|
+
output = Formatters::Output.new(verbose: verbose)
|
|
85
|
+
runner = Runner.new(output: output)
|
|
86
|
+
|
|
87
|
+
# Set up API call logging if verbose
|
|
88
|
+
if verbose
|
|
89
|
+
runner.api_client.on_request = ->(method, count) {
|
|
90
|
+
output.debug("[API ##{count}] #{method}")
|
|
91
|
+
}
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
command = command_class.new(args, runner: runner)
|
|
95
|
+
result = command.execute
|
|
96
|
+
|
|
97
|
+
# Show API call count if verbose
|
|
98
|
+
if verbose && runner.api_client.call_count > 0
|
|
99
|
+
output.debug("Total API calls: #{runner.api_client.call_count}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
result
|
|
103
|
+
ensure
|
|
104
|
+
# Clean up HTTP connections
|
|
105
|
+
runner&.api_client&.close
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def preset_exists?(name)
|
|
109
|
+
# PresetStore handles JSON parse errors internally via on_warning callback
|
|
110
|
+
# ConfigError should propagate as it indicates a real configuration problem
|
|
111
|
+
Services::PresetStore.new.exists?(name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def log_error(error)
|
|
115
|
+
Support::ErrorLogger.log(error)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module SlackCli
|
|
6
|
+
module Commands
|
|
7
|
+
class Activity < Base
|
|
8
|
+
def execute
|
|
9
|
+
result = validate_options
|
|
10
|
+
return result if result
|
|
11
|
+
|
|
12
|
+
workspace = target_workspaces.first
|
|
13
|
+
api = runner.activity_api(workspace.name)
|
|
14
|
+
|
|
15
|
+
response = api.feed(limit: @options[:limit], types: activity_types)
|
|
16
|
+
|
|
17
|
+
unless response['ok']
|
|
18
|
+
error("Failed to fetch activity: #{response['error']}")
|
|
19
|
+
return 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
items = response['items'] || []
|
|
23
|
+
|
|
24
|
+
if @options[:json]
|
|
25
|
+
output_json(items)
|
|
26
|
+
else
|
|
27
|
+
display_activity(items, workspace)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
0
|
|
31
|
+
rescue ApiError => e
|
|
32
|
+
error("Failed to fetch activity: #{e.message}")
|
|
33
|
+
1
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
protected
|
|
37
|
+
|
|
38
|
+
def default_options
|
|
39
|
+
super.merge(
|
|
40
|
+
limit: 20,
|
|
41
|
+
filter: :all,
|
|
42
|
+
show_messages: false
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def handle_option(arg, args, remaining)
|
|
47
|
+
case arg
|
|
48
|
+
when '-n', '--limit'
|
|
49
|
+
@options[:limit] = args.shift.to_i
|
|
50
|
+
when '--reactions'
|
|
51
|
+
@options[:filter] = :reactions
|
|
52
|
+
when '--mentions'
|
|
53
|
+
@options[:filter] = :mentions
|
|
54
|
+
when '--threads'
|
|
55
|
+
@options[:filter] = :threads
|
|
56
|
+
when '--show-messages', '-m'
|
|
57
|
+
@options[:show_messages] = true
|
|
58
|
+
else
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def help_text
|
|
64
|
+
help = Support::HelpFormatter.new('slk activity [options]')
|
|
65
|
+
help.description('Show recent activity from the activity feed.')
|
|
66
|
+
|
|
67
|
+
help.section('OPTIONS') do |s|
|
|
68
|
+
s.option('-n, --limit N', 'Number of items (default: 20, max: 50)')
|
|
69
|
+
s.option('--reactions', 'Show only reaction activity')
|
|
70
|
+
s.option('--mentions', 'Show only mentions')
|
|
71
|
+
s.option('--threads', 'Show only thread replies')
|
|
72
|
+
s.option('-m, --show-messages', 'Show the message content for each activity')
|
|
73
|
+
s.option('--json', 'Output as JSON')
|
|
74
|
+
s.option('-w, --workspace', 'Specify workspace')
|
|
75
|
+
s.option('-v, --verbose', 'Show debug information')
|
|
76
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
help.render
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def activity_types
|
|
85
|
+
case @options[:filter]
|
|
86
|
+
when :reactions
|
|
87
|
+
'message_reaction'
|
|
88
|
+
when :mentions
|
|
89
|
+
'at_user,at_user_group,at_channel,at_everyone'
|
|
90
|
+
when :threads
|
|
91
|
+
'thread_v2'
|
|
92
|
+
else
|
|
93
|
+
# All activity types that the Slack web UI uses
|
|
94
|
+
'thread_v2,message_reaction,bot_dm_bundle,at_user,at_user_group,at_channel,at_everyone'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def display_activity(items, workspace)
|
|
99
|
+
return puts 'No activity found.' if items.empty?
|
|
100
|
+
|
|
101
|
+
items.each do |item|
|
|
102
|
+
display_activity_item(item, workspace)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def display_activity_item(item, workspace)
|
|
107
|
+
type = item.dig('item', 'type')
|
|
108
|
+
timestamp = format_activity_time(item['feed_ts'])
|
|
109
|
+
|
|
110
|
+
case type
|
|
111
|
+
when 'message_reaction'
|
|
112
|
+
display_reaction_activity(item, workspace, timestamp)
|
|
113
|
+
when 'at_user', 'at_user_group', 'at_channel', 'at_everyone'
|
|
114
|
+
display_mention_activity(item, workspace, timestamp)
|
|
115
|
+
when 'thread_v2'
|
|
116
|
+
display_thread_v2_activity(item, workspace, timestamp)
|
|
117
|
+
when 'bot_dm_bundle'
|
|
118
|
+
display_bot_dm_activity(item, workspace, timestamp)
|
|
119
|
+
else
|
|
120
|
+
# Unknown activity type - skip silently
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def display_reaction_activity(item, workspace, timestamp)
|
|
125
|
+
reaction_data = item.dig('item', 'reaction')
|
|
126
|
+
message_data = item.dig('item', 'message')
|
|
127
|
+
return unless reaction_data && message_data
|
|
128
|
+
|
|
129
|
+
user_id = reaction_data['user']
|
|
130
|
+
username = resolve_user(workspace, user_id)
|
|
131
|
+
emoji_name = reaction_data['name']
|
|
132
|
+
emoji = runner.emoji_replacer.lookup_emoji(emoji_name) || ":#{emoji_name}:"
|
|
133
|
+
channel_id = message_data['channel']
|
|
134
|
+
channel = resolve_channel(workspace, channel_id)
|
|
135
|
+
|
|
136
|
+
puts "#{output.blue(timestamp)} #{output.bold(username)} reacted #{emoji} in #{channel}"
|
|
137
|
+
|
|
138
|
+
if @options[:show_messages]
|
|
139
|
+
message = fetch_message(workspace, channel_id, message_data['ts'])
|
|
140
|
+
display_message_preview(message, workspace)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def display_mention_activity(item, workspace, timestamp)
|
|
145
|
+
message_data = item.dig('item', 'message')
|
|
146
|
+
return unless message_data
|
|
147
|
+
|
|
148
|
+
user_id = message_data['author_user_id'] || message_data['user']
|
|
149
|
+
username = resolve_user(workspace, user_id)
|
|
150
|
+
channel_id = message_data['channel']
|
|
151
|
+
channel = resolve_channel(workspace, channel_id)
|
|
152
|
+
|
|
153
|
+
puts "#{output.blue(timestamp)} #{output.bold(username)} mentioned you in #{channel}"
|
|
154
|
+
|
|
155
|
+
if @options[:show_messages]
|
|
156
|
+
message = fetch_message(workspace, channel_id, message_data['ts'])
|
|
157
|
+
display_message_preview(message, workspace)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def display_thread_v2_activity(item, workspace, timestamp)
|
|
162
|
+
thread_entry = item.dig('item', 'bundle_info', 'payload', 'thread_entry')
|
|
163
|
+
return unless thread_entry
|
|
164
|
+
|
|
165
|
+
channel_id = thread_entry['channel_id']
|
|
166
|
+
channel = resolve_channel(workspace, channel_id)
|
|
167
|
+
|
|
168
|
+
puts "#{output.blue(timestamp)} Thread activity in #{channel}"
|
|
169
|
+
|
|
170
|
+
if @options[:show_messages] && thread_entry['thread_ts']
|
|
171
|
+
# Fetch the thread parent message
|
|
172
|
+
message = fetch_message(workspace, channel_id, thread_entry['thread_ts'])
|
|
173
|
+
display_message_preview(message, workspace)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def display_bot_dm_activity(item, workspace, timestamp)
|
|
178
|
+
message_data = item.dig('item', 'bundle_info', 'payload', 'message')
|
|
179
|
+
return unless message_data
|
|
180
|
+
|
|
181
|
+
channel_id = message_data['channel']
|
|
182
|
+
message_ts = message_data['ts']
|
|
183
|
+
channel = resolve_channel(workspace, channel_id)
|
|
184
|
+
|
|
185
|
+
puts "#{output.blue(timestamp)} Bot message in #{channel}"
|
|
186
|
+
|
|
187
|
+
# Always try to fetch and show the message content (or when --show-messages is enabled)
|
|
188
|
+
if @options[:show_messages]
|
|
189
|
+
message = fetch_message(workspace, channel_id, message_ts)
|
|
190
|
+
display_message_preview(message, workspace) if message
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_user(workspace, user_id)
|
|
195
|
+
# Try cache first
|
|
196
|
+
cached = cache_store.get_user(workspace.name, user_id)
|
|
197
|
+
return cached if cached
|
|
198
|
+
|
|
199
|
+
# Fall back to user ID
|
|
200
|
+
user_id
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def resolve_channel(workspace, channel_id)
|
|
204
|
+
# DMs and Group DMs - don't try to resolve
|
|
205
|
+
return 'DM' if channel_id.start_with?('D')
|
|
206
|
+
return 'Group DM' if channel_id.start_with?('G')
|
|
207
|
+
|
|
208
|
+
# Try cache first
|
|
209
|
+
cached = cache_store.get_channel_name(workspace.name, channel_id)
|
|
210
|
+
return "##{cached}" if cached
|
|
211
|
+
|
|
212
|
+
# Try to fetch from API
|
|
213
|
+
begin
|
|
214
|
+
api = runner.conversations_api(workspace.name)
|
|
215
|
+
response = api.info(channel: channel_id)
|
|
216
|
+
if response['ok'] && response['channel']
|
|
217
|
+
name = response['channel']['name']
|
|
218
|
+
cache_store.set_channel(workspace.name, name, channel_id)
|
|
219
|
+
return "##{name}"
|
|
220
|
+
end
|
|
221
|
+
rescue ApiError
|
|
222
|
+
# Fall back to channel ID if API call fails
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Fall back to channel ID
|
|
226
|
+
channel_id
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def fetch_message(workspace, channel_id, message_ts)
|
|
230
|
+
api = runner.conversations_api(workspace.name)
|
|
231
|
+
# Fetch a window of messages around the target timestamp
|
|
232
|
+
# Use oldest (exclusive) and latest (inclusive) to create a window
|
|
233
|
+
oldest_ts = (message_ts.to_f - 1).to_s # 1 second before
|
|
234
|
+
latest_ts = (message_ts.to_f + 1).to_s # 1 second after
|
|
235
|
+
|
|
236
|
+
response = api.history(
|
|
237
|
+
channel: channel_id,
|
|
238
|
+
limit: 10,
|
|
239
|
+
oldest: oldest_ts,
|
|
240
|
+
latest: latest_ts
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
return nil unless response['ok'] && response['messages']&.any?
|
|
244
|
+
|
|
245
|
+
# Find the exact message by timestamp
|
|
246
|
+
response['messages'].find { |msg| msg['ts'] == message_ts }
|
|
247
|
+
rescue ApiError
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def display_message_preview(message, workspace)
|
|
252
|
+
return unless message
|
|
253
|
+
|
|
254
|
+
# Get username
|
|
255
|
+
username = if message['user']
|
|
256
|
+
resolve_user(workspace, message['user'])
|
|
257
|
+
elsif message['bot_id']
|
|
258
|
+
'Bot'
|
|
259
|
+
else
|
|
260
|
+
'Unknown'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Get text and replace mentions
|
|
264
|
+
text = message['text'] || ''
|
|
265
|
+
text = '[No text]' if text.empty?
|
|
266
|
+
text = runner.mention_replacer.replace(text, workspace) unless text == '[No text]'
|
|
267
|
+
|
|
268
|
+
# Format as indented preview
|
|
269
|
+
lines = text.lines
|
|
270
|
+
first_line = lines.first&.strip || text
|
|
271
|
+
first_line = "#{first_line[0..100]}..." if first_line.length > 100
|
|
272
|
+
|
|
273
|
+
puts " └─ #{username}: #{first_line}"
|
|
274
|
+
|
|
275
|
+
# Show additional lines if any
|
|
276
|
+
if lines.length > 1
|
|
277
|
+
remaining = lines[1..2].map(&:strip).reject(&:empty?)
|
|
278
|
+
remaining.each do |line|
|
|
279
|
+
line = "#{line[0..100]}..." if line.length > 100
|
|
280
|
+
puts " #{line}"
|
|
281
|
+
end
|
|
282
|
+
puts " [#{lines.length - 3} more lines...]" if lines.length > 3
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def format_activity_time(slack_timestamp)
|
|
287
|
+
time = Time.at(slack_timestamp.to_f)
|
|
288
|
+
time.strftime('%b %d %-I:%M %p') # e.g., "Jan 13 2:45 PM"
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :runner, :options, :positional_args
|
|
7
|
+
|
|
8
|
+
def initialize(args, runner:)
|
|
9
|
+
@runner = runner
|
|
10
|
+
@options = default_options
|
|
11
|
+
@positional_args = parse_options(args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute
|
|
15
|
+
raise NotImplementedError, "Subclass must implement #execute"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
protected
|
|
19
|
+
|
|
20
|
+
# Convenience accessors
|
|
21
|
+
def output = runner.output
|
|
22
|
+
def config = runner.config
|
|
23
|
+
def cache_store = runner.cache_store
|
|
24
|
+
def preset_store = runner.preset_store
|
|
25
|
+
def token_store = runner.token_store
|
|
26
|
+
def api_client = runner.api_client
|
|
27
|
+
|
|
28
|
+
def default_options
|
|
29
|
+
{
|
|
30
|
+
workspace: nil,
|
|
31
|
+
all: false,
|
|
32
|
+
verbose: false,
|
|
33
|
+
quiet: false,
|
|
34
|
+
json: false,
|
|
35
|
+
width: default_width
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Default wrap width: 72 for interactive terminals, nil (no wrap) otherwise
|
|
40
|
+
def default_width
|
|
41
|
+
$stdout.tty? ? 72 : nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_options(args)
|
|
45
|
+
remaining = []
|
|
46
|
+
args = args.dup
|
|
47
|
+
@unknown_options = []
|
|
48
|
+
|
|
49
|
+
while args.any?
|
|
50
|
+
arg = args.shift
|
|
51
|
+
|
|
52
|
+
case arg
|
|
53
|
+
when "-w", "--workspace"
|
|
54
|
+
@options[:workspace] = args.shift
|
|
55
|
+
when "--width"
|
|
56
|
+
value = args.shift
|
|
57
|
+
@options[:width] = value == '0' ? nil : value.to_i
|
|
58
|
+
when "--no-wrap"
|
|
59
|
+
@options[:width] = nil
|
|
60
|
+
when "--all"
|
|
61
|
+
@options[:all] = true
|
|
62
|
+
when "-v", "--verbose"
|
|
63
|
+
@options[:verbose] = true
|
|
64
|
+
when "-q", "--quiet"
|
|
65
|
+
@options[:quiet] = true
|
|
66
|
+
when "--json"
|
|
67
|
+
@options[:json] = true
|
|
68
|
+
when "-h", "--help"
|
|
69
|
+
@options[:help] = true
|
|
70
|
+
when /^-/
|
|
71
|
+
# Let subclass handle unknown options
|
|
72
|
+
handle_option(arg, args, remaining)
|
|
73
|
+
else
|
|
74
|
+
remaining << arg
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
remaining
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Override in subclass to handle command-specific options
|
|
82
|
+
# Return true if option was handled, false to raise unknown option error
|
|
83
|
+
def handle_option(arg, args, remaining)
|
|
84
|
+
# By default, unknown options are errors
|
|
85
|
+
# Subclasses can override and return true to accept the option,
|
|
86
|
+
# or call super to get this error behavior
|
|
87
|
+
@unknown_options ||= []
|
|
88
|
+
@unknown_options << arg
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check for unknown options and return error code if any were passed
|
|
93
|
+
def check_unknown_options
|
|
94
|
+
return nil if @unknown_options.nil? || @unknown_options.empty?
|
|
95
|
+
|
|
96
|
+
error("Unknown option: #{@unknown_options.first}")
|
|
97
|
+
error("Run with --help for available options.")
|
|
98
|
+
1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns true if there are unknown options
|
|
102
|
+
def has_unknown_options?
|
|
103
|
+
@unknown_options && @unknown_options.any?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get workspaces to operate on based on options
|
|
107
|
+
def target_workspaces
|
|
108
|
+
if @options[:all]
|
|
109
|
+
runner.all_workspaces
|
|
110
|
+
elsif @options[:workspace]
|
|
111
|
+
[runner.workspace(@options[:workspace])]
|
|
112
|
+
else
|
|
113
|
+
[runner.workspace]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Show help if requested
|
|
118
|
+
def show_help?
|
|
119
|
+
@options[:help]
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def show_help
|
|
123
|
+
output.puts help_text
|
|
124
|
+
0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Call at start of execute to check for help flag and unknown options
|
|
128
|
+
# Returns exit code if should return early, nil otherwise
|
|
129
|
+
def validate_options
|
|
130
|
+
return show_help if show_help?
|
|
131
|
+
return check_unknown_options if has_unknown_options?
|
|
132
|
+
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def help_text
|
|
137
|
+
"No help available for this command."
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Output helpers
|
|
141
|
+
def success(message)
|
|
142
|
+
output.success(message) unless @options[:quiet]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def info(message)
|
|
146
|
+
output.info(message) unless @options[:quiet]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def warn(message)
|
|
150
|
+
output.warn(message)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def error(message)
|
|
154
|
+
output.error(message)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def debug(message)
|
|
158
|
+
output.debug(message) if @options[:verbose]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def puts(message = "")
|
|
162
|
+
output.puts(message) unless @options[:quiet]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def print(message)
|
|
166
|
+
output.print(message) unless @options[:quiet]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# JSON output helper
|
|
170
|
+
def output_json(data)
|
|
171
|
+
output.puts(JSON.pretty_generate(data))
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/help_formatter"
|
|
4
|
+
|
|
5
|
+
module SlackCli
|
|
6
|
+
module Commands
|
|
7
|
+
class Cache < Base
|
|
8
|
+
def execute
|
|
9
|
+
result = validate_options
|
|
10
|
+
return result if result
|
|
11
|
+
|
|
12
|
+
case positional_args
|
|
13
|
+
in ["status" | "info"] | []
|
|
14
|
+
show_status
|
|
15
|
+
in ["clear", *rest]
|
|
16
|
+
clear_cache(rest.first)
|
|
17
|
+
in ["populate" | "refresh", *rest]
|
|
18
|
+
populate_cache(rest.first)
|
|
19
|
+
else
|
|
20
|
+
error("Unknown action: #{positional_args.first}")
|
|
21
|
+
1
|
|
22
|
+
end
|
|
23
|
+
rescue ApiError => e
|
|
24
|
+
error("Failed: #{e.message}")
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
def help_text
|
|
31
|
+
help = Support::HelpFormatter.new("slk cache <action> [workspace]")
|
|
32
|
+
help.description("Manage user and channel cache.")
|
|
33
|
+
|
|
34
|
+
help.section("ACTIONS") do |s|
|
|
35
|
+
s.action("status", "Show cache status")
|
|
36
|
+
s.action("clear [ws]", "Clear cache (all or specific workspace)")
|
|
37
|
+
s.action("populate [ws]", "Populate user cache from API")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
help.section("OPTIONS") do |s|
|
|
41
|
+
s.option("-w, --workspace", "Specify workspace")
|
|
42
|
+
s.option("-q, --quiet", "Suppress output")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
help.render
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def show_status
|
|
51
|
+
target_workspaces.each do |workspace|
|
|
52
|
+
if target_workspaces.size > 1
|
|
53
|
+
puts output.bold(workspace.name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
user_count = cache_store.user_cache_size(workspace.name)
|
|
57
|
+
channel_count = cache_store.channel_cache_size(workspace.name)
|
|
58
|
+
|
|
59
|
+
puts " Users cached: #{user_count}"
|
|
60
|
+
puts " Channels cached: #{channel_count}"
|
|
61
|
+
|
|
62
|
+
if cache_store.user_cache_file_exists?(workspace.name)
|
|
63
|
+
puts " User cache: #{output.green("present")}"
|
|
64
|
+
else
|
|
65
|
+
puts " User cache: #{output.yellow("not populated")}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
0
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def clear_cache(workspace_name)
|
|
73
|
+
if workspace_name
|
|
74
|
+
cache_store.clear_user_cache(workspace_name)
|
|
75
|
+
cache_store.clear_channel_cache(workspace_name)
|
|
76
|
+
success("Cleared cache for #{workspace_name}")
|
|
77
|
+
else
|
|
78
|
+
cache_store.clear_user_cache
|
|
79
|
+
cache_store.clear_channel_cache
|
|
80
|
+
success("Cleared all caches")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def populate_cache(workspace_name)
|
|
87
|
+
workspaces = workspace_name ? [runner.workspace(workspace_name)] : target_workspaces
|
|
88
|
+
|
|
89
|
+
workspaces.each do |workspace|
|
|
90
|
+
puts "Populating user cache for #{workspace.name}..."
|
|
91
|
+
|
|
92
|
+
api = runner.users_api(workspace.name)
|
|
93
|
+
all_users = []
|
|
94
|
+
cursor = nil
|
|
95
|
+
|
|
96
|
+
loop do
|
|
97
|
+
response = api.list(cursor: cursor)
|
|
98
|
+
members = response["members"] || []
|
|
99
|
+
all_users.concat(members.map { |m| Models::User.from_api(m) })
|
|
100
|
+
|
|
101
|
+
cursor = response.dig("response_metadata", "next_cursor")
|
|
102
|
+
break if cursor.nil? || cursor.empty?
|
|
103
|
+
|
|
104
|
+
print "."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
count = cache_store.populate_user_cache(workspace.name, all_users)
|
|
108
|
+
puts
|
|
109
|
+
success("Cached #{count} users for #{workspace.name}")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
0
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|