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,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/help_formatter"
|
|
4
|
+
|
|
5
|
+
module SlackCli
|
|
6
|
+
module Commands
|
|
7
|
+
class Dnd < Base
|
|
8
|
+
def execute
|
|
9
|
+
result = validate_options
|
|
10
|
+
return result if result
|
|
11
|
+
|
|
12
|
+
case positional_args
|
|
13
|
+
in ["status" | "info"]
|
|
14
|
+
get_status
|
|
15
|
+
in ["on" | "snooze", *rest]
|
|
16
|
+
duration = rest.first ? Models::Duration.parse(rest.first) : Models::Duration.parse("1h")
|
|
17
|
+
set_snooze(duration)
|
|
18
|
+
in ["off" | "end"]
|
|
19
|
+
end_snooze
|
|
20
|
+
in [duration_str] if duration_str.match?(/^\d+[hms]?$/)
|
|
21
|
+
duration = Models::Duration.parse(duration_str)
|
|
22
|
+
set_snooze(duration)
|
|
23
|
+
in []
|
|
24
|
+
get_status
|
|
25
|
+
else
|
|
26
|
+
error("Unknown action: #{positional_args.first}")
|
|
27
|
+
error("Valid actions: status, on, off, or a duration (e.g., 1h)")
|
|
28
|
+
1
|
|
29
|
+
end
|
|
30
|
+
rescue ArgumentError => e
|
|
31
|
+
error("Invalid duration: #{e.message}")
|
|
32
|
+
1
|
|
33
|
+
rescue ApiError => e
|
|
34
|
+
error("Failed: #{e.message}")
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
def help_text
|
|
41
|
+
help = Support::HelpFormatter.new("slk dnd [action] [duration]")
|
|
42
|
+
help.description("Manage Do Not Disturb (snooze) settings.")
|
|
43
|
+
help.note("GET shows all workspaces by default. SET applies to primary only.")
|
|
44
|
+
|
|
45
|
+
help.section("ACTIONS") do |s|
|
|
46
|
+
s.action("(none)", "Show current DND status (all workspaces)")
|
|
47
|
+
s.action("status", "Show current DND status")
|
|
48
|
+
s.action("on [duration]", "Enable snooze (default: 1h)")
|
|
49
|
+
s.action("off", "Disable snooze")
|
|
50
|
+
s.action("<duration>", "Enable snooze for specified duration")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
help.section("DURATION FORMAT") do |s|
|
|
54
|
+
s.item("1h", "1 hour")
|
|
55
|
+
s.item("30m", "30 minutes")
|
|
56
|
+
s.item("1h30m", "1 hour 30 minutes")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
help.section("OPTIONS") do |s|
|
|
60
|
+
s.option("-w, --workspace", "Limit to specific workspace")
|
|
61
|
+
s.option("--all", "Set across all workspaces")
|
|
62
|
+
s.option("-q, --quiet", "Suppress output")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
help.render
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def get_status
|
|
71
|
+
# GET defaults to all workspaces unless -w specified
|
|
72
|
+
workspaces = @options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
|
|
73
|
+
|
|
74
|
+
workspaces.each do |workspace|
|
|
75
|
+
api = runner.dnd_api(workspace.name)
|
|
76
|
+
data = api.info
|
|
77
|
+
|
|
78
|
+
if workspaces.size > 1
|
|
79
|
+
puts output.bold(workspace.name)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if data["snooze_enabled"]
|
|
83
|
+
remaining = api.snooze_remaining
|
|
84
|
+
if remaining
|
|
85
|
+
puts " DND: #{output.yellow("snoozing")} (#{remaining} remaining)"
|
|
86
|
+
else
|
|
87
|
+
puts " DND: #{output.yellow("snoozing")} (expired)"
|
|
88
|
+
end
|
|
89
|
+
else
|
|
90
|
+
puts " DND: #{output.green("off")}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Show scheduled DND if present
|
|
94
|
+
if data["dnd_enabled"]
|
|
95
|
+
start_time = data["next_dnd_start_ts"]
|
|
96
|
+
end_time = data["next_dnd_end_ts"]
|
|
97
|
+
if start_time && end_time
|
|
98
|
+
start_str = Time.at(start_time).strftime("%H:%M")
|
|
99
|
+
end_str = Time.at(end_time).strftime("%H:%M")
|
|
100
|
+
puts " Schedule: #{start_str} - #{end_str}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
0
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def set_snooze(duration)
|
|
109
|
+
target_workspaces.each do |workspace|
|
|
110
|
+
api = runner.dnd_api(workspace.name)
|
|
111
|
+
api.set_snooze(duration)
|
|
112
|
+
|
|
113
|
+
success("DND enabled for #{duration} on #{workspace.name}")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
show_all_workspaces_hint
|
|
117
|
+
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def end_snooze
|
|
122
|
+
target_workspaces.each do |workspace|
|
|
123
|
+
api = runner.dnd_api(workspace.name)
|
|
124
|
+
api.end_snooze
|
|
125
|
+
|
|
126
|
+
success("DND disabled on #{workspace.name}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
show_all_workspaces_hint
|
|
130
|
+
|
|
131
|
+
0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def show_all_workspaces_hint
|
|
135
|
+
# Show hint if user has multiple workspaces and didn't use --all or -w
|
|
136
|
+
return if @options[:all] || @options[:workspace]
|
|
137
|
+
return if runner.all_workspaces.size <= 1
|
|
138
|
+
|
|
139
|
+
info("Tip: Use --all to set across all workspaces")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/inline_images"
|
|
4
|
+
require_relative "../support/help_formatter"
|
|
5
|
+
|
|
6
|
+
module SlackCli
|
|
7
|
+
module Commands
|
|
8
|
+
class Emoji < Base
|
|
9
|
+
include Support::InlineImages
|
|
10
|
+
|
|
11
|
+
NETWORK_ERRORS = [
|
|
12
|
+
SocketError,
|
|
13
|
+
Errno::ECONNREFUSED,
|
|
14
|
+
Errno::ETIMEDOUT,
|
|
15
|
+
Net::OpenTimeout,
|
|
16
|
+
Net::ReadTimeout,
|
|
17
|
+
URI::InvalidURIError,
|
|
18
|
+
OpenSSL::SSL::SSLError
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def execute
|
|
22
|
+
result = validate_options
|
|
23
|
+
return result if result
|
|
24
|
+
|
|
25
|
+
case positional_args
|
|
26
|
+
in ["status" | "list"] | []
|
|
27
|
+
show_status
|
|
28
|
+
in ["sync-standard"]
|
|
29
|
+
sync_standard
|
|
30
|
+
in ["download", *rest]
|
|
31
|
+
download_emoji(rest.first)
|
|
32
|
+
in ["clear", *rest]
|
|
33
|
+
clear_emoji(rest.first)
|
|
34
|
+
in ["search", query, *_]
|
|
35
|
+
search_emoji(query)
|
|
36
|
+
in ["search"]
|
|
37
|
+
error("Usage: slk emoji search <query>")
|
|
38
|
+
1
|
|
39
|
+
else
|
|
40
|
+
error("Unknown action: #{positional_args.first}")
|
|
41
|
+
1
|
|
42
|
+
end
|
|
43
|
+
rescue ApiError => e
|
|
44
|
+
error("Failed: #{e.message}")
|
|
45
|
+
1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
protected
|
|
49
|
+
|
|
50
|
+
def default_options
|
|
51
|
+
super.merge(force: false)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_option(arg, args, remaining)
|
|
55
|
+
case arg
|
|
56
|
+
when "-f", "--force"
|
|
57
|
+
@options[:force] = true
|
|
58
|
+
else
|
|
59
|
+
super
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def help_text
|
|
64
|
+
help = Support::HelpFormatter.new("slk emoji <action> [workspace]")
|
|
65
|
+
help.description("Manage emoji cache.")
|
|
66
|
+
|
|
67
|
+
help.section("ACTIONS") do |s|
|
|
68
|
+
s.action("status", "Show emoji cache status")
|
|
69
|
+
s.action("search <query>", "Search emoji by name (all workspaces by default)")
|
|
70
|
+
s.action("sync-standard", "Download standard emoji database (gemoji)")
|
|
71
|
+
s.action("download [ws]", "Download workspace custom emoji")
|
|
72
|
+
s.action("clear [ws]", "Clear emoji cache")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
help.section("OPTIONS") do |s|
|
|
76
|
+
s.option("-w, --workspace", "Limit to specific workspace")
|
|
77
|
+
s.option("-f, --force", "Skip confirmation for clear")
|
|
78
|
+
s.option("-q, --quiet", "Suppress output")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
help.render
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def show_status
|
|
87
|
+
paths = Support::XdgPaths.new
|
|
88
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
89
|
+
gemoji_path = File.join(paths.cache_dir, "gemoji.json")
|
|
90
|
+
|
|
91
|
+
# Show standard emoji status
|
|
92
|
+
if File.exist?(gemoji_path)
|
|
93
|
+
begin
|
|
94
|
+
gemoji = JSON.parse(File.read(gemoji_path))
|
|
95
|
+
puts "Standard emoji database: #{gemoji.size} emojis"
|
|
96
|
+
rescue JSON::ParserError
|
|
97
|
+
puts "Standard emoji database: #{output.yellow("corrupted")}"
|
|
98
|
+
puts " Run 'slk emoji sync-standard' to re-download"
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
puts "Standard emoji database: #{output.yellow("not downloaded")}"
|
|
102
|
+
puts " Run 'slk emoji sync-standard' to download"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
puts
|
|
106
|
+
puts "Workspace emojis: (#{emoji_dir})"
|
|
107
|
+
|
|
108
|
+
target_workspaces.each do |workspace|
|
|
109
|
+
workspace_dir = File.join(emoji_dir, workspace.name)
|
|
110
|
+
|
|
111
|
+
if Dir.exist?(workspace_dir)
|
|
112
|
+
files = Dir.glob(File.join(workspace_dir, "*"))
|
|
113
|
+
count = files.count
|
|
114
|
+
size = files.sum { |f| safe_file_size(f) }
|
|
115
|
+
size_str = format_size(size)
|
|
116
|
+
puts " #{workspace.name}: #{count} emojis (#{size_str})"
|
|
117
|
+
else
|
|
118
|
+
puts " #{workspace.name}: #{output.yellow("not downloaded")}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def search_emoji(query)
|
|
126
|
+
paths = Support::XdgPaths.new
|
|
127
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
128
|
+
gemoji_path = File.join(paths.cache_dir, "gemoji.json")
|
|
129
|
+
pattern = Regexp.new(Regexp.escape(query), Regexp::IGNORECASE)
|
|
130
|
+
|
|
131
|
+
results = []
|
|
132
|
+
|
|
133
|
+
# Search standard emoji
|
|
134
|
+
if File.exist?(gemoji_path)
|
|
135
|
+
begin
|
|
136
|
+
gemoji = JSON.parse(File.read(gemoji_path))
|
|
137
|
+
gemoji.each do |name, char|
|
|
138
|
+
results << { name: name, char: char, source: "standard" } if name.match?(pattern)
|
|
139
|
+
end
|
|
140
|
+
rescue JSON::ParserError
|
|
141
|
+
# Skip standard emoji search if cache is corrupted
|
|
142
|
+
debug("Standard emoji cache corrupted, skipping")
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Search workspace custom emoji (all workspaces by default, or -w to limit)
|
|
147
|
+
workspaces = @options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
|
|
148
|
+
workspaces.each do |workspace|
|
|
149
|
+
workspace_dir = File.join(emoji_dir, workspace.name)
|
|
150
|
+
next unless Dir.exist?(workspace_dir)
|
|
151
|
+
|
|
152
|
+
Dir.glob(File.join(workspace_dir, "*")).each do |filepath|
|
|
153
|
+
name = File.basename(filepath, ".*")
|
|
154
|
+
results << { name: name, path: filepath, source: workspace.name } if name.match?(pattern)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
if results.empty?
|
|
159
|
+
puts "No emoji matching '#{query}'"
|
|
160
|
+
return 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Group by source
|
|
164
|
+
by_source = results.group_by { |r| r[:source] }
|
|
165
|
+
|
|
166
|
+
by_source.each do |source, items|
|
|
167
|
+
puts output.bold(source == "standard" ? "Standard emoji:" : "#{source}:")
|
|
168
|
+
items.sort_by { |r| r[:name] }.each do |item|
|
|
169
|
+
if item[:char]
|
|
170
|
+
puts "#{item[:char]} :#{item[:name]}:"
|
|
171
|
+
elsif item[:path]
|
|
172
|
+
unless print_inline_image_with_text(item[:path], ":#{item[:name]}:")
|
|
173
|
+
puts ":#{item[:name]}:"
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
puts ":#{item[:name]}:"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
puts
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
puts "Found #{results.size} emoji matching '#{query}'"
|
|
183
|
+
0
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def print_progress(current, total, downloaded, skipped)
|
|
187
|
+
# Only update every 1% or when downloading (to show new count)
|
|
188
|
+
pct = ((current.to_f / total) * 100).round
|
|
189
|
+
@last_pct ||= -1
|
|
190
|
+
return if pct == @last_pct && downloaded == (@last_downloaded || 0)
|
|
191
|
+
|
|
192
|
+
@last_pct = pct
|
|
193
|
+
@last_downloaded = downloaded
|
|
194
|
+
|
|
195
|
+
bar_width = 20
|
|
196
|
+
filled = (pct * bar_width / 100).round
|
|
197
|
+
bar = "=" * filled + "-" * (bar_width - filled)
|
|
198
|
+
print "\r [#{bar}] #{pct}% (#{current}/#{total}) +#{downloaded} new"
|
|
199
|
+
$stdout.flush
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def format_size(bytes)
|
|
203
|
+
if bytes >= 1024 * 1024
|
|
204
|
+
"#{(bytes / (1024.0 * 1024)).round}M"
|
|
205
|
+
elsif bytes >= 1024
|
|
206
|
+
"#{(bytes / 1024.0).round}K"
|
|
207
|
+
else
|
|
208
|
+
"#{bytes}B"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Get file size, returning 0 if file doesn't exist or is inaccessible
|
|
213
|
+
def safe_file_size(path)
|
|
214
|
+
File.size(path)
|
|
215
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
216
|
+
0
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def sync_standard
|
|
220
|
+
paths = Support::XdgPaths.new
|
|
221
|
+
cache_dir = paths.cache_dir
|
|
222
|
+
emoji_json_path = File.join(cache_dir, "gemoji.json")
|
|
223
|
+
|
|
224
|
+
puts "Downloading standard emoji database..."
|
|
225
|
+
|
|
226
|
+
# Download gemoji JSON from GitHub
|
|
227
|
+
gemoji_url = "https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json"
|
|
228
|
+
|
|
229
|
+
begin
|
|
230
|
+
uri = URI.parse(gemoji_url)
|
|
231
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
232
|
+
http.use_ssl = true
|
|
233
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
234
|
+
http.cert_store = OpenSSL::X509::Store.new
|
|
235
|
+
http.cert_store.set_default_paths
|
|
236
|
+
|
|
237
|
+
request = Net::HTTP::Get.new(uri)
|
|
238
|
+
response = http.request(request)
|
|
239
|
+
|
|
240
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
241
|
+
error("Failed to download: HTTP #{response.code}")
|
|
242
|
+
return 1
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Parse and transform to shortcode -> emoji mapping
|
|
246
|
+
emoji_data = JSON.parse(response.body)
|
|
247
|
+
emoji_map = {}
|
|
248
|
+
|
|
249
|
+
emoji_data.each do |emoji|
|
|
250
|
+
char = emoji["emoji"]
|
|
251
|
+
next unless char
|
|
252
|
+
|
|
253
|
+
# Add all aliases
|
|
254
|
+
(emoji["aliases"] || []).each do |name|
|
|
255
|
+
emoji_map[name] = char
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Save to cache
|
|
260
|
+
FileUtils.mkdir_p(cache_dir)
|
|
261
|
+
File.write(emoji_json_path, JSON.pretty_generate(emoji_map))
|
|
262
|
+
|
|
263
|
+
success("Downloaded #{emoji_map.size} standard emoji mappings")
|
|
264
|
+
puts " Location: #{emoji_json_path}"
|
|
265
|
+
|
|
266
|
+
0
|
|
267
|
+
rescue JSON::ParserError => e
|
|
268
|
+
error("Failed to parse emoji data: #{e.message}")
|
|
269
|
+
1
|
|
270
|
+
rescue *NETWORK_ERRORS => e
|
|
271
|
+
error("Network error: #{e.message}")
|
|
272
|
+
1
|
|
273
|
+
rescue SystemCallError => e
|
|
274
|
+
error("File system error: #{e.message}")
|
|
275
|
+
1
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def download_emoji(workspace_name)
|
|
280
|
+
workspaces = workspace_name ? [runner.workspace(workspace_name)] : target_workspaces
|
|
281
|
+
paths = Support::XdgPaths.new
|
|
282
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
283
|
+
|
|
284
|
+
workspaces.each do |workspace|
|
|
285
|
+
puts "Fetching emoji list for #{workspace.name}..."
|
|
286
|
+
|
|
287
|
+
api = runner.emoji_api(workspace.name)
|
|
288
|
+
emoji_map = api.custom_emoji
|
|
289
|
+
|
|
290
|
+
workspace_dir = File.join(emoji_dir, workspace.name)
|
|
291
|
+
FileUtils.mkdir_p(workspace_dir)
|
|
292
|
+
|
|
293
|
+
downloaded = 0
|
|
294
|
+
skipped = 0
|
|
295
|
+
failed = 0
|
|
296
|
+
total = emoji_map.size
|
|
297
|
+
processed = 0
|
|
298
|
+
|
|
299
|
+
# Filter to only downloadable (non-alias) emoji
|
|
300
|
+
to_download = emoji_map.reject { |_, url| url.start_with?("alias:") }
|
|
301
|
+
aliases = total - to_download.size
|
|
302
|
+
|
|
303
|
+
to_download.each do |name, url|
|
|
304
|
+
processed += 1
|
|
305
|
+
ext = File.extname(URI.parse(url).path)
|
|
306
|
+
ext = ".png" if ext.empty?
|
|
307
|
+
filepath = File.join(workspace_dir, "#{name}#{ext}")
|
|
308
|
+
|
|
309
|
+
# Skip if already exists
|
|
310
|
+
if File.exist?(filepath)
|
|
311
|
+
skipped += 1
|
|
312
|
+
print_progress(processed, to_download.size, downloaded, skipped)
|
|
313
|
+
next
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Download
|
|
317
|
+
begin
|
|
318
|
+
uri = URI.parse(url)
|
|
319
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
320
|
+
http.use_ssl = true
|
|
321
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
322
|
+
http.cert_store = OpenSSL::X509::Store.new
|
|
323
|
+
http.cert_store.set_default_paths
|
|
324
|
+
http.open_timeout = 10
|
|
325
|
+
http.read_timeout = 30
|
|
326
|
+
|
|
327
|
+
request = Net::HTTP::Get.new(uri)
|
|
328
|
+
response = http.request(request)
|
|
329
|
+
|
|
330
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
331
|
+
File.binwrite(filepath, response.body)
|
|
332
|
+
downloaded += 1
|
|
333
|
+
else
|
|
334
|
+
failed += 1
|
|
335
|
+
end
|
|
336
|
+
rescue *NETWORK_ERRORS, SystemCallError => e
|
|
337
|
+
debug("Failed to download emoji #{name}: #{e.message}")
|
|
338
|
+
failed += 1
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
print_progress(processed, to_download.size, downloaded, skipped)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
puts "\r#{" " * 60}\r" # Clear progress line
|
|
345
|
+
success("Downloaded #{downloaded} new emoji for #{workspace.name}")
|
|
346
|
+
puts " Skipped: #{skipped} (already exist), #{aliases} aliases, #{failed} failed" if skipped > 0 || failed > 0
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
0
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def clear_emoji(workspace_name)
|
|
353
|
+
paths = Support::XdgPaths.new
|
|
354
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
355
|
+
|
|
356
|
+
# Gather info about what will be deleted
|
|
357
|
+
to_clear = []
|
|
358
|
+
if workspace_name
|
|
359
|
+
workspace_dir = File.join(emoji_dir, workspace_name)
|
|
360
|
+
if Dir.exist?(workspace_dir)
|
|
361
|
+
to_clear << { name: workspace_name, dir: workspace_dir }
|
|
362
|
+
else
|
|
363
|
+
puts "No emoji cache for #{workspace_name}"
|
|
364
|
+
return 0
|
|
365
|
+
end
|
|
366
|
+
else
|
|
367
|
+
target_workspaces.each do |workspace|
|
|
368
|
+
workspace_dir = File.join(emoji_dir, workspace.name)
|
|
369
|
+
to_clear << { name: workspace.name, dir: workspace_dir } if Dir.exist?(workspace_dir)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if to_clear.empty?
|
|
373
|
+
puts "No emoji caches to clear"
|
|
374
|
+
return 0
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Show what will be deleted
|
|
379
|
+
puts "Will delete:"
|
|
380
|
+
total_count = 0
|
|
381
|
+
total_size = 0
|
|
382
|
+
to_clear.each do |entry|
|
|
383
|
+
files = Dir.glob(File.join(entry[:dir], "*"))
|
|
384
|
+
count = files.count
|
|
385
|
+
size = files.sum { |f| safe_file_size(f) }
|
|
386
|
+
total_count += count
|
|
387
|
+
total_size += size
|
|
388
|
+
puts " #{entry[:name]}: #{count} files (#{format_size(size)})"
|
|
389
|
+
end
|
|
390
|
+
puts " Total: #{total_count} files (#{format_size(total_size)})"
|
|
391
|
+
|
|
392
|
+
# Confirm unless --force
|
|
393
|
+
unless @options[:force]
|
|
394
|
+
print "\nAre you sure? [y/N] "
|
|
395
|
+
response = $stdin.gets&.chomp&.downcase
|
|
396
|
+
unless response == "y" || response == "yes"
|
|
397
|
+
puts "Cancelled"
|
|
398
|
+
return 0
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Delete
|
|
403
|
+
to_clear.each do |entry|
|
|
404
|
+
FileUtils.rm_rf(entry[:dir])
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
success("Cleared #{total_count} emoji files")
|
|
408
|
+
0
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
module Commands
|
|
5
|
+
class Help < Base
|
|
6
|
+
def execute
|
|
7
|
+
topic = positional_args.first
|
|
8
|
+
|
|
9
|
+
if topic
|
|
10
|
+
show_command_help(topic)
|
|
11
|
+
else
|
|
12
|
+
show_general_help
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def show_general_help
|
|
21
|
+
puts <<~HELP
|
|
22
|
+
#{output.bold("slk")} - Slack CLI v#{VERSION}
|
|
23
|
+
|
|
24
|
+
#{output.bold("USAGE:")}
|
|
25
|
+
slk <command> [options]
|
|
26
|
+
|
|
27
|
+
#{output.bold("COMMANDS:")}
|
|
28
|
+
#{output.cyan("status")} Get or set your status
|
|
29
|
+
#{output.cyan("presence")} Get or set your presence (away/active)
|
|
30
|
+
#{output.cyan("dnd")} Manage Do Not Disturb
|
|
31
|
+
#{output.cyan("messages")} Read channel or DM messages
|
|
32
|
+
#{output.cyan("unread")} View and clear unread messages
|
|
33
|
+
#{output.cyan("preset")} Manage and apply status presets
|
|
34
|
+
#{output.cyan("workspaces")} Manage Slack workspaces
|
|
35
|
+
#{output.cyan("cache")} Manage user/channel cache
|
|
36
|
+
#{output.cyan("emoji")} Download workspace custom emoji
|
|
37
|
+
#{output.cyan("config")} Configuration and setup
|
|
38
|
+
|
|
39
|
+
#{output.bold("GLOBAL OPTIONS:")}
|
|
40
|
+
-w, --workspace NAME Use specific workspace
|
|
41
|
+
--all Apply to all workspaces
|
|
42
|
+
-v, --verbose Show debug output
|
|
43
|
+
-q, --quiet Suppress output
|
|
44
|
+
--json Output as JSON (where supported)
|
|
45
|
+
-h, --help Show help
|
|
46
|
+
|
|
47
|
+
#{output.bold("EXAMPLES:")}
|
|
48
|
+
slk status Show current status
|
|
49
|
+
slk status "Working" :laptop: Set status
|
|
50
|
+
slk status clear Clear status
|
|
51
|
+
slk dnd 1h Enable DND for 1 hour
|
|
52
|
+
slk messages #general Read channel messages
|
|
53
|
+
slk preset meeting Apply preset
|
|
54
|
+
|
|
55
|
+
Run #{output.cyan("slk <command> --help")} for command-specific help.
|
|
56
|
+
HELP
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def show_command_help(topic)
|
|
60
|
+
command_class = CLI::COMMANDS[topic]
|
|
61
|
+
|
|
62
|
+
if command_class
|
|
63
|
+
# Create instance just to get help text
|
|
64
|
+
# Call --help directly since help_text is protected
|
|
65
|
+
runner_stub = Runner.new(output: output)
|
|
66
|
+
cmd = command_class.new(["--help"], runner: runner_stub)
|
|
67
|
+
cmd.execute
|
|
68
|
+
else
|
|
69
|
+
error("Unknown command: #{topic}")
|
|
70
|
+
puts
|
|
71
|
+
puts "Available commands: #{CLI::COMMANDS.keys.join(", ")}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|