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,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/inline_images'
|
|
4
|
+
require_relative '../support/help_formatter'
|
|
5
|
+
|
|
6
|
+
module Slk
|
|
7
|
+
module Commands
|
|
8
|
+
# Gets or sets user status text and emoji
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class Status < Base
|
|
11
|
+
include Support::InlineImages
|
|
12
|
+
|
|
13
|
+
def execute
|
|
14
|
+
result = validate_options
|
|
15
|
+
return result if result
|
|
16
|
+
|
|
17
|
+
dispatch_action
|
|
18
|
+
rescue ApiError => e
|
|
19
|
+
error("Failed: #{e.message}")
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def dispatch_action
|
|
24
|
+
case positional_args
|
|
25
|
+
in ['clear', *] then clear_status
|
|
26
|
+
in [text, *rest] then set_status(text, rest)
|
|
27
|
+
in [] then get_status
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
def default_options
|
|
34
|
+
super.merge(presence: nil, dnd: nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def handle_option(arg, args, remaining)
|
|
38
|
+
case arg
|
|
39
|
+
when '-p', '--presence'
|
|
40
|
+
@options[:presence] = args.shift
|
|
41
|
+
when '-d', '--dnd'
|
|
42
|
+
@options[:dnd] = args.shift
|
|
43
|
+
else
|
|
44
|
+
super
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def help_text
|
|
49
|
+
help = Support::HelpFormatter.new('slk status [text] [emoji] [duration] [options]')
|
|
50
|
+
help.description('Get or set your Slack status.')
|
|
51
|
+
help.note('GET shows all workspaces by default. SET applies to primary only.')
|
|
52
|
+
add_examples_section(help)
|
|
53
|
+
add_options_section(help)
|
|
54
|
+
help.render
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_examples_section(help)
|
|
58
|
+
help.section('EXAMPLES') do |s|
|
|
59
|
+
s.example('slk status', 'Show status (all workspaces)')
|
|
60
|
+
s.example('slk status clear', 'Clear status')
|
|
61
|
+
s.example('slk status "Working" :laptop:', 'Set status with emoji')
|
|
62
|
+
s.example('slk status "Meeting" :calendar: 1h', 'Set status for 1 hour')
|
|
63
|
+
s.example('slk status "Focus" :headphones: 2h -p away -d 2h')
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_options_section(help)
|
|
68
|
+
help.section('OPTIONS') do |s|
|
|
69
|
+
s.option('-p, --presence VALUE', 'Also set presence (away/auto/active)')
|
|
70
|
+
s.option('-d, --dnd DURATION', "Also set DND (or 'off')")
|
|
71
|
+
s.option('-w, --workspace', 'Limit to specific workspace')
|
|
72
|
+
s.option('--all', 'Set across all workspaces')
|
|
73
|
+
s.option('-v, --verbose', 'Show debug information')
|
|
74
|
+
s.option('-q, --quiet', 'Suppress output')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def get_status # rubocop:disable Naming/AccessorMethodName
|
|
81
|
+
# GET defaults to all workspaces unless -w specified
|
|
82
|
+
workspaces = target_workspaces_for_get
|
|
83
|
+
|
|
84
|
+
workspaces.each do |workspace|
|
|
85
|
+
status = runner.users_api(workspace.name).get_status
|
|
86
|
+
print_workspace_status(workspaces, workspace, status)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def target_workspaces_for_get
|
|
93
|
+
@options[:workspace] ? [runner.workspace(@options[:workspace])] : runner.all_workspaces
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_workspace_status(workspaces, workspace, status)
|
|
97
|
+
puts output.bold(workspace.name) if workspaces.size > 1
|
|
98
|
+
|
|
99
|
+
if status.empty?
|
|
100
|
+
puts ' (no status set)'
|
|
101
|
+
else
|
|
102
|
+
display_status(workspace, status)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def display_status(workspace, status)
|
|
107
|
+
emoji_path = workspace_emoji_path(workspace.name, status.emoji)
|
|
108
|
+
|
|
109
|
+
if emoji_path && inline_images_supported?
|
|
110
|
+
print_status_with_image(emoji_path, status)
|
|
111
|
+
else
|
|
112
|
+
puts " #{status}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def workspace_emoji_path(workspace_name, emoji)
|
|
117
|
+
emoji_name = emoji.delete_prefix(':').delete_suffix(':')
|
|
118
|
+
find_workspace_emoji(workspace_name, emoji_name)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def print_status_with_image(emoji_path, status)
|
|
122
|
+
parts = []
|
|
123
|
+
parts << status.text unless status.text.empty?
|
|
124
|
+
parts << "(#{status.time_remaining})" if status.time_remaining
|
|
125
|
+
print_inline_image_with_text(emoji_path, " #{parts.join(' ')}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def find_workspace_emoji(workspace_name, emoji_name)
|
|
129
|
+
return nil if emoji_name.empty?
|
|
130
|
+
|
|
131
|
+
paths = Support::XdgPaths.new
|
|
132
|
+
emoji_dir = config.emoji_dir || paths.cache_dir
|
|
133
|
+
workspace_dir = File.join(emoji_dir, workspace_name)
|
|
134
|
+
return nil unless Dir.exist?(workspace_dir)
|
|
135
|
+
|
|
136
|
+
# Look for emoji file with any extension
|
|
137
|
+
Dir.glob(File.join(workspace_dir, "#{emoji_name}.*")).first
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def set_status(text, rest)
|
|
141
|
+
emoji = extract_emoji(rest)
|
|
142
|
+
duration = extract_duration(rest)
|
|
143
|
+
|
|
144
|
+
target_workspaces.each do |workspace|
|
|
145
|
+
apply_status_to_workspace(workspace, text, emoji, duration)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
show_all_workspaces_hint
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def extract_emoji(rest)
|
|
153
|
+
rest.find { |arg| arg.start_with?(':') && arg.end_with?(':') } || ':speech_balloon:'
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def extract_duration(rest)
|
|
157
|
+
duration_str = rest.find { |arg| arg.match?(/^\d+[hms]?$/) }
|
|
158
|
+
duration_str ? Models::Duration.parse(duration_str) : Models::Duration.zero
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def apply_status_to_workspace(workspace, text, emoji, duration)
|
|
162
|
+
api = runner.users_api(workspace.name)
|
|
163
|
+
api.set_status(text: text, emoji: emoji, duration: duration)
|
|
164
|
+
|
|
165
|
+
log_status_set(workspace.name, text, emoji, duration)
|
|
166
|
+
apply_presence(workspace) if @options[:presence]
|
|
167
|
+
apply_dnd(workspace) if @options[:dnd]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def log_status_set(workspace_name, text, emoji, duration)
|
|
171
|
+
success("Status set on #{workspace_name}")
|
|
172
|
+
debug(" Text: #{text}")
|
|
173
|
+
debug(" Emoji: #{emoji}")
|
|
174
|
+
debug(" Duration: #{duration}") unless duration.zero?
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def apply_presence(workspace)
|
|
178
|
+
value = @options[:presence]
|
|
179
|
+
value = 'auto' if value == 'active'
|
|
180
|
+
|
|
181
|
+
api = runner.users_api(workspace.name)
|
|
182
|
+
api.set_presence(value)
|
|
183
|
+
success("Presence set to #{value} on #{workspace.name}")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def apply_dnd(workspace)
|
|
187
|
+
value = @options[:dnd]
|
|
188
|
+
dnd_api = runner.dnd_api(workspace.name)
|
|
189
|
+
|
|
190
|
+
if value == 'off'
|
|
191
|
+
dnd_api.end_snooze
|
|
192
|
+
success("DND disabled on #{workspace.name}")
|
|
193
|
+
else
|
|
194
|
+
duration = Models::Duration.parse(value)
|
|
195
|
+
dnd_api.set_snooze(duration)
|
|
196
|
+
success("DND enabled for #{value} on #{workspace.name}")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def clear_status
|
|
201
|
+
target_workspaces.each do |workspace|
|
|
202
|
+
api = runner.users_api(workspace.name)
|
|
203
|
+
api.clear_status
|
|
204
|
+
|
|
205
|
+
success("Status cleared on #{workspace.name}")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
show_all_workspaces_hint
|
|
209
|
+
|
|
210
|
+
0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def show_all_workspaces_hint
|
|
214
|
+
# Show hint if user has multiple workspaces and didn't use --all or -w
|
|
215
|
+
return if @options[:all] || @options[:workspace]
|
|
216
|
+
return if runner.all_workspaces.size <= 1
|
|
217
|
+
|
|
218
|
+
info('Tip: Use --all to set across all workspaces')
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
# rubocop:enable Metrics/ClassLength
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'messages'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Views a message thread from a Slack URL
|
|
8
|
+
class Thread < Messages
|
|
9
|
+
def execute
|
|
10
|
+
result = validate_options
|
|
11
|
+
return result if result
|
|
12
|
+
|
|
13
|
+
target = positional_args.first
|
|
14
|
+
return usage_error unless target
|
|
15
|
+
return url_required_error unless Support::SlackUrlParser.new.slack_url?(target)
|
|
16
|
+
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
protected
|
|
21
|
+
|
|
22
|
+
def usage_error
|
|
23
|
+
error('Usage: slk thread <url>')
|
|
24
|
+
1
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def url_required_error
|
|
28
|
+
error('thread command requires a Slack URL')
|
|
29
|
+
1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def default_options
|
|
33
|
+
super.merge(
|
|
34
|
+
limit: 1,
|
|
35
|
+
limit_set: true, # Prevent apply_default_limit from overriding
|
|
36
|
+
threads: true
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def help_text
|
|
41
|
+
help = Support::HelpFormatter.new('slk thread <url> [options]')
|
|
42
|
+
help.description('View a message thread from a Slack URL.')
|
|
43
|
+
add_usage_section(help)
|
|
44
|
+
add_options_section(help)
|
|
45
|
+
add_examples_section(help)
|
|
46
|
+
help.render
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def add_usage_section(help)
|
|
52
|
+
help.section('USAGE') { |s| s.item('<slack_url>', 'Slack message URL') }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_options_section(help)
|
|
56
|
+
help.section('OPTIONS') do |s|
|
|
57
|
+
s.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
58
|
+
s.option('--no-reactions', 'Hide reactions')
|
|
59
|
+
s.option('--no-names', 'Skip user name lookups (faster)')
|
|
60
|
+
s.option('--json', 'Output as JSON')
|
|
61
|
+
s.option('-v, --verbose', 'Show debug information')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def add_examples_section(help)
|
|
66
|
+
help.section('EXAMPLES') do |s|
|
|
67
|
+
s.item('slk thread https://work.slack.com/archives/C123/p1234567890', 'View thread')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../support/help_formatter'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Commands
|
|
7
|
+
# Views and manages unread messages across workspaces
|
|
8
|
+
# rubocop:disable Metrics/ClassLength
|
|
9
|
+
class Unread < Base
|
|
10
|
+
include Support::UserResolver
|
|
11
|
+
|
|
12
|
+
def execute
|
|
13
|
+
result = validate_options
|
|
14
|
+
return result if result
|
|
15
|
+
|
|
16
|
+
dispatch_action
|
|
17
|
+
rescue ApiError => e
|
|
18
|
+
error("Failed: #{e.message}")
|
|
19
|
+
1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def dispatch_action
|
|
25
|
+
case positional_args
|
|
26
|
+
in ['clear', *rest] then clear_unread(rest.first)
|
|
27
|
+
in [] then show_unread
|
|
28
|
+
else unknown_action
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unknown_action
|
|
33
|
+
error("Unknown action: #{positional_args.first}")
|
|
34
|
+
1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def default_options
|
|
40
|
+
super.merge(
|
|
41
|
+
all: true, # Default to all workspaces
|
|
42
|
+
muted: false,
|
|
43
|
+
limit: 10
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def handle_option(arg, args, remaining)
|
|
48
|
+
case arg
|
|
49
|
+
when '--muted'
|
|
50
|
+
@options[:muted] = true
|
|
51
|
+
when '-n', '--limit'
|
|
52
|
+
@options[:limit] = args.shift.to_i
|
|
53
|
+
else
|
|
54
|
+
super
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def help_text
|
|
59
|
+
help = Support::HelpFormatter.new('slk unread [action] [options]')
|
|
60
|
+
help.description('View and manage unread messages (all workspaces by default).')
|
|
61
|
+
add_actions_section(help)
|
|
62
|
+
add_options_section(help)
|
|
63
|
+
help.render
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_actions_section(help)
|
|
67
|
+
help.section('ACTIONS') do |s|
|
|
68
|
+
s.action('(none)', 'Show unread messages')
|
|
69
|
+
s.action('clear', 'Mark all as read')
|
|
70
|
+
s.action('clear #channel', 'Mark specific channel as read')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def add_options_section(help)
|
|
75
|
+
help.section('OPTIONS') do |s|
|
|
76
|
+
add_core_options(s)
|
|
77
|
+
add_formatting_options(s)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_core_options(section)
|
|
82
|
+
section.option('-n, --limit N', 'Messages per channel (default: 10)')
|
|
83
|
+
section.option('--muted', 'Include/clear muted channels')
|
|
84
|
+
section.option('-w, --workspace', 'Limit to specific workspace')
|
|
85
|
+
section.option('--json', 'Output as JSON')
|
|
86
|
+
section.option('-q, --quiet', 'Suppress output')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def add_formatting_options(section)
|
|
90
|
+
section.option('--no-emoji', 'Show :emoji: codes instead of unicode')
|
|
91
|
+
section.option('--no-reactions', 'Hide reactions')
|
|
92
|
+
section.option('--reaction-names', 'Show reactions with user names')
|
|
93
|
+
section.option('--reaction-timestamps', 'Show when each person reacted')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def show_unread
|
|
99
|
+
target_workspaces.each do |workspace|
|
|
100
|
+
puts output.bold(workspace.name) if @options[:all] || target_workspaces.size > 1
|
|
101
|
+
|
|
102
|
+
unread_data = fetch_unread_data(workspace)
|
|
103
|
+
|
|
104
|
+
if @options[:json]
|
|
105
|
+
output_unread_json(workspace, unread_data)
|
|
106
|
+
else
|
|
107
|
+
display_unread(workspace, unread_data)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
0
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fetch_unread_data(workspace)
|
|
115
|
+
counts = runner.client_api(workspace.name).counts
|
|
116
|
+
muted_ids = fetch_muted_ids(workspace)
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
unread_ims: filter_unread_ims(counts['ims'] || []),
|
|
120
|
+
unread_channels: filter_unread_channels(counts['channels'] || [], muted_ids)
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def fetch_muted_ids(workspace)
|
|
125
|
+
@options[:muted] ? [] : runner.users_api(workspace.name).muted_channels
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def filter_unread_ims(ims)
|
|
129
|
+
ims.select { |i| i['has_unreads'] }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def filter_unread_channels(channels, muted_ids)
|
|
133
|
+
channels
|
|
134
|
+
.select { |c| c['has_unreads'] || (c['mention_count'] || 0).positive? }
|
|
135
|
+
.reject { |c| muted_ids.include?(c['id']) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def output_unread_json(workspace, data)
|
|
139
|
+
output_json({
|
|
140
|
+
channels: data[:unread_channels].map { |c| format_channel_json(workspace, c) },
|
|
141
|
+
dms: data[:unread_ims].map { |i| format_dm_json(workspace, i) }
|
|
142
|
+
})
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def format_channel_json(workspace, channel)
|
|
146
|
+
channel_hash = { id: channel['id'], mentions: channel['mention_count'] }
|
|
147
|
+
channel_name = cache_store.get_channel_name(workspace.name, channel['id'])
|
|
148
|
+
channel_hash[:name] = channel_name if channel_name
|
|
149
|
+
channel_hash
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def format_dm_json(workspace, dm_item)
|
|
153
|
+
dm_hash = { id: dm_item['id'], mentions: dm_item['mention_count'] }
|
|
154
|
+
user_id = dm_item['user_id'] || dm_item['user']
|
|
155
|
+
if user_id
|
|
156
|
+
user_name = cache_store.get_user(workspace.name, user_id)
|
|
157
|
+
dm_hash[:user_name] = user_name if user_name
|
|
158
|
+
end
|
|
159
|
+
dm_hash
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def display_unread(workspace, data)
|
|
163
|
+
conversations_api = runner.conversations_api(workspace.name)
|
|
164
|
+
formatter = runner.message_formatter
|
|
165
|
+
|
|
166
|
+
display_unread_dms(workspace, data[:unread_ims], conversations_api, formatter)
|
|
167
|
+
display_unread_channels(workspace, data[:unread_channels], conversations_api, formatter)
|
|
168
|
+
show_threads(workspace, formatter)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def display_unread_dms(workspace, unread_ims, conversations_api, formatter)
|
|
172
|
+
unread_ims.each do |im|
|
|
173
|
+
mention_count = im['mention_count'] || 0
|
|
174
|
+
user_name = resolve_dm_user_name(workspace, im['id'], conversations_api)
|
|
175
|
+
puts
|
|
176
|
+
puts output.bold("@#{user_name}") + (mention_count.positive? ? " (#{mention_count} mentions)" : '')
|
|
177
|
+
puts
|
|
178
|
+
show_channel_messages(workspace, im['id'], @options[:limit], conversations_api, formatter)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def display_unread_channels(workspace, unreads, conversations_api, formatter)
|
|
183
|
+
return puts('No unread messages') if unreads.empty?
|
|
184
|
+
|
|
185
|
+
unreads.each { |ch| display_channel(workspace, ch, conversations_api, formatter) }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def display_channel(workspace, channel, conversations_api, formatter)
|
|
189
|
+
name = cache_store.get_channel_name(workspace.name, channel['id']) || channel['id']
|
|
190
|
+
puts
|
|
191
|
+
puts "#{output.bold("##{name}")} (showing last #{@options[:limit]})"
|
|
192
|
+
puts
|
|
193
|
+
show_channel_messages(workspace, channel['id'], @options[:limit], conversations_api, formatter)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def show_threads(workspace, formatter)
|
|
197
|
+
threads_response = runner.threads_api(workspace.name).get_view(limit: 20)
|
|
198
|
+
return unless threads_response['ok']
|
|
199
|
+
|
|
200
|
+
total_unreads = threads_response['total_unread_replies'] || 0
|
|
201
|
+
return if total_unreads.zero?
|
|
202
|
+
|
|
203
|
+
print_threads_header(total_unreads)
|
|
204
|
+
(threads_response['threads'] || []).each { |t| display_thread(workspace, t, formatter) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def print_threads_header(total_unreads)
|
|
208
|
+
puts
|
|
209
|
+
puts "#{output.bold('🧵 Threads')} (#{total_unreads} unread replies)"
|
|
210
|
+
puts
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def display_thread(workspace, thread, formatter)
|
|
214
|
+
unread_replies = thread['unread_replies'] || []
|
|
215
|
+
return if unread_replies.empty?
|
|
216
|
+
|
|
217
|
+
print_thread_header(workspace, thread)
|
|
218
|
+
print_thread_replies(workspace, thread, unread_replies, formatter)
|
|
219
|
+
puts
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def print_thread_header(workspace, thread)
|
|
223
|
+
root_msg = thread['root_msg'] || {}
|
|
224
|
+
channel_id = root_msg['channel']
|
|
225
|
+
conversation_label = resolve_conversation_label(workspace, channel_id)
|
|
226
|
+
root_user = extract_user_from_message(root_msg, workspace)
|
|
227
|
+
|
|
228
|
+
puts "#{output.blue(" #{conversation_label}")} - thread by #{output.bold(root_user)}"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def print_thread_replies(workspace, thread, unread_replies, formatter)
|
|
232
|
+
channel_id = (thread['root_msg'] || {})['channel']
|
|
233
|
+
unread_replies.first(@options[:limit]).each do |reply|
|
|
234
|
+
message = Models::Message.from_api(reply, channel_id: channel_id)
|
|
235
|
+
puts " #{formatter.format_simple(message, workspace: workspace, options: format_options)}"
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def show_channel_messages(workspace, channel_id, limit, api, formatter)
|
|
240
|
+
messages = fetch_channel_messages(workspace, channel_id, limit, api)
|
|
241
|
+
messages.each do |message|
|
|
242
|
+
puts formatter.format_simple(message, workspace: workspace, options: format_options)
|
|
243
|
+
end
|
|
244
|
+
rescue ApiError => e
|
|
245
|
+
puts output.dim(" (Could not fetch messages: #{e.message})")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def fetch_channel_messages(workspace, channel_id, limit, api)
|
|
249
|
+
history = api.history(channel: channel_id, limit: limit)
|
|
250
|
+
raw_messages = (history['messages'] || []).reverse
|
|
251
|
+
messages = raw_messages.map { |msg| Models::Message.from_api(msg, channel_id: channel_id) }
|
|
252
|
+
|
|
253
|
+
return messages unless @options[:reaction_timestamps]
|
|
254
|
+
|
|
255
|
+
enricher = Services::ReactionEnricher.new(activity_api: runner.activity_api(workspace.name))
|
|
256
|
+
enricher.enrich_messages(messages, channel_id)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def clear_unread(channel_name)
|
|
260
|
+
target_workspaces.each do |ws|
|
|
261
|
+
channel_name ? clear_single_channel(ws, channel_name) : clear_all_channels(ws)
|
|
262
|
+
end
|
|
263
|
+
0
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def clear_single_channel(workspace, channel_name)
|
|
267
|
+
channel_id = resolve_channel_id(workspace, channel_name)
|
|
268
|
+
return unless unread_marker(workspace).mark_single_channel(channel_id)
|
|
269
|
+
|
|
270
|
+
success("Marked ##{channel_name} as read on #{workspace.name}")
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def clear_all_channels(workspace)
|
|
274
|
+
counts = unread_marker(workspace).mark_all(options: { muted: @options[:muted] })
|
|
275
|
+
success("Cleared #{counts[:channels]} channels and #{counts[:threads]} threads on #{workspace.name}")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def unread_marker(workspace)
|
|
279
|
+
Services::UnreadMarker.new(
|
|
280
|
+
conversations_api: runner.conversations_api(workspace.name),
|
|
281
|
+
threads_api: runner.threads_api(workspace.name),
|
|
282
|
+
client_api: runner.client_api(workspace.name),
|
|
283
|
+
users_api: runner.users_api(workspace.name),
|
|
284
|
+
on_debug: ->(msg) { debug(msg) }
|
|
285
|
+
)
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def resolve_channel_id(workspace, channel_name)
|
|
289
|
+
return channel_name if channel_name.match?(/^[CDG][A-Z0-9]+$/)
|
|
290
|
+
|
|
291
|
+
name = channel_name.delete_prefix('#')
|
|
292
|
+
cache_store.get_channel_id(workspace.name, name) || resolve_channel(workspace, name)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def resolve_channel(workspace, name)
|
|
296
|
+
api = runner.conversations_api(workspace.name)
|
|
297
|
+
response = api.list
|
|
298
|
+
channels = response['channels'] || []
|
|
299
|
+
channel = channels.find { |c| c['name'] == name }
|
|
300
|
+
channel&.dig('id') || raise(ConfigError, "Channel not found: ##{name}")
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
# rubocop:enable Metrics/ClassLength
|
|
304
|
+
end
|
|
305
|
+
end
|