slk 0.1.0 → 0.2.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/CHANGELOG.md +22 -1
- data/README.md +5 -5
- data/bin/slk +3 -3
- data/lib/{slack_cli → slk}/api/activity.rb +10 -11
- data/lib/{slack_cli → slk}/api/bots.rb +5 -4
- data/lib/slk/api/client.rb +51 -0
- data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
- data/lib/slk/api/dnd.rb +41 -0
- data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
- data/lib/{slack_cli → slk}/api/threads.rb +13 -12
- data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
- data/lib/slk/api/users.rb +105 -0
- data/lib/slk/cli.rb +157 -0
- data/lib/slk/commands/activity.rb +152 -0
- data/lib/{slack_cli → slk}/commands/base.rb +67 -41
- data/lib/slk/commands/cache.rb +141 -0
- data/lib/slk/commands/catchup.rb +411 -0
- data/lib/slk/commands/config.rb +114 -0
- data/lib/slk/commands/dnd.rb +172 -0
- data/lib/slk/commands/emoji.rb +352 -0
- data/lib/slk/commands/help.rb +97 -0
- data/lib/slk/commands/messages.rb +299 -0
- data/lib/slk/commands/presence.rb +109 -0
- data/lib/slk/commands/preset.rb +231 -0
- data/lib/slk/commands/status.rb +223 -0
- data/lib/slk/commands/thread.rb +72 -0
- data/lib/slk/commands/unread.rb +305 -0
- data/lib/slk/commands/workspaces.rb +168 -0
- data/lib/slk/formatters/activity_formatter.rb +148 -0
- data/lib/slk/formatters/attachment_formatter.rb +65 -0
- data/lib/slk/formatters/block_formatter.rb +57 -0
- data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
- data/lib/slk/formatters/emoji_replacer.rb +141 -0
- data/lib/slk/formatters/json_message_formatter.rb +95 -0
- data/lib/slk/formatters/mention_replacer.rb +158 -0
- data/lib/slk/formatters/message_formatter.rb +174 -0
- data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
- data/lib/slk/formatters/reaction_formatter.rb +87 -0
- data/lib/{slack_cli → slk}/models/channel.rb +12 -10
- data/lib/slk/models/duration.rb +94 -0
- data/lib/slk/models/message.rb +242 -0
- data/lib/slk/models/preset.rb +78 -0
- data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
- data/lib/{slack_cli → slk}/models/status.rb +6 -6
- data/lib/slk/models/user.rb +55 -0
- data/lib/slk/models/workspace.rb +54 -0
- data/lib/{slack_cli → slk}/runner.rb +22 -19
- data/lib/slk/services/activity_enricher.rb +124 -0
- data/lib/slk/services/api_client.rb +145 -0
- data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
- data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
- data/lib/slk/services/emoji_downloader.rb +103 -0
- data/lib/slk/services/emoji_searcher.rb +72 -0
- data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
- data/lib/slk/services/gemoji_sync.rb +97 -0
- data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
- data/lib/slk/services/reaction_enricher.rb +82 -0
- data/lib/slk/services/setup_wizard.rb +131 -0
- data/lib/slk/services/target_resolver.rb +108 -0
- data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
- data/lib/slk/services/unread_marker.rb +101 -0
- data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
- data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
- data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
- data/lib/slk/support/interactive_prompt.rb +29 -0
- data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
- data/lib/slk/support/text_wrapper.rb +57 -0
- data/lib/slk/support/user_resolver.rb +141 -0
- data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
- data/lib/slk/version.rb +5 -0
- data/lib/slk.rb +112 -0
- metadata +80 -59
- data/lib/slack_cli/api/client.rb +0 -49
- data/lib/slack_cli/api/dnd.rb +0 -40
- data/lib/slack_cli/api/users.rb +0 -101
- data/lib/slack_cli/cli.rb +0 -118
- data/lib/slack_cli/commands/activity.rb +0 -292
- data/lib/slack_cli/commands/cache.rb +0 -116
- data/lib/slack_cli/commands/catchup.rb +0 -484
- data/lib/slack_cli/commands/config.rb +0 -159
- data/lib/slack_cli/commands/dnd.rb +0 -143
- data/lib/slack_cli/commands/emoji.rb +0 -412
- data/lib/slack_cli/commands/help.rb +0 -76
- data/lib/slack_cli/commands/messages.rb +0 -317
- data/lib/slack_cli/commands/presence.rb +0 -107
- data/lib/slack_cli/commands/preset.rb +0 -239
- data/lib/slack_cli/commands/status.rb +0 -194
- data/lib/slack_cli/commands/thread.rb +0 -62
- data/lib/slack_cli/commands/unread.rb +0 -312
- data/lib/slack_cli/commands/workspaces.rb +0 -151
- data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
- data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
- data/lib/slack_cli/formatters/message_formatter.rb +0 -429
- data/lib/slack_cli/models/duration.rb +0 -85
- data/lib/slack_cli/models/message.rb +0 -217
- data/lib/slack_cli/models/preset.rb +0 -73
- data/lib/slack_cli/models/user.rb +0 -56
- data/lib/slack_cli/models/workspace.rb +0 -52
- data/lib/slack_cli/services/api_client.rb +0 -149
- data/lib/slack_cli/services/reaction_enricher.rb +0 -87
- data/lib/slack_cli/support/user_resolver.rb +0 -114
- data/lib/slack_cli/version.rb +0 -5
- data/lib/slack_cli.rb +0 -91
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Interactive review and dismissal of unread messages
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
|
+
class Catchup < Base
|
|
10
|
+
include Support::UserResolver
|
|
11
|
+
include Support::InteractivePrompt
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
result = validate_options
|
|
15
|
+
return result if result
|
|
16
|
+
|
|
17
|
+
if @options[:batch]
|
|
18
|
+
batch_catchup
|
|
19
|
+
else
|
|
20
|
+
interactive_catchup
|
|
21
|
+
end
|
|
22
|
+
rescue ApiError => e
|
|
23
|
+
error("Failed: #{e.message}")
|
|
24
|
+
1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def default_options
|
|
30
|
+
super.merge(
|
|
31
|
+
all: true, # Default to all workspaces
|
|
32
|
+
batch: false,
|
|
33
|
+
muted: false,
|
|
34
|
+
limit: 5
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def handle_option(arg, args, remaining)
|
|
39
|
+
case arg
|
|
40
|
+
when '--batch'
|
|
41
|
+
@options[:batch] = true
|
|
42
|
+
when '--muted'
|
|
43
|
+
@options[:muted] = true
|
|
44
|
+
when '-n', '--limit'
|
|
45
|
+
@options[:limit] = args.shift.to_i
|
|
46
|
+
else
|
|
47
|
+
super
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def help_text
|
|
52
|
+
help = Support::HelpFormatter.new('slk catchup [options]')
|
|
53
|
+
help.description('Interactively review and dismiss unread messages (all workspaces by default).')
|
|
54
|
+
add_options_section(help)
|
|
55
|
+
add_keys_section(help)
|
|
56
|
+
help.render
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_options_section(help)
|
|
60
|
+
help.section('OPTIONS') do |s|
|
|
61
|
+
add_primary_options(s)
|
|
62
|
+
add_formatting_options(s)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_primary_options(section)
|
|
67
|
+
section.option('--batch', 'Non-interactive mode (mark all as read)')
|
|
68
|
+
section.option('--muted', 'Include muted channels')
|
|
69
|
+
section.option('-n, --limit N', 'Messages per channel (default: 5)')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_formatting_options(section)
|
|
73
|
+
section.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
74
|
+
section.option('--no-reactions', 'Hide reactions')
|
|
75
|
+
section.option('--reaction-names', 'Show reactions with user names')
|
|
76
|
+
section.option('--reaction-timestamps', 'Show when each person reacted')
|
|
77
|
+
section.option('-w, --workspace', 'Limit to specific workspace')
|
|
78
|
+
section.option('-q, --quiet', 'Suppress output')
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_keys_section(help)
|
|
82
|
+
help.section('INTERACTIVE KEYS') do |s|
|
|
83
|
+
s.item('s / Enter', 'Skip channel')
|
|
84
|
+
s.item('r', 'Mark as read and continue')
|
|
85
|
+
s.item('o', 'Open in Slack')
|
|
86
|
+
s.item('q', 'Quit')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def batch_catchup
|
|
93
|
+
target_workspaces.each { |ws| batch_mark_workspace(ws) }
|
|
94
|
+
0
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def batch_mark_workspace(workspace)
|
|
98
|
+
marker = build_unread_marker(workspace)
|
|
99
|
+
counts = marker.mark_all(options: { muted: @options[:muted] })
|
|
100
|
+
success("Marked #{counts[:dms]} DMs, #{counts[:channels]} channels, " \
|
|
101
|
+
"and #{counts[:threads]} threads as read on #{workspace.name}")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_unread_marker(workspace)
|
|
105
|
+
Services::UnreadMarker.new(
|
|
106
|
+
conversations_api: runner.conversations_api(workspace.name),
|
|
107
|
+
threads_api: runner.threads_api(workspace.name),
|
|
108
|
+
client_api: runner.client_api(workspace.name),
|
|
109
|
+
users_api: runner.users_api(workspace.name),
|
|
110
|
+
on_debug: ->(msg) { debug(msg) }
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def interactive_catchup
|
|
115
|
+
target_workspaces.each do |workspace|
|
|
116
|
+
result = process_workspace(workspace)
|
|
117
|
+
return 0 if result == :quit
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
puts
|
|
121
|
+
success('Catchup complete!')
|
|
122
|
+
0
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def process_workspace(workspace)
|
|
126
|
+
items = gather_unread_items(workspace)
|
|
127
|
+
|
|
128
|
+
if items[:empty]
|
|
129
|
+
puts "No unread messages in #{workspace.name}"
|
|
130
|
+
return :continue
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
puts output.bold("\n#{workspace.name}: #{items[:total]} items with unreads\n")
|
|
134
|
+
process_all_items(workspace, items)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def gather_unread_items(workspace)
|
|
138
|
+
counts = runner.client_api(workspace.name).counts
|
|
139
|
+
ims = filter_unread_ims(counts['ims'] || [])
|
|
140
|
+
channels = filter_unread_channels(workspace, counts['channels'] || [])
|
|
141
|
+
threads_response = fetch_unread_threads(workspace)
|
|
142
|
+
|
|
143
|
+
build_items_result(ims, channels, threads_response)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def build_items_result(ims, channels, threads_response)
|
|
147
|
+
{
|
|
148
|
+
ims: ims, channels: channels, threads_response: threads_response,
|
|
149
|
+
total: ims.size + channels.size + (threads_response ? 1 : 0),
|
|
150
|
+
empty: ims.empty? && channels.empty? && !threads_response
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def filter_unread_ims(ims)
|
|
155
|
+
ims.select { |i| i['has_unreads'] }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def filter_unread_channels(workspace, channels)
|
|
159
|
+
muted_ids = @options[:muted] ? [] : runner.users_api(workspace.name).muted_channels
|
|
160
|
+
channels
|
|
161
|
+
.select { |c| c['has_unreads'] || (c['mention_count'] || 0).positive? }
|
|
162
|
+
.reject { |c| muted_ids.include?(c['id']) }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def fetch_unread_threads(workspace)
|
|
166
|
+
response = runner.threads_api(workspace.name).get_view(limit: 20)
|
|
167
|
+
response if response['ok'] && (response['total_unread_replies'] || 0).positive?
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def process_all_items(workspace, items)
|
|
171
|
+
index = { current: 0, total: items[:total] }
|
|
172
|
+
|
|
173
|
+
return :quit if process_dms(workspace, items[:ims], index)
|
|
174
|
+
return :quit if items[:threads_response] && process_threads_item(workspace, items[:threads_response], index)
|
|
175
|
+
return :quit if process_channels(workspace, items[:channels], index)
|
|
176
|
+
|
|
177
|
+
:continue
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# rubocop:disable Naming/PredicateMethod
|
|
181
|
+
def process_dms(workspace, ims, index)
|
|
182
|
+
ims.each do |im|
|
|
183
|
+
return true if process_dm(workspace, im, index[:current], index[:total]) == :quit
|
|
184
|
+
|
|
185
|
+
index[:current] += 1
|
|
186
|
+
end
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def process_channels(workspace, channels, index)
|
|
191
|
+
channels.each do |channel|
|
|
192
|
+
return true if process_channel(workspace, channel, index[:current], index[:total]) == :quit
|
|
193
|
+
|
|
194
|
+
index[:current] += 1
|
|
195
|
+
end
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def process_threads_item(workspace, threads_response, index)
|
|
200
|
+
result = process_threads(workspace, threads_response, index[:current], index[:total])
|
|
201
|
+
index[:current] += 1
|
|
202
|
+
result == :quit
|
|
203
|
+
end
|
|
204
|
+
# rubocop:enable Naming/PredicateMethod
|
|
205
|
+
|
|
206
|
+
def process_channel(workspace, channel, index, total)
|
|
207
|
+
channel_id = channel['id']
|
|
208
|
+
channel_name = cache_store.get_channel_name(workspace.name, channel_id) || channel_id
|
|
209
|
+
label = "##{channel_name}"
|
|
210
|
+
|
|
211
|
+
process_conversation(workspace, channel, index, total, label)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def process_dm(workspace, dm_item, index, total)
|
|
215
|
+
channel_id = dm_item['id']
|
|
216
|
+
conversations = runner.conversations_api(workspace.name)
|
|
217
|
+
user_name = resolve_dm_user_name(workspace, channel_id, conversations)
|
|
218
|
+
label = "@#{user_name}"
|
|
219
|
+
|
|
220
|
+
process_conversation(workspace, dm_item, index, total, label)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def process_conversation(workspace, item, index, total, label)
|
|
224
|
+
channel_id = item['id']
|
|
225
|
+
last_read = item['last_read']
|
|
226
|
+
latest_ts = item['latest']
|
|
227
|
+
mentions = item['mention_count'] || 0
|
|
228
|
+
|
|
229
|
+
messages = fetch_unread_messages(workspace, channel_id, last_read)
|
|
230
|
+
display_conversation_header(index, total, label, mentions)
|
|
231
|
+
display_messages(workspace, messages, channel_id)
|
|
232
|
+
prompt_conversation_action(workspace, channel_id, latest_ts)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def fetch_unread_messages(workspace, channel_id, last_read)
|
|
236
|
+
conversations = runner.conversations_api(workspace.name)
|
|
237
|
+
history_opts = { channel: channel_id, limit: @options[:limit] }
|
|
238
|
+
history_opts[:oldest] = last_read if last_read
|
|
239
|
+
history = conversations.history(**history_opts)
|
|
240
|
+
(history['messages'] || []).reverse
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def display_conversation_header(index, total, label, mentions)
|
|
244
|
+
puts
|
|
245
|
+
puts output.bold("[#{index + 1}/#{total}] #{label}")
|
|
246
|
+
puts output.yellow("#{mentions} mentions") if mentions.positive?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def display_messages(workspace, raw_messages, channel_id)
|
|
250
|
+
messages = raw_messages.map { |msg| Models::Message.from_api(msg, channel_id: channel_id) }
|
|
251
|
+
messages = enrich_messages(workspace, messages, channel_id) if @options[:reaction_timestamps]
|
|
252
|
+
|
|
253
|
+
messages.each do |message|
|
|
254
|
+
formatted = runner.message_formatter.format_simple(message, workspace: workspace, options: format_options)
|
|
255
|
+
puts " #{formatted}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def enrich_messages(workspace, messages, channel_id)
|
|
260
|
+
enricher = Services::ReactionEnricher.new(activity_api: runner.activity_api(workspace.name))
|
|
261
|
+
enricher.enrich_messages(messages, channel_id)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def prompt_conversation_action(workspace, channel_id, latest_ts)
|
|
265
|
+
conversations = runner.conversations_api(workspace.name)
|
|
266
|
+
prompt = output.cyan('[s]kip [r]ead [o]pen [q]uit')
|
|
267
|
+
loop do
|
|
268
|
+
input = prompt_for_action(prompt)
|
|
269
|
+
result = handle_channel_action(input, workspace, channel_id, latest_ts, conversations)
|
|
270
|
+
return result if result
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def handle_channel_action(input, workspace, channel_id, latest_ts, conversations)
|
|
275
|
+
case input&.downcase
|
|
276
|
+
when 's', "\r", "\n", nil then :next
|
|
277
|
+
when 'q', "\u0003", "\u0004" then :quit
|
|
278
|
+
when 'r' then mark_channel_read(conversations, channel_id, latest_ts)
|
|
279
|
+
when 'o' then open_channel_in_slack(workspace, channel_id)
|
|
280
|
+
else
|
|
281
|
+
print_invalid_key
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def mark_channel_read(conversations, channel_id, latest_ts)
|
|
287
|
+
if latest_ts
|
|
288
|
+
conversations.mark(channel: channel_id, timestamp: latest_ts)
|
|
289
|
+
success('Marked as read')
|
|
290
|
+
end
|
|
291
|
+
:next
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def open_channel_in_slack(workspace, channel_id)
|
|
295
|
+
team_id = runner.client_api(workspace.name).team_id
|
|
296
|
+
system('open', "slack://channel?team=#{team_id}&id=#{channel_id}")
|
|
297
|
+
success('Opened in Slack')
|
|
298
|
+
:next
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def print_invalid_key
|
|
302
|
+
print "\r#{output.red('Invalid key')} - #{output.cyan('[s]kip [r]ead [o]pen [q]uit')}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def process_threads(workspace, threads_response, index, total)
|
|
306
|
+
total_unreads = threads_response['total_unread_replies'] || 0
|
|
307
|
+
threads = threads_response['threads'] || []
|
|
308
|
+
|
|
309
|
+
puts
|
|
310
|
+
puts output.bold("[#{index + 1}/#{total}] 🧵 Threads (#{total_unreads} unread replies)")
|
|
311
|
+
|
|
312
|
+
thread_mark_data = threads.filter_map { |thread| display_thread(workspace, thread) }
|
|
313
|
+
|
|
314
|
+
prompt_threads_action(workspace, thread_mark_data)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def display_thread(workspace, thread)
|
|
318
|
+
unread_replies = thread['unread_replies'] || []
|
|
319
|
+
return nil if unread_replies.empty?
|
|
320
|
+
|
|
321
|
+
root_msg = thread['root_msg'] || {}
|
|
322
|
+
print_thread_header(workspace, root_msg)
|
|
323
|
+
display_thread_replies(workspace, unread_replies, root_msg['channel'])
|
|
324
|
+
puts
|
|
325
|
+
|
|
326
|
+
build_thread_mark_data(root_msg, unread_replies)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def print_thread_header(workspace, root_msg)
|
|
330
|
+
label = resolve_conversation_label(workspace, root_msg['channel'])
|
|
331
|
+
user = extract_user_from_message(root_msg, workspace)
|
|
332
|
+
puts "#{output.blue(" #{label}")} - thread by #{output.bold(user)}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def build_thread_mark_data(root_msg, unread_replies)
|
|
336
|
+
{
|
|
337
|
+
channel: root_msg['channel'],
|
|
338
|
+
thread_ts: root_msg['thread_ts'],
|
|
339
|
+
ts: unread_replies.map { |r| r['ts'] }.max
|
|
340
|
+
}
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def display_thread_replies(workspace, replies, channel_id)
|
|
344
|
+
messages = replies.map { |reply| Models::Message.from_api(reply, channel_id: channel_id) }
|
|
345
|
+
messages = enrich_messages(workspace, messages, channel_id) if @options[:reaction_timestamps]
|
|
346
|
+
|
|
347
|
+
messages.each do |message|
|
|
348
|
+
formatted = runner.message_formatter.format_simple(message, workspace: workspace, options: format_options)
|
|
349
|
+
puts " #{formatted}"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def prompt_threads_action(workspace, thread_mark_data)
|
|
354
|
+
prompt = output.cyan('[s]kip [r]ead [o]pen [q]uit')
|
|
355
|
+
loop do
|
|
356
|
+
input = prompt_for_action(prompt)
|
|
357
|
+
result = handle_threads_action(input, workspace, thread_mark_data)
|
|
358
|
+
return result if result
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def handle_threads_action(input, workspace, thread_mark_data)
|
|
363
|
+
case input&.downcase
|
|
364
|
+
when 's', "\r", "\n", nil then :next
|
|
365
|
+
when 'q', "\u0003", "\u0004" then :quit
|
|
366
|
+
when 'r' then handle_mark_threads(workspace, thread_mark_data)
|
|
367
|
+
when 'o' then handle_open_threads(workspace, thread_mark_data)
|
|
368
|
+
else handle_invalid_key
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def handle_mark_threads(workspace, thread_mark_data)
|
|
373
|
+
mark_threads_as_read(workspace, thread_mark_data)
|
|
374
|
+
:next
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def handle_open_threads(workspace, thread_mark_data)
|
|
378
|
+
open_first_thread(workspace, thread_mark_data)
|
|
379
|
+
:next
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def handle_invalid_key
|
|
383
|
+
print_invalid_key
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def mark_threads_as_read(workspace, thread_mark_data)
|
|
388
|
+
threads_api = runner.threads_api(workspace.name)
|
|
389
|
+
marked = 0
|
|
390
|
+
thread_mark_data.each do |data|
|
|
391
|
+
threads_api.mark(channel: data[:channel], thread_ts: data[:thread_ts], timestamp: data[:ts])
|
|
392
|
+
marked += 1
|
|
393
|
+
rescue ApiError => e
|
|
394
|
+
debug("Could not mark thread #{data[:thread_ts]} in #{data[:channel]}: #{e.message}")
|
|
395
|
+
end
|
|
396
|
+
success("Marked #{marked} thread(s) as read")
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def open_first_thread(workspace, thread_mark_data)
|
|
400
|
+
return unless thread_mark_data.any?
|
|
401
|
+
|
|
402
|
+
first = thread_mark_data.first
|
|
403
|
+
team_id = runner.client_api(workspace.name).team_id
|
|
404
|
+
url = "slack://channel?team=#{team_id}&id=#{first[:channel]}&thread_ts=#{first[:thread_ts]}"
|
|
405
|
+
system('open', url)
|
|
406
|
+
success('Opened in Slack')
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
# rubocop:enable Metrics/ClassLength
|
|
410
|
+
end
|
|
411
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Manages CLI configuration settings
|
|
8
|
+
class Config < Base
|
|
9
|
+
def execute
|
|
10
|
+
result = validate_options
|
|
11
|
+
return result if result
|
|
12
|
+
|
|
13
|
+
dispatch_action
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def dispatch_action
|
|
19
|
+
case positional_args
|
|
20
|
+
in ['show'] | [] then show_config
|
|
21
|
+
in ['setup'] | [_] then run_setup
|
|
22
|
+
in ['get', key] then get_value(key)
|
|
23
|
+
in ['set', key, value] then set_value(key, value)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
protected
|
|
28
|
+
|
|
29
|
+
def help_text
|
|
30
|
+
help = Support::HelpFormatter.new('slk config [action]')
|
|
31
|
+
help.description('Manage configuration.')
|
|
32
|
+
add_actions_section(help)
|
|
33
|
+
add_keys_section(help)
|
|
34
|
+
add_options_section(help)
|
|
35
|
+
help.render
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def add_actions_section(help)
|
|
39
|
+
help.section('ACTIONS') do |s|
|
|
40
|
+
s.action('show', 'Show current configuration')
|
|
41
|
+
s.action('setup', 'Run setup wizard')
|
|
42
|
+
s.action('get <key>', 'Get a config value')
|
|
43
|
+
s.action('set <key> <val>', 'Set a config value')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def add_keys_section(help)
|
|
48
|
+
help.section('CONFIG KEYS') do |s|
|
|
49
|
+
s.item('primary_workspace', 'Default workspace name')
|
|
50
|
+
s.item('ssh_key', 'Path to SSH key for encryption')
|
|
51
|
+
s.item('emoji_dir', 'Custom emoji directory')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_options_section(help)
|
|
56
|
+
help.section('OPTIONS') do |s|
|
|
57
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def show_config
|
|
64
|
+
display_config_values
|
|
65
|
+
display_workspace_info
|
|
66
|
+
display_paths
|
|
67
|
+
0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def display_config_values
|
|
71
|
+
puts 'Configuration:'
|
|
72
|
+
puts " Primary workspace: #{config.primary_workspace || '(not set)'}"
|
|
73
|
+
puts " SSH key: #{config.ssh_key || '(not set)'}"
|
|
74
|
+
puts " Emoji dir: #{config.emoji_dir || '(default)'}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def display_workspace_info
|
|
78
|
+
puts
|
|
79
|
+
puts "Workspaces: #{runner.workspace_names.join(', ')}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def display_paths
|
|
83
|
+
puts
|
|
84
|
+
paths = Support::XdgPaths.new
|
|
85
|
+
puts "Config dir: #{paths.config_dir}"
|
|
86
|
+
puts "Cache dir: #{paths.cache_dir}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_setup
|
|
90
|
+
wizard = Services::SetupWizard.new(
|
|
91
|
+
runner: runner,
|
|
92
|
+
config: config,
|
|
93
|
+
token_store: token_store,
|
|
94
|
+
output: output
|
|
95
|
+
)
|
|
96
|
+
wizard.run
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def get_value(key)
|
|
100
|
+
value = config[key]
|
|
101
|
+
puts value || '(not set)'
|
|
102
|
+
|
|
103
|
+
0
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def set_value(key, value)
|
|
107
|
+
config[key] = value
|
|
108
|
+
success("Set #{key} = #{value}")
|
|
109
|
+
|
|
110
|
+
0
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Manages Do Not Disturb (snooze) settings
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
|
+
class Dnd < Base
|
|
10
|
+
def execute
|
|
11
|
+
result = validate_options
|
|
12
|
+
return result if result
|
|
13
|
+
|
|
14
|
+
dispatch_action
|
|
15
|
+
rescue ArgumentError => e
|
|
16
|
+
error("Invalid duration: #{e.message}")
|
|
17
|
+
1
|
|
18
|
+
rescue ApiError => e
|
|
19
|
+
error("Failed: #{e.message}")
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dispatch_action
|
|
24
|
+
case positional_args
|
|
25
|
+
in ['status' | 'info'] | [] then get_status
|
|
26
|
+
in ['on' | 'snooze', *rest] then enable_snooze(rest.first)
|
|
27
|
+
in ['off' | 'end'] then end_snooze
|
|
28
|
+
in [duration_str] if duration_str.match?(/^\d+[hms]?$/) then enable_snooze(duration_str)
|
|
29
|
+
else unknown_action
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def enable_snooze(duration_str)
|
|
34
|
+
duration = Models::Duration.parse(duration_str || '1h')
|
|
35
|
+
set_snooze(duration)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def unknown_action
|
|
39
|
+
error("Unknown action: #{positional_args.first}")
|
|
40
|
+
error('Valid actions: status, on, off, or a duration (e.g., 1h)')
|
|
41
|
+
1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def help_text
|
|
47
|
+
help = Support::HelpFormatter.new('slk dnd [action] [duration]')
|
|
48
|
+
help.description('Manage Do Not Disturb (snooze) settings.')
|
|
49
|
+
help.note('GET shows all workspaces by default. SET applies to primary only.')
|
|
50
|
+
add_actions_section(help)
|
|
51
|
+
add_duration_section(help)
|
|
52
|
+
add_options_section(help)
|
|
53
|
+
help.render
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_actions_section(help)
|
|
57
|
+
help.section('ACTIONS') do |s|
|
|
58
|
+
s.action('(none)', 'Show current DND status (all workspaces)')
|
|
59
|
+
s.action('status', 'Show current DND status')
|
|
60
|
+
s.action('on [duration]', 'Enable snooze (default: 1h)')
|
|
61
|
+
s.action('off', 'Disable snooze')
|
|
62
|
+
s.action('<duration>', 'Enable snooze for specified duration')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_duration_section(help)
|
|
67
|
+
help.section('DURATION FORMAT') do |s|
|
|
68
|
+
s.item('1h', '1 hour')
|
|
69
|
+
s.item('30m', '30 minutes')
|
|
70
|
+
s.item('1h30m', '1 hour 30 minutes')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_options_section(help)
|
|
75
|
+
help.section('OPTIONS') do |s|
|
|
76
|
+
s.option('-w, --workspace', 'Limit to specific workspace')
|
|
77
|
+
s.option('--all', 'Set across all workspaces')
|
|
78
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def get_status # rubocop:disable Naming/AccessorMethodName
|
|
85
|
+
workspaces = target_workspaces_for_get
|
|
86
|
+
|
|
87
|
+
workspaces.each do |workspace|
|
|
88
|
+
print_workspace_dnd_status(workspaces, workspace)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
0
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def target_workspaces_for_get
|
|
95
|
+
@options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def print_workspace_dnd_status(workspaces, workspace)
|
|
99
|
+
api = runner.dnd_api(workspace.name)
|
|
100
|
+
data = api.info
|
|
101
|
+
|
|
102
|
+
puts output.bold(workspace.name) if workspaces.size > 1
|
|
103
|
+
print_snooze_status(api, data)
|
|
104
|
+
print_scheduled_dnd(data)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def print_snooze_status(api, data)
|
|
108
|
+
if data['snooze_enabled']
|
|
109
|
+
print_snoozing_status(api)
|
|
110
|
+
else
|
|
111
|
+
puts " DND: #{output.green('off')}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def print_snoozing_status(api)
|
|
116
|
+
remaining = api.snooze_remaining
|
|
117
|
+
if remaining
|
|
118
|
+
puts " DND: #{output.yellow('snoozing')} (#{remaining} remaining)"
|
|
119
|
+
else
|
|
120
|
+
puts " DND: #{output.yellow('snoozing')} (expired)"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print_scheduled_dnd(data)
|
|
125
|
+
return unless data['dnd_enabled']
|
|
126
|
+
|
|
127
|
+
start_time = data['next_dnd_start_ts']
|
|
128
|
+
end_time = data['next_dnd_end_ts']
|
|
129
|
+
return unless start_time && end_time
|
|
130
|
+
|
|
131
|
+
start_str = Time.at(start_time).strftime('%H:%M')
|
|
132
|
+
end_str = Time.at(end_time).strftime('%H:%M')
|
|
133
|
+
puts " Schedule: #{start_str} - #{end_str}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def set_snooze(duration) # rubocop:disable Naming/AccessorMethodName
|
|
137
|
+
target_workspaces.each do |workspace|
|
|
138
|
+
api = runner.dnd_api(workspace.name)
|
|
139
|
+
api.set_snooze(duration)
|
|
140
|
+
|
|
141
|
+
success("DND enabled for #{duration} on #{workspace.name}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
show_all_workspaces_hint
|
|
145
|
+
|
|
146
|
+
0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def end_snooze
|
|
150
|
+
target_workspaces.each do |workspace|
|
|
151
|
+
api = runner.dnd_api(workspace.name)
|
|
152
|
+
api.end_snooze
|
|
153
|
+
|
|
154
|
+
success("DND disabled on #{workspace.name}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
show_all_workspaces_hint
|
|
158
|
+
|
|
159
|
+
0
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def show_all_workspaces_hint
|
|
163
|
+
# Show hint if user has multiple workspaces and didn't use --all or -w
|
|
164
|
+
return if @options[:all] || @options[:workspace]
|
|
165
|
+
return if runner.all_workspaces.size <= 1
|
|
166
|
+
|
|
167
|
+
info('Tip: Use --all to set across all workspaces')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
# rubocop:enable Metrics/ClassLength
|
|
171
|
+
end
|
|
172
|
+
end
|