slk 0.14.0 → 0.16.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 +30 -0
- data/README.md +8 -3
- data/lib/slk/api/huddles.rb +25 -0
- data/lib/slk/api/users.rb +4 -2
- data/lib/slk/commands/activity.rb +6 -0
- data/lib/slk/commands/catchup.rb +29 -11
- data/lib/slk/commands/debug.rb +38 -5
- data/lib/slk/commands/huddles.rb +2 -1
- data/lib/slk/commands/messages.rb +20 -3
- data/lib/slk/commands/thread.rb +1 -2
- data/lib/slk/commands/unread.rb +34 -19
- data/lib/slk/commands/who.rb +4 -3
- data/lib/slk/formatters/json_message_formatter.rb +1 -1
- data/lib/slk/formatters/mention_replacer.rb +9 -3
- data/lib/slk/formatters/search_formatter.rb +4 -0
- data/lib/slk/models/search_result.rb +3 -0
- data/lib/slk/services/activity_enricher.rb +33 -13
- data/lib/slk/services/api_client.rb +1 -1
- data/lib/slk/services/cache_store.rb +13 -2
- data/lib/slk/services/channel_lookup.rb +78 -0
- data/lib/slk/services/group_dm_label.rb +83 -0
- data/lib/slk/services/huddle_channel_label.rb +9 -1
- data/lib/slk/services/huddle_fetch.rb +10 -1
- data/lib/slk/services/huddle_room.rb +47 -0
- data/lib/slk/services/huddle_room_enricher.rb +88 -0
- data/lib/slk/services/huddle_scanner.rb +12 -6
- data/lib/slk/services/pooled_fanout.rb +59 -0
- data/lib/slk/services/sent_channel_label.rb +4 -0
- data/lib/slk/services/target_resolver.rb +2 -13
- data/lib/slk/services/unread_marker.rb +8 -5
- data/lib/slk/support/group_dm_name.rb +39 -0
- data/lib/slk/support/user_resolver.rb +21 -5
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +7 -0
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5edea2fd65213e0875f06f53729966f8658ae880bd38321bd9a9d8980200c07a
|
|
4
|
+
data.tar.gz: 7a532e0e1991807962aae21099ad2e9c2c98b03a364dc3e6db90ebf7f2fdabf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 256c73b33433f9488bfcd1333c14f57e953e80fea54a07500239933cdf2129e682beb5d18cc3ab6f730d77570fd103ea77ab2b2ad7f2f8386d80924e6240f5e1
|
|
7
|
+
data.tar.gz: 388ffeef9e34949a549fb4cc7bcd8560e7132ad9ebde68b189c7dccfac5b749e3e21445c41fea2f614e5a3f1f179101b45b15727a219a3ab289d916aa0cb4f8a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.16.0] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `slk debug huddle <call id|user>` (hidden command) prints the raw `huddles.get` response. A user is resolved to the call in their profile's `huddle_state_call_id`.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `slk huddles` and `slk who` show the huddle's channel again. Slack stopped sending `huddle_state_channel_id` on profiles, so every huddle read "channel unknown". The call id is now looked up with `huddles.get`, which also adds Slack Connect guests missing from `users.list`. Huddles in conversations you can't see still show "channel unknown". Room and guest-name lookups run on four connections per workspace, about 3× faster than one at a time.
|
|
19
|
+
|
|
20
|
+
## [0.15.0] - 2026-10-13
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Named group DMs ("named chats") show under their name, such as `#ifams review`, in `slk search`, `slk sent` (header and JSON `channel_label`), and `slk huddles`. Before, search showed `<@ifams review>`, sent listed the members, and huddles showed `group DM`. Slack reports these as `is_mpim` with `properties.has_custom_mpdm_name`. Search results carry no `properties`, so a group DM whose name is not an `mpdm-*` slug also counts as named. These commands don't cache a named group DM by name, because a chat can be renamed or share a name with a real channel.
|
|
25
|
+
- `slk debug channel <id|url|#name>` (hidden command) prints the raw `conversations.info` response.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- `slk unread` and `slk catchup` now show group DMs. Slack lists them under `mpims` in `client.counts`, and both commands only read `ims` and `channels`, so a group DM's unreads never appeared. A named group DM is headed by its name (`#ifams review`); any other by the people in it besides you (`@Ada Lovelace, Grace Hopper`). A muted group DM is skipped like a muted channel unless `--muted` is passed. `slk unread --json` adds a `group_dms` array of `{id, mentions, label}`.
|
|
30
|
+
- `slk unread clear` and `slk catchup --batch` now mark group DMs read, and their summaries count them ("Cleared 2 DMs, 1 group DMs, 5 channels and 3 threads"). `unread clear` also reports DMs, which it marked but did not count.
|
|
31
|
+
- Thread headers in `slk unread` and `slk catchup` show a group DM the same way: `#ifams review` for a named one, `@Ada Lovelace, Grace Hopper` for others. Before, they showed the raw `#mpdm-ada--eric--grace-1` slug, and cached it. A slug that other commands already cached is ignored.
|
|
32
|
+
- `slk activity` shows a group DM as `#ifams review` when it is named and `@Ada Lovelace, Grace Hopper` otherwise, in text and in `--json` `channel_name`. Before, it showed and cached the raw `#mpdm-…` slug. Legacy `G…` conversations now show their real name too; they read "Group DM" only when Slack won't say what they are.
|
|
33
|
+
- `slk messages --json` and `slk thread --json` fill `channel_name` from one `conversations.info` lookup when the name isn't cached: `ifams review` for a named group DM, `@Ada Lovelace, Grace Hopper` for others, never the `mpdm-…` slug. DMs and `--no-names` still use the cache only. `slk thread subscribe` and `unsubscribe` name the conversation the same way.
|
|
34
|
+
- `slk messages "#name"`, `slk unread clear "#name"`, and `slk debug channel "#name"` now find named group DMs (`slk messages "#ifams review"`) and any channel you belong to. Lookup pages through `users.conversations` (channels, private channels, and group DMs; archived excluded) and matches names without regard to case. Before, it read one page of `conversations.list`, which Slack caps at 100 channels and which leaves out group DMs. A channel wins over a named group DM with the same name. Public channels you have not joined still fall back to that one page.
|
|
35
|
+
- The channel cache no longer stores group DM `mpdm-*` slugs. These are Slack's internal names, not names anyone types. Slugs cached by older versions are ignored when names are read, and dropped the next time the cache is written. `slk activity` and channel mentions in messages also stop caching named group DMs by name, the same as `slk huddles`.
|
|
36
|
+
- Writing to the channel cache before reading from it no longer replaces the file with just the new entry.
|
|
37
|
+
- `slk unread` no longer prints "No unread messages" after listing unread DMs when no channel is unread.
|
|
38
|
+
- `slk unread clear "#general"` printed "Marked ##general"; it now prints one `#`.
|
|
39
|
+
|
|
10
40
|
## [0.14.0] - 2026-10-09
|
|
11
41
|
|
|
12
42
|
### Added
|
data/README.md
CHANGED
|
@@ -184,8 +184,11 @@ Use `--show-messages` (or `-m`) to preview the actual message content for each a
|
|
|
184
184
|
slk unread # Show unread counts
|
|
185
185
|
slk unread clear # Mark all as read
|
|
186
186
|
slk unread clear general # Mark channel as read
|
|
187
|
+
slk unread clear "#ifams review" # Named group DMs work by name too
|
|
187
188
|
```
|
|
188
189
|
|
|
190
|
+
Unread DMs, group DMs, channels, and threads are all listed. A named group DM shows under its name (`#ifams review`); others show the people in them.
|
|
191
|
+
|
|
189
192
|
### Catchup (Interactive Triage)
|
|
190
193
|
|
|
191
194
|
```bash
|
|
@@ -229,9 +232,11 @@ slk huddles --json # Call id, channel, and participants
|
|
|
229
232
|
```
|
|
230
233
|
|
|
231
234
|
Slack has no huddle list. `slk huddles` reads `huddle_state` from `users.list`
|
|
232
|
-
and groups people who share a call id.
|
|
233
|
-
|
|
234
|
-
Slack Connect guests
|
|
235
|
+
and groups people who share a call id. Profiles no longer say which channel a
|
|
236
|
+
huddle is in, so each call id is looked up with `huddles.get` (undocumented),
|
|
237
|
+
which also lists Slack Connect guests missing from the roster. That only works
|
|
238
|
+
for conversations you can see; any other huddle shows "channel unknown" and
|
|
239
|
+
only its roster members. The roster is
|
|
235
240
|
fetched live and is not cached. `--all` scans every workspace at the same time.
|
|
236
241
|
One connection can only read one roster, so each workspace gets its own.
|
|
237
242
|
People who are not in a huddle are dropped as each page arrives. `slk who`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Api
|
|
5
|
+
# Wrapper for Slack's undocumented huddles.* endpoints (the web client's).
|
|
6
|
+
class Huddles
|
|
7
|
+
def initialize(api_client, workspace)
|
|
8
|
+
@api = api_client
|
|
9
|
+
@workspace = workspace
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The room for one call id: channels, live participants, has_ended.
|
|
13
|
+
# Raises not_authorized for a huddle in a conversation the token
|
|
14
|
+
# cannot see.
|
|
15
|
+
def get(huddle_id)
|
|
16
|
+
info(huddle_id)['huddle'] || {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The whole response, for `slk debug huddle`.
|
|
20
|
+
def info(huddle_id)
|
|
21
|
+
@api.post(@workspace, 'huddles.get', { huddle_id: huddle_id })
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/slk/api/users.rb
CHANGED
|
@@ -143,9 +143,11 @@ module Slk
|
|
|
143
143
|
|
|
144
144
|
public
|
|
145
145
|
|
|
146
|
-
def conversations(cursor: nil, limit: 1000
|
|
147
|
-
|
|
146
|
+
def conversations(cursor: nil, limit: 1000, types: 'public_channel,private_channel,mpim,im',
|
|
147
|
+
exclude_archived: false)
|
|
148
|
+
params = { limit: limit, types: types }
|
|
148
149
|
params[:cursor] = cursor if cursor
|
|
150
|
+
params[:exclude_archived] = true if exclude_archived
|
|
149
151
|
@api.post_form(@workspace, 'users.conversations', params)
|
|
150
152
|
end
|
|
151
153
|
end
|
|
@@ -109,10 +109,16 @@ module Slk
|
|
|
109
109
|
Services::ActivityEnricher.new(
|
|
110
110
|
cache_store: cache_store,
|
|
111
111
|
conversations_api: runner.conversations_api(workspace.name),
|
|
112
|
+
group_dm_label: group_dm_label,
|
|
112
113
|
on_debug: ->(msg) { debug(msg) }
|
|
113
114
|
)
|
|
114
115
|
end
|
|
115
116
|
|
|
117
|
+
# One per run, so each group DM is looked up once however often it appears.
|
|
118
|
+
def group_dm_label
|
|
119
|
+
@group_dm_label ||= Services::GroupDmLabel.new(runner: runner, on_debug: ->(msg) { debug(msg) })
|
|
120
|
+
end
|
|
121
|
+
|
|
116
122
|
def formatter(workspace)
|
|
117
123
|
Formatters::ActivityFormatter.new(
|
|
118
124
|
output: output,
|
data/lib/slk/commands/catchup.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Slk
|
|
|
106
106
|
def batch_mark_workspace(workspace)
|
|
107
107
|
marker = build_unread_marker(workspace)
|
|
108
108
|
counts = marker.mark_all(options: { muted: @options[:muted] })
|
|
109
|
-
success("Marked #{counts[:dms]} DMs, #{counts[:channels]} channels, " \
|
|
109
|
+
success("Marked #{counts[:dms]} DMs, #{counts[:group_dms]} group DMs, #{counts[:channels]} channels, " \
|
|
110
110
|
"and #{counts[:threads]} threads as read on #{workspace.name}")
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -145,18 +145,25 @@ module Slk
|
|
|
145
145
|
|
|
146
146
|
def gather_unread_items(workspace)
|
|
147
147
|
counts = runner.client_api(workspace.name).counts
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
muted_ids = muted_channel_ids(workspace)
|
|
149
|
+
build_items_result(
|
|
150
|
+
filter_unread_ims(counts['ims'] || []),
|
|
151
|
+
filter_unread_channels(counts['mpims'] || [], muted_ids),
|
|
152
|
+
filter_unread_channels(counts['channels'] || [], muted_ids),
|
|
153
|
+
fetch_unread_threads(workspace)
|
|
154
|
+
)
|
|
155
|
+
end
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
def muted_channel_ids(workspace)
|
|
158
|
+
@options[:muted] ? [] : runner.users_api(workspace.name).muted_channels
|
|
153
159
|
end
|
|
154
160
|
|
|
155
|
-
def build_items_result(ims, channels, threads_response)
|
|
161
|
+
def build_items_result(ims, group_dms, channels, threads_response)
|
|
162
|
+
conversations = ims.size + group_dms.size + channels.size
|
|
156
163
|
{
|
|
157
|
-
ims: ims, channels: channels, threads_response: threads_response,
|
|
158
|
-
total:
|
|
159
|
-
empty:
|
|
164
|
+
ims: ims, group_dms: group_dms, channels: channels, threads_response: threads_response,
|
|
165
|
+
total: conversations + (threads_response ? 1 : 0),
|
|
166
|
+
empty: conversations.zero? && !threads_response
|
|
160
167
|
}
|
|
161
168
|
end
|
|
162
169
|
|
|
@@ -164,8 +171,8 @@ module Slk
|
|
|
164
171
|
ims.select { |i| i['has_unreads'] }
|
|
165
172
|
end
|
|
166
173
|
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
# Channels and group DMs; a muted group DM is skipped like a muted channel.
|
|
175
|
+
def filter_unread_channels(channels, muted_ids)
|
|
169
176
|
channels
|
|
170
177
|
.select { |c| c['has_unreads'] || (c['mention_count'] || 0).positive? }
|
|
171
178
|
.reject { |c| muted_ids.include?(c['id']) }
|
|
@@ -180,6 +187,7 @@ module Slk
|
|
|
180
187
|
index = { current: 0, total: items[:total] }
|
|
181
188
|
|
|
182
189
|
return :quit if process_dms(workspace, items[:ims], index)
|
|
190
|
+
return :quit if process_group_dms(workspace, items[:group_dms], index)
|
|
183
191
|
return :quit if items[:threads_response] && process_threads_item(workspace, items[:threads_response], index)
|
|
184
192
|
return :quit if process_channels(workspace, items[:channels], index)
|
|
185
193
|
|
|
@@ -196,6 +204,16 @@ module Slk
|
|
|
196
204
|
false
|
|
197
205
|
end
|
|
198
206
|
|
|
207
|
+
def process_group_dms(workspace, group_dms, index)
|
|
208
|
+
group_dms.each do |group_dm|
|
|
209
|
+
label = group_dm_label.label(workspace, group_dm['id'])
|
|
210
|
+
return true if process_conversation(workspace, group_dm, index[:current], index[:total], label) == :quit
|
|
211
|
+
|
|
212
|
+
index[:current] += 1
|
|
213
|
+
end
|
|
214
|
+
false
|
|
215
|
+
end
|
|
216
|
+
|
|
199
217
|
def process_channels(workspace, channels, index)
|
|
200
218
|
channels.each do |channel|
|
|
201
219
|
return true if process_channel(workspace, channel, index[:current], index[:total]) == :quit
|
data/lib/slk/commands/debug.rb
CHANGED
|
@@ -25,11 +25,13 @@ module Slk
|
|
|
25
25
|
in ['profile'] then dump_profile(nil)
|
|
26
26
|
in ['team'] then dump_team
|
|
27
27
|
in ['schema'] then dump_schema
|
|
28
|
-
in [
|
|
28
|
+
in [action, target] if TARGET_ACTIONS.key?(action) then send(TARGET_ACTIONS[action], target)
|
|
29
29
|
else unknown_action
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
TARGET_ACTIONS = { 'thread' => :dump_thread, 'channel' => :dump_channel, 'huddle' => :dump_huddle }.freeze
|
|
34
|
+
|
|
33
35
|
private
|
|
34
36
|
|
|
35
37
|
def dump_profile(user_input)
|
|
@@ -61,7 +63,7 @@ module Slk
|
|
|
61
63
|
# carry `subscribed`/`last_read`?) and subscriptions.thread.get. Each call
|
|
62
64
|
# is captured separately so one failing does not hide the other.
|
|
63
65
|
def dump_thread(url)
|
|
64
|
-
resolved =
|
|
66
|
+
resolved = resolve_target(url)
|
|
65
67
|
ts = resolved.thread_ts || resolved.msg_ts
|
|
66
68
|
raise UsageError, 'URL must point to a specific message' unless ts
|
|
67
69
|
|
|
@@ -69,9 +71,38 @@ module Slk
|
|
|
69
71
|
0
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
# Raw conversations.info: shows the flags labels key off (is_mpim,
|
|
75
|
+
# properties.has_custom_mpdm_name for named group DMs, is_ext_shared).
|
|
76
|
+
def dump_channel(target)
|
|
77
|
+
resolved = resolve_target(target)
|
|
78
|
+
info = runner.conversations_api(resolved.workspace.name).info(channel: resolved.channel_id)
|
|
79
|
+
output.puts(JSON.pretty_generate(info))
|
|
80
|
+
0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Raw huddles.get for a call id (R...), or for the call a user is in
|
|
84
|
+
# now. Profiles carry only the call id; this is where the channel and
|
|
85
|
+
# the full participant list (Slack Connect guests too) come from.
|
|
86
|
+
def dump_huddle(target)
|
|
87
|
+
workspace = runner.workspace(@options[:workspace])
|
|
88
|
+
call_id = huddle_call_id(workspace, target)
|
|
89
|
+
output.puts(JSON.pretty_generate(Api::Huddles.new(api_client, workspace).info(call_id)))
|
|
90
|
+
0
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def huddle_call_id(workspace, target)
|
|
94
|
+
return target if target.match?(/\AR[A-Z0-9]+\z/)
|
|
95
|
+
|
|
96
|
+
user_id = resolve_user_id(workspace, target)
|
|
97
|
+
call_id = runner.users_api(workspace.name).info(user_id).dig('user', 'profile', 'huddle_state_call_id')
|
|
98
|
+
raise ApiError, "#{target} is not in a huddle" if call_id.to_s.empty?
|
|
99
|
+
|
|
100
|
+
call_id
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def resolve_target(target)
|
|
73
104
|
Services::TargetResolver.new(runner: runner, cache_store: cache_store)
|
|
74
|
-
.resolve(
|
|
105
|
+
.resolve(target, default_workspace: runner.workspace(@options[:workspace]))
|
|
75
106
|
end
|
|
76
107
|
|
|
77
108
|
def thread_calls(workspace_name, channel, thread_ts)
|
|
@@ -118,7 +149,7 @@ module Slk
|
|
|
118
149
|
|
|
119
150
|
def unknown_action
|
|
120
151
|
error("Unknown debug action: #{positional_args.first.inspect}")
|
|
121
|
-
error('Valid actions: profile [user], team, schema, thread <url>')
|
|
152
|
+
error('Valid actions: profile [user], team, schema, thread <url>, channel <id|url>, huddle <call|user>')
|
|
122
153
|
1
|
|
123
154
|
end
|
|
124
155
|
|
|
@@ -135,6 +166,8 @@ module Slk
|
|
|
135
166
|
team Dump team.info
|
|
136
167
|
schema Dump team.profile.get
|
|
137
168
|
thread <url> Dump thread parent + subscriptions.thread.get
|
|
169
|
+
channel <id|url> Dump conversations.info (also #name)
|
|
170
|
+
huddle <call|user> Dump huddles.get for a call id (R...) or a user's current call
|
|
138
171
|
|
|
139
172
|
USER
|
|
140
173
|
(none) | me Self
|
data/lib/slk/commands/huddles.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Slk
|
|
|
25
25
|
help.note('Slack has no huddle list. This reads huddle_state from users.list')
|
|
26
26
|
help.note('and groups people who share a call id. The roster is fetched live.')
|
|
27
27
|
help.note('Primary workspace only. --all scans every workspace at the same time.')
|
|
28
|
-
help.note('
|
|
28
|
+
help.note('Channels and Slack Connect guests come from huddles.get, so only')
|
|
29
|
+
help.note('huddles in conversations you can see get them.')
|
|
29
30
|
add_options_section(help)
|
|
30
31
|
add_examples_section(help)
|
|
31
32
|
help.render
|
|
@@ -10,6 +10,7 @@ module Slk
|
|
|
10
10
|
class Messages < Base
|
|
11
11
|
include Support::InlineImages
|
|
12
12
|
include Support::AttachmentFetching
|
|
13
|
+
include Support::UserResolver
|
|
13
14
|
|
|
14
15
|
# rubocop:disable Metrics/MethodLength
|
|
15
16
|
def execute
|
|
@@ -149,7 +150,7 @@ module Slk
|
|
|
149
150
|
end
|
|
150
151
|
|
|
151
152
|
def output_json_messages(messages, workspace, channel_id)
|
|
152
|
-
format_options = json_format_options(channel_id)
|
|
153
|
+
format_options = json_format_options(workspace, channel_id)
|
|
153
154
|
|
|
154
155
|
json_messages = messages.map do |message|
|
|
155
156
|
format_message_as_json(message, workspace, channel_id, format_options)
|
|
@@ -158,8 +159,24 @@ module Slk
|
|
|
158
159
|
output_json(json_messages)
|
|
159
160
|
end
|
|
160
161
|
|
|
161
|
-
def json_format_options(channel_id)
|
|
162
|
-
{ no_names: @options[:no_names], reaction_timestamps: @options[:reaction_timestamps], channel_id: channel_id
|
|
162
|
+
def json_format_options(workspace, channel_id)
|
|
163
|
+
{ no_names: @options[:no_names], reaction_timestamps: @options[:reaction_timestamps], channel_id: channel_id,
|
|
164
|
+
channel_name: json_channel_name(workspace, channel_id) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# One lookup per run: a group DM gets its name or "@other, members"
|
|
168
|
+
# (not its mpdm-* slug). DMs and --no-names keep the cache-only name.
|
|
169
|
+
def json_channel_name(workspace, channel_id)
|
|
170
|
+
return nil if @options[:no_names] || channel_id.start_with?('D')
|
|
171
|
+
|
|
172
|
+
conversation_label(workspace, channel_id)&.delete_prefix('#')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# "#name", "@other, members" for a group DM, or "@user" for a DM;
|
|
176
|
+
# nil when Slack won't say.
|
|
177
|
+
def conversation_label(workspace, channel_id)
|
|
178
|
+
label = resolve_conversation_label(workspace, channel_id)
|
|
179
|
+
label unless label == "##{channel_id}"
|
|
163
180
|
end
|
|
164
181
|
|
|
165
182
|
def format_message_as_json(message, workspace, channel_id, format_options)
|
data/lib/slk/commands/thread.rb
CHANGED
|
@@ -130,8 +130,7 @@ module Slk
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def channel_label(workspace, channel_id)
|
|
133
|
-
|
|
134
|
-
name ? "##{name}" : channel_id
|
|
133
|
+
conversation_label(workspace, channel_id) || channel_id
|
|
135
134
|
end
|
|
136
135
|
|
|
137
136
|
def subscription_error(action, err)
|
data/lib/slk/commands/unread.rb
CHANGED
|
@@ -133,6 +133,7 @@ module Slk
|
|
|
133
133
|
|
|
134
134
|
{
|
|
135
135
|
unread_ims: filter_unread_ims(counts['ims'] || []),
|
|
136
|
+
unread_group_dms: filter_unread_channels(counts['mpims'] || [], muted_ids),
|
|
136
137
|
unread_channels: filter_unread_channels(counts['channels'] || [], muted_ids)
|
|
137
138
|
}
|
|
138
139
|
end
|
|
@@ -154,10 +155,16 @@ module Slk
|
|
|
154
155
|
def unread_json(workspace, data)
|
|
155
156
|
{
|
|
156
157
|
channels: data[:unread_channels].map { |c| format_channel_json(workspace, c) },
|
|
157
|
-
dms: data[:unread_ims].map { |i| format_dm_json(workspace, i) }
|
|
158
|
+
dms: data[:unread_ims].map { |i| format_dm_json(workspace, i) },
|
|
159
|
+
group_dms: data[:unread_group_dms].map { |g| format_group_dm_json(workspace, g) }
|
|
158
160
|
}
|
|
159
161
|
end
|
|
160
162
|
|
|
163
|
+
def format_group_dm_json(workspace, group_dm)
|
|
164
|
+
{ id: group_dm['id'], mentions: group_dm['mention_count'],
|
|
165
|
+
label: group_dm_label.label(workspace, group_dm['id']) }
|
|
166
|
+
end
|
|
167
|
+
|
|
161
168
|
def format_channel_json(workspace, channel)
|
|
162
169
|
channel_hash = { id: channel['id'], mentions: channel['mention_count'] }
|
|
163
170
|
channel_name = cache_store.get_channel_name(workspace.name, channel['id'])
|
|
@@ -180,23 +187,37 @@ module Slk
|
|
|
180
187
|
formatter = runner.message_formatter
|
|
181
188
|
|
|
182
189
|
display_unread_dms(workspace, data[:unread_ims], conversations_api, formatter)
|
|
183
|
-
|
|
190
|
+
display_unread_group_dms(workspace, data[:unread_group_dms], conversations_api, formatter)
|
|
191
|
+
display_unread_channels(workspace, data, conversations_api, formatter)
|
|
184
192
|
show_threads(workspace, formatter)
|
|
185
193
|
end
|
|
186
194
|
|
|
187
195
|
def display_unread_dms(workspace, unread_ims, conversations_api, formatter)
|
|
188
196
|
unread_ims.each do |im|
|
|
189
|
-
mention_count = im['mention_count'] || 0
|
|
190
197
|
user_name = resolve_dm_user_name(workspace, im['id'], conversations_api)
|
|
191
|
-
|
|
192
|
-
puts output.bold("@#{user_name}") + (mention_count.positive? ? " (#{mention_count} mentions)" : '')
|
|
193
|
-
puts
|
|
198
|
+
print_conversation_header("@#{user_name}", im['mention_count'] || 0)
|
|
194
199
|
show_channel_messages(workspace, im['id'], @options[:limit], conversations_api, formatter)
|
|
195
200
|
end
|
|
196
201
|
end
|
|
197
202
|
|
|
198
|
-
def
|
|
199
|
-
|
|
203
|
+
def display_unread_group_dms(workspace, group_dms, conversations_api, formatter)
|
|
204
|
+
group_dms.each do |group_dm|
|
|
205
|
+
print_conversation_header(group_dm_label.label(workspace, group_dm['id']), group_dm['mention_count'] || 0)
|
|
206
|
+
show_channel_messages(workspace, group_dm['id'], @options[:limit], conversations_api, formatter)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def print_conversation_header(label, mention_count)
|
|
211
|
+
puts
|
|
212
|
+
puts output.bold(label) + (mention_count.positive? ? " (#{mention_count} mentions)" : '')
|
|
213
|
+
puts
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# "No unread messages" only when nothing at all is unread, not merely
|
|
217
|
+
# when the DMs and group DMs above are the only unreads.
|
|
218
|
+
def display_unread_channels(workspace, data, conversations_api, formatter)
|
|
219
|
+
unreads = data[:unread_channels]
|
|
220
|
+
return puts('No unread messages') if [unreads, data[:unread_ims], data[:unread_group_dms]].all?(&:empty?)
|
|
200
221
|
|
|
201
222
|
unreads.each { |ch| display_channel(workspace, ch, conversations_api, formatter) }
|
|
202
223
|
end
|
|
@@ -283,12 +304,13 @@ module Slk
|
|
|
283
304
|
channel_id = resolve_channel_id(workspace, channel_name)
|
|
284
305
|
return unless unread_marker(workspace).mark_single_channel(channel_id)
|
|
285
306
|
|
|
286
|
-
success("Marked ##{channel_name} as read on #{workspace.name}")
|
|
307
|
+
success("Marked ##{channel_name.delete_prefix('#')} as read on #{workspace.name}")
|
|
287
308
|
end
|
|
288
309
|
|
|
289
310
|
def clear_all_channels(workspace)
|
|
290
311
|
counts = unread_marker(workspace).mark_all(options: { muted: @options[:muted] })
|
|
291
|
-
success("Cleared #{counts[:
|
|
312
|
+
success("Cleared #{counts[:dms]} DMs, #{counts[:group_dms]} group DMs, #{counts[:channels]} channels " \
|
|
313
|
+
"and #{counts[:threads]} threads on #{workspace.name}")
|
|
292
314
|
end
|
|
293
315
|
|
|
294
316
|
def unread_marker(workspace)
|
|
@@ -305,15 +327,8 @@ module Slk
|
|
|
305
327
|
return channel_name if channel_name.match?(/^[CDG][A-Z0-9]+$/)
|
|
306
328
|
|
|
307
329
|
name = channel_name.delete_prefix('#')
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
def resolve_channel(workspace, name)
|
|
312
|
-
api = runner.conversations_api(workspace.name)
|
|
313
|
-
response = api.list
|
|
314
|
-
channels = response['channels'] || []
|
|
315
|
-
channel = channels.find { |c| c['name'] == name }
|
|
316
|
-
channel&.dig('id') || raise(ConfigError, "Channel not found: ##{name}")
|
|
330
|
+
Services::ChannelLookup.new(runner: runner, cache_store: cache_store).find_id(workspace, name) ||
|
|
331
|
+
raise(ConfigError, "Channel not found: ##{name}")
|
|
317
332
|
end
|
|
318
333
|
end
|
|
319
334
|
# rubocop:enable Metrics/ClassLength
|
data/lib/slk/commands/who.rb
CHANGED
|
@@ -89,12 +89,13 @@ module Slk
|
|
|
89
89
|
# Channel ids are useless on a card. A failed lookup still leaves the
|
|
90
90
|
# state, which is the part Slack always sends.
|
|
91
91
|
def attach_huddle_channel(workspace, profile)
|
|
92
|
-
|
|
92
|
+
channel_id = Services::HuddleRoom.new(api_client: api_client, workspace: workspace).channel_for(profile)
|
|
93
|
+
return profile if channel_id.to_s.empty?
|
|
93
94
|
|
|
94
|
-
label = huddle_channels.label(workspace,
|
|
95
|
+
label = huddle_channels.label(workspace, channel_id)
|
|
95
96
|
return profile unless label
|
|
96
97
|
|
|
97
|
-
Models::Profile.new(**profile.to_h, huddle_channel: label)
|
|
98
|
+
Models::Profile.new(**profile.to_h, huddle_channel_id: channel_id, huddle_channel: label)
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def huddle_channels
|
|
@@ -87,7 +87,7 @@ module Slk
|
|
|
87
87
|
workspace_name = workspace&.name
|
|
88
88
|
return unless workspace_name
|
|
89
89
|
|
|
90
|
-
channel_name = @cache.get_channel_name(workspace_name, options[:channel_id])
|
|
90
|
+
channel_name = options[:channel_name] || @cache.get_channel_name(workspace_name, options[:channel_id])
|
|
91
91
|
result[:channel_name] = channel_name if channel_name
|
|
92
92
|
end
|
|
93
93
|
end
|
|
@@ -109,14 +109,20 @@ module Slk
|
|
|
109
109
|
response = Api::Conversations.new(@api, workspace).info(channel: channel_id)
|
|
110
110
|
return nil unless response['ok'] && response['channel']
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
@cache.set_channel(workspace.name, name, channel_id) if name
|
|
114
|
-
name
|
|
112
|
+
remember_channel_name(workspace, channel_id, response['channel'])
|
|
115
113
|
rescue ApiError => e
|
|
116
114
|
@on_debug&.call("Channel lookup failed for #{channel_id}: #{e.message}")
|
|
117
115
|
nil
|
|
118
116
|
end
|
|
119
117
|
|
|
118
|
+
# Group DMs stay out of the name cache: a named one can be renamed or
|
|
119
|
+
# share a real channel's name, and an mpdm-* slug isn't a name.
|
|
120
|
+
def remember_channel_name(workspace, channel_id, channel)
|
|
121
|
+
name = channel['name']
|
|
122
|
+
@cache.set_channel(workspace.name, name, channel_id) if name && !channel['is_mpim']
|
|
123
|
+
name
|
|
124
|
+
end
|
|
125
|
+
|
|
120
126
|
def lookup_subteam_handle(workspace, subteam_id)
|
|
121
127
|
cached = @cache.get_subteam(workspace.name, subteam_id)
|
|
122
128
|
return cached if cached
|
|
@@ -37,7 +37,11 @@ module Slk
|
|
|
37
37
|
channel_label_for(result.channel_type, result.channel_name, workspace)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Named group DMs show as #name; their name is not a user ID to resolve.
|
|
40
41
|
def channel_label_for(type, name, workspace)
|
|
42
|
+
named = type == 'mpim' && Support::GroupDmName.label(name)
|
|
43
|
+
return named if named
|
|
44
|
+
|
|
41
45
|
if %w[im mpim].include?(type)
|
|
42
46
|
@mentions.replace("<@#{name}>", workspace)
|
|
43
47
|
else
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Slk
|
|
4
4
|
module Services
|
|
5
|
-
# Enriches activity feed items with resolved user/channel names
|
|
5
|
+
# Enriches activity feed items with resolved user/channel names.
|
|
6
|
+
# Group DMs show as "#name" when named, else "@other, members" (given a
|
|
7
|
+
# GroupDmLabel), never as their mpdm-* slug, and are never cached by name.
|
|
6
8
|
class ActivityEnricher
|
|
7
|
-
def initialize(cache_store:, conversations_api:, on_debug: nil)
|
|
9
|
+
def initialize(cache_store:, conversations_api:, group_dm_label: nil, on_debug: nil)
|
|
8
10
|
@cache = cache_store
|
|
9
11
|
@conversations_api = conversations_api
|
|
12
|
+
@group_dm_label = group_dm_label
|
|
10
13
|
@on_debug = on_debug
|
|
11
14
|
end
|
|
12
15
|
|
|
@@ -40,15 +43,16 @@ module Slk
|
|
|
40
43
|
@cache.get_user(workspace.name, user_id) || user_id
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
# Resolve channel ID to
|
|
46
|
+
# Resolve channel ID to a label (with API fallback). Legacy G ids are
|
|
47
|
+
# group DMs or old private channels; they read "Group DM" if Slack
|
|
48
|
+
# won't say which.
|
|
44
49
|
def resolve_channel(workspace, channel_id, with_hash: true)
|
|
45
50
|
return 'DM' if channel_id.start_with?('D')
|
|
46
|
-
return 'Group DM' if channel_id.start_with?('G')
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
return channel_id unless
|
|
52
|
+
label = channel_label(workspace, channel_id)
|
|
53
|
+
return channel_id.start_with?('G') ? 'Group DM' : channel_id unless label
|
|
50
54
|
|
|
51
|
-
with_hash ?
|
|
55
|
+
with_hash ? label : label.delete_prefix('#')
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
private
|
|
@@ -101,21 +105,37 @@ module Slk
|
|
|
101
105
|
data['channel_name'] = resolve_channel(workspace, channel_id, with_hash: false) if channel_id
|
|
102
106
|
end
|
|
103
107
|
|
|
104
|
-
def
|
|
108
|
+
def channel_label(workspace, channel_id)
|
|
109
|
+
known = @group_dm_label&.known(workspace, channel_id)
|
|
110
|
+
return known if known
|
|
111
|
+
|
|
105
112
|
cached = @cache.get_channel_name(workspace.name, channel_id)
|
|
106
|
-
return cached if cached
|
|
113
|
+
return "##{cached}" if cached
|
|
114
|
+
|
|
115
|
+
fetch_channel_label(workspace, channel_id)
|
|
116
|
+
end
|
|
107
117
|
|
|
118
|
+
def fetch_channel_label(workspace, channel_id)
|
|
108
119
|
response = @conversations_api.info(channel: channel_id)
|
|
109
|
-
|
|
120
|
+
channel = response['channel'] if response['ok']
|
|
121
|
+
return nil unless channel
|
|
122
|
+
return group_dm_label(workspace, channel_id, channel) if channel['is_mpim']
|
|
123
|
+
return nil unless channel['name']
|
|
110
124
|
|
|
111
|
-
name
|
|
112
|
-
|
|
113
|
-
name
|
|
125
|
+
@cache.set_channel(workspace.name, channel['name'], channel_id)
|
|
126
|
+
"##{channel['name']}"
|
|
114
127
|
rescue ApiError => e
|
|
115
128
|
@on_debug&.call("Could not resolve channel #{channel_id}: #{e.message}")
|
|
116
129
|
nil
|
|
117
130
|
end
|
|
118
131
|
|
|
132
|
+
def group_dm_label(workspace, channel_id, channel)
|
|
133
|
+
return @group_dm_label.label_from(workspace, channel_id, channel) if @group_dm_label
|
|
134
|
+
|
|
135
|
+
named = Support::GroupDmName.custom(channel)
|
|
136
|
+
named ? "##{named}" : 'Group DM'
|
|
137
|
+
end
|
|
138
|
+
|
|
119
139
|
def debug_missing(item_type, missing_data)
|
|
120
140
|
@on_debug&.call("Could not enrich #{item_type} item - missing #{missing_data} data")
|
|
121
141
|
end
|