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