slk 0.14.0 → 0.15.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 +20 -0
- data/README.md +3 -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 +15 -4
- 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/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/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/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 +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '080abcf262e2c943cb01e3fe6fc5619ee0921d93d4418deeb5f42d6fc4154a92'
|
|
4
|
+
data.tar.gz: 762989ed0de46ef13372d550a80517bf2c3c7ce73eaf916c8d78fafd5d6e6f57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc5bc2aafe45a6ab8a20fac51086e426d2586012be06b82d87fa3560597386b6d0a504a1489b8b75cf9af5c00627dd7b10a2714af8479fee37202408963e10b8
|
|
7
|
+
data.tar.gz: a835b18a0a8155c73d2618fa484393dcfaf9b11fbbd0583530209b810e0cadd09ddf45fa5618c54a75f9581dfd0a165d35257cd6faa0add128c08a51d03b0047
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.15.0] - 2026-10-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
- `slk debug channel <id|url|#name>` (hidden command) prints the raw `conversations.info` response.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `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}`.
|
|
20
|
+
- `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.
|
|
21
|
+
- 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.
|
|
22
|
+
- `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.
|
|
23
|
+
- `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.
|
|
24
|
+
- `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.
|
|
25
|
+
- 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`.
|
|
26
|
+
- Writing to the channel cache before reading from it no longer replaces the file with just the new entry.
|
|
27
|
+
- `slk unread` no longer prints "No unread messages" after listing unread DMs when no channel is unread.
|
|
28
|
+
- `slk unread clear "#general"` printed "Marked ##general"; it now prints one `#`.
|
|
29
|
+
|
|
10
30
|
## [0.14.0] - 2026-10-09
|
|
11
31
|
|
|
12
32
|
### 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
|
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
|
@@ -26,6 +26,7 @@ module Slk
|
|
|
26
26
|
in ['team'] then dump_team
|
|
27
27
|
in ['schema'] then dump_schema
|
|
28
28
|
in ['thread', url] then dump_thread(url)
|
|
29
|
+
in ['channel', target] then dump_channel(target)
|
|
29
30
|
else unknown_action
|
|
30
31
|
end
|
|
31
32
|
end
|
|
@@ -61,7 +62,7 @@ module Slk
|
|
|
61
62
|
# carry `subscribed`/`last_read`?) and subscriptions.thread.get. Each call
|
|
62
63
|
# is captured separately so one failing does not hide the other.
|
|
63
64
|
def dump_thread(url)
|
|
64
|
-
resolved =
|
|
65
|
+
resolved = resolve_target(url)
|
|
65
66
|
ts = resolved.thread_ts || resolved.msg_ts
|
|
66
67
|
raise UsageError, 'URL must point to a specific message' unless ts
|
|
67
68
|
|
|
@@ -69,9 +70,18 @@ module Slk
|
|
|
69
70
|
0
|
|
70
71
|
end
|
|
71
72
|
|
|
72
|
-
|
|
73
|
+
# Raw conversations.info: shows the flags labels key off (is_mpim,
|
|
74
|
+
# properties.has_custom_mpdm_name for named group DMs, is_ext_shared).
|
|
75
|
+
def dump_channel(target)
|
|
76
|
+
resolved = resolve_target(target)
|
|
77
|
+
info = runner.conversations_api(resolved.workspace.name).info(channel: resolved.channel_id)
|
|
78
|
+
output.puts(JSON.pretty_generate(info))
|
|
79
|
+
0
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def resolve_target(target)
|
|
73
83
|
Services::TargetResolver.new(runner: runner, cache_store: cache_store)
|
|
74
|
-
.resolve(
|
|
84
|
+
.resolve(target, default_workspace: runner.workspace(@options[:workspace]))
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
def thread_calls(workspace_name, channel, thread_ts)
|
|
@@ -118,7 +128,7 @@ module Slk
|
|
|
118
128
|
|
|
119
129
|
def unknown_action
|
|
120
130
|
error("Unknown debug action: #{positional_args.first.inspect}")
|
|
121
|
-
error('Valid actions: profile [user], team, schema, thread <url>')
|
|
131
|
+
error('Valid actions: profile [user], team, schema, thread <url>, channel <id|url>')
|
|
122
132
|
1
|
|
123
133
|
end
|
|
124
134
|
|
|
@@ -135,6 +145,7 @@ module Slk
|
|
|
135
145
|
team Dump team.info
|
|
136
146
|
schema Dump team.profile.get
|
|
137
147
|
thread <url> Dump thread parent + subscriptions.thread.get
|
|
148
|
+
channel <id|url> Dump conversations.info (also #name)
|
|
138
149
|
|
|
139
150
|
USER
|
|
140
151
|
(none) | me Self
|
|
@@ -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
|
|
@@ -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
|
|
@@ -110,16 +110,23 @@ module Slk
|
|
|
110
110
|
result
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
+
# Group DM mpdm-* slugs are never returned (older versions cached them),
|
|
114
|
+
# so callers fall through to a real label.
|
|
113
115
|
def get_channel_name(workspace_name, channel_id)
|
|
114
116
|
load_channel_cache(workspace_name)
|
|
115
117
|
cache = @channel_cache[workspace_name] || {}
|
|
116
|
-
result = cache.
|
|
118
|
+
result = cache.find { |name, id| id == channel_id && !group_dm_slug?(name) }&.first
|
|
117
119
|
log_cache_access('channel_name', workspace_name, channel_id, result)
|
|
118
120
|
result
|
|
119
121
|
end
|
|
120
122
|
|
|
123
|
+
# Refuses group DM mpdm-* slugs: they are Slack's internal names, not
|
|
124
|
+
# what people type or see. Writing drops any slugs cached before this.
|
|
121
125
|
def set_channel(workspace_name, channel_name, channel_id)
|
|
122
|
-
|
|
126
|
+
return if group_dm_slug?(channel_name)
|
|
127
|
+
|
|
128
|
+
load_channel_cache(workspace_name)
|
|
129
|
+
@channel_cache[workspace_name].reject! { |name, _id| group_dm_slug?(name) }
|
|
123
130
|
@channel_cache[workspace_name][channel_name] = channel_id
|
|
124
131
|
save_channel_cache(workspace_name)
|
|
125
132
|
end
|
|
@@ -184,6 +191,10 @@ module Slk
|
|
|
184
191
|
|
|
185
192
|
private
|
|
186
193
|
|
|
194
|
+
def group_dm_slug?(name)
|
|
195
|
+
name.to_s.match?(Support::GroupDmName::SLUG)
|
|
196
|
+
end
|
|
197
|
+
|
|
187
198
|
def load_user_cache(workspace_name)
|
|
188
199
|
return if @user_cache.key?(workspace_name)
|
|
189
200
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Services
|
|
5
|
+
# Finds a conversation id from a name typed as `#name`.
|
|
6
|
+
#
|
|
7
|
+
# Order: the channel cache, then every page of the conversations you
|
|
8
|
+
# belong to (users.conversations: public and private channels plus group
|
|
9
|
+
# DMs, so a named group DM such as "ifams review" resolves), then one page
|
|
10
|
+
# of conversations.list for public channels you have not joined. That
|
|
11
|
+
# last step is only one page, as before: the full workspace list runs to
|
|
12
|
+
# thousands of channels at 100 per page.
|
|
13
|
+
#
|
|
14
|
+
# A channel wins over a named group DM with the same name. A group DM is
|
|
15
|
+
# cached only after a full scan finds no channel of that name among yours.
|
|
16
|
+
class ChannelLookup
|
|
17
|
+
MEMBER_TYPES = 'public_channel,private_channel,mpim'
|
|
18
|
+
MAX_PAGES = 50
|
|
19
|
+
|
|
20
|
+
def initialize(runner:, cache_store:)
|
|
21
|
+
@runner = runner
|
|
22
|
+
@cache = cache_store
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param workspace [Models::Workspace]
|
|
26
|
+
# @param name [String] channel or group DM name, without the leading #
|
|
27
|
+
# @return [String, nil] the conversation id, or nil if none matches
|
|
28
|
+
def find_id(workspace, name)
|
|
29
|
+
@cache.get_channel_id(workspace.name, name) || lookup(workspace, name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def lookup(workspace, name)
|
|
35
|
+
channel = member_conversation(workspace, name) || listed_channel(workspace, name)
|
|
36
|
+
return nil unless channel
|
|
37
|
+
|
|
38
|
+
@cache.set_channel(workspace.name, channel['name'], channel['id'])
|
|
39
|
+
channel['id']
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def member_conversation(workspace, name)
|
|
43
|
+
group_dm = nil
|
|
44
|
+
each_member_page(workspace) do |conversations|
|
|
45
|
+
conversations.each do |conversation|
|
|
46
|
+
next unless name_matches?(conversation, name)
|
|
47
|
+
return conversation unless conversation['is_mpim']
|
|
48
|
+
|
|
49
|
+
group_dm ||= conversation
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
group_dm
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def each_member_page(workspace)
|
|
56
|
+
api = @runner.users_api(workspace.name)
|
|
57
|
+
cursor = nil
|
|
58
|
+
MAX_PAGES.times do
|
|
59
|
+
response = api.conversations(cursor: cursor, types: MEMBER_TYPES, exclude_archived: true)
|
|
60
|
+
yield response['channels'] || []
|
|
61
|
+
cursor = response.dig('response_metadata', 'next_cursor').to_s
|
|
62
|
+
break if cursor.empty?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def listed_channel(workspace, name)
|
|
67
|
+
channels = @runner.conversations_api(workspace.name).list['channels'] || []
|
|
68
|
+
channels.find { |channel| name_matches?(channel, name) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Channel names are lowercase; a group DM's name keeps whatever case
|
|
72
|
+
# it was given, so "#IFAMS Review" and "#ifams review" both find it.
|
|
73
|
+
def name_matches?(conversation, name)
|
|
74
|
+
conversation['name'].to_s.casecmp?(name)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Services
|
|
5
|
+
# Header label for a group DM in unread and catchup (conversation and
|
|
6
|
+
# thread headers). A named group DM
|
|
7
|
+
# shows its name ("#ifams review"); any other shows the people in it
|
|
8
|
+
# besides you ("@Ada Lovelace, Grace Hopper") rather than the opaque
|
|
9
|
+
# mpdm-* slug. One conversations.info per chat, remembered for the run.
|
|
10
|
+
class GroupDmLabel
|
|
11
|
+
def initialize(runner:, on_debug: nil)
|
|
12
|
+
@runner = runner
|
|
13
|
+
@on_debug = on_debug
|
|
14
|
+
@labels = {}
|
|
15
|
+
@self_ids = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @return [String] "#name", "@names", or "#<channel id>" if Slack refuses
|
|
19
|
+
def label(workspace, channel_id)
|
|
20
|
+
@labels[[workspace.name, channel_id]] ||= fetch_label(workspace, channel_id)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [String, nil] a label already worked out this run, if any
|
|
24
|
+
def known(workspace, channel_id)
|
|
25
|
+
@labels[[workspace.name, channel_id]]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Same label when the caller already holds conversations.info's channel.
|
|
29
|
+
def label_from(workspace, channel_id, channel)
|
|
30
|
+
@labels[[workspace.name, channel_id]] ||= label_for(workspace, channel) || "##{channel_id}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def fetch_label(workspace, channel_id)
|
|
36
|
+
channel = @runner.conversations_api(workspace.name).info(channel: channel_id)['channel'] || {}
|
|
37
|
+
label_for(workspace, channel) || "##{channel_id}"
|
|
38
|
+
rescue ApiError => e
|
|
39
|
+
@on_debug&.call("Group DM lookup failed for #{channel_id}: #{e.message}")
|
|
40
|
+
"##{channel_id}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def label_for(workspace, channel)
|
|
44
|
+
named = Support::GroupDmName.custom(channel)
|
|
45
|
+
return "##{named}" if named
|
|
46
|
+
|
|
47
|
+
names = member_names(workspace, channel['members'] || [])
|
|
48
|
+
return "@#{names.join(', ')}" if names.any?
|
|
49
|
+
|
|
50
|
+
"##{channel['name']}" unless channel['name'].to_s.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def member_names(workspace, members)
|
|
54
|
+
self_id = self_user_id(workspace)
|
|
55
|
+
others = members.reject { |id| id == self_id }
|
|
56
|
+
others = members if others.empty?
|
|
57
|
+
others.map { |id| user_name(workspace, id) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def user_name(workspace, user_id)
|
|
61
|
+
UserLookup.new(cache_store: @runner.cache_store, workspace: workspace,
|
|
62
|
+
api_client: @runner.api_client, on_debug: @on_debug).resolve_name_or_bot(user_id) || user_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Same meta key the sent and who commands use, so one auth.test serves all.
|
|
66
|
+
def self_user_id(workspace)
|
|
67
|
+
@self_ids.fetch(workspace.name) do
|
|
68
|
+
@self_ids[workspace.name] = @runner.cache_store.get_meta(workspace.name, 'self_user_id') ||
|
|
69
|
+
fetch_self_user_id(workspace)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fetch_self_user_id(workspace)
|
|
74
|
+
id = @runner.client_api(workspace.name).auth_test['user_id']
|
|
75
|
+
@runner.cache_store.set_meta(workspace.name, 'self_user_id', id) if id
|
|
76
|
+
id
|
|
77
|
+
rescue ApiError => e
|
|
78
|
+
@on_debug&.call("Could not identify self in #{workspace.name}: #{e.message}")
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -38,11 +38,19 @@ module Slk
|
|
|
38
38
|
|
|
39
39
|
def label_from(workspace, channel_id, channel)
|
|
40
40
|
return 'DM' if channel['is_im']
|
|
41
|
-
return
|
|
41
|
+
return group_label(channel) if channel['is_mpim']
|
|
42
42
|
|
|
43
43
|
named_label(workspace, channel_id, channel['name'])
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# A named group DM shows its name but stays out of the channel cache:
|
|
47
|
+
# the name can be changed at any time and can match a real channel's
|
|
48
|
+
# name, and the cache maps names to ids, so `#name` would go to the chat.
|
|
49
|
+
def group_label(channel)
|
|
50
|
+
name = Support::GroupDmName.custom(channel)
|
|
51
|
+
name ? "##{name}" : 'group DM'
|
|
52
|
+
end
|
|
53
|
+
|
|
46
54
|
def named_label(workspace, channel_id, name)
|
|
47
55
|
return nil if name.to_s.empty?
|
|
48
56
|
|
|
@@ -5,6 +5,7 @@ module Slk
|
|
|
5
5
|
# Resolves sent-conversation destinations while retaining raw channel IDs
|
|
6
6
|
# and names in JSON. Group DMs need member lookup: their Slack name is an
|
|
7
7
|
# opaque mpdm-* slug, not a user ID that MentionReplacer can resolve.
|
|
8
|
+
# Named group DMs ("named chats") skip that and show as #name.
|
|
8
9
|
class SentChannelLabel
|
|
9
10
|
USER_ID_PATTERN = /\A[UW][A-Z0-9]+\z/
|
|
10
11
|
|
|
@@ -28,6 +29,9 @@ module Slk
|
|
|
28
29
|
private
|
|
29
30
|
|
|
30
31
|
def group_label(workspace, channel_id, name, self_user_id, self_username)
|
|
32
|
+
named = Support::GroupDmName.label(name)
|
|
33
|
+
return named if named
|
|
34
|
+
|
|
31
35
|
names = group_member_names(workspace, channel_id, self_user_id)
|
|
32
36
|
return "@#{names.join(', ')}" if names.any?
|
|
33
37
|
|
|
@@ -66,19 +66,8 @@ module Slk
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def resolve_channel(workspace, name)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
fetch_and_cache_channel(workspace, name)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def fetch_and_cache_channel(workspace, name)
|
|
76
|
-
channels = @runner.conversations_api(workspace.name).list['channels'] || []
|
|
77
|
-
channel = channels.find { |c| c['name'] == name }
|
|
78
|
-
raise ConfigError, "Channel not found: ##{name}" unless channel
|
|
79
|
-
|
|
80
|
-
@cache.set_channel(workspace.name, name, channel['id'])
|
|
81
|
-
channel['id']
|
|
69
|
+
ChannelLookup.new(runner: @runner, cache_store: @cache).find_id(workspace, name) ||
|
|
70
|
+
raise(ConfigError, "Channel not found: ##{name}")
|
|
82
71
|
end
|
|
83
72
|
|
|
84
73
|
def resolve_dm(workspace, username)
|
|
@@ -14,13 +14,15 @@ module Slk
|
|
|
14
14
|
|
|
15
15
|
# Mark all unreads in a workspace, return counts
|
|
16
16
|
# @param options [Hash] :muted - include muted channels
|
|
17
|
-
# @return [Hash] { dms: count, channels: count, threads: count }
|
|
17
|
+
# @return [Hash] { dms: count, group_dms: count, channels: count, threads: count }
|
|
18
18
|
def mark_all(options: {})
|
|
19
19
|
counts = @client.counts
|
|
20
|
+
muted_ids = options[:muted] ? [] : @users.muted_channels
|
|
20
21
|
|
|
21
22
|
{
|
|
22
23
|
dms: mark_dms(counts['ims'] || []),
|
|
23
|
-
|
|
24
|
+
group_dms: mark_channels(counts['mpims'] || [], muted_ids),
|
|
25
|
+
channels: mark_channels(counts['channels'] || [], muted_ids),
|
|
24
26
|
threads: mark_threads
|
|
25
27
|
}
|
|
26
28
|
end
|
|
@@ -44,13 +46,14 @@ module Slk
|
|
|
44
46
|
count
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
# Channels and group DMs (client.counts `mpims`): a muted group DM is
|
|
50
|
+
# skipped the same way a muted channel is.
|
|
51
|
+
def mark_channels(channels, muted_ids)
|
|
49
52
|
count = 0
|
|
50
53
|
|
|
51
54
|
channels.each do |channel|
|
|
52
55
|
next unless channel['has_unreads']
|
|
53
|
-
next if
|
|
56
|
+
next if muted_ids.include?(channel['id'])
|
|
54
57
|
|
|
55
58
|
count += 1 if mark_conversation(channel['id'])
|
|
56
59
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Support
|
|
5
|
+
# Group DMs can be given a name in Slack ("named chats"). They are still
|
|
6
|
+
# mpims, but `name` holds what the user typed ("ifams review") rather
|
|
7
|
+
# than the generated mpdm-alice--bob-1 slug. conversations.info flags them
|
|
8
|
+
# with properties.has_custom_mpdm_name; search.messages sends no
|
|
9
|
+
# properties, so a name that isn't an mpdm-* slug counts as custom too.
|
|
10
|
+
module GroupDmName
|
|
11
|
+
SLUG = /\Ampdm-/
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param channel [Hash] conversation payload from Slack
|
|
16
|
+
# @return [String, nil] the custom name, or nil for anything else
|
|
17
|
+
def custom(channel)
|
|
18
|
+
return nil unless channel.is_a?(Hash) && channel['is_mpim']
|
|
19
|
+
|
|
20
|
+
name = channel['name'].to_s
|
|
21
|
+
return nil if name.empty?
|
|
22
|
+
return name if channel.dig('properties', 'has_custom_mpdm_name')
|
|
23
|
+
|
|
24
|
+
custom_name?(name) ? name : nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param name [String, nil] a group DM's Slack name
|
|
28
|
+
# @return [Boolean] true when the name was set by a person
|
|
29
|
+
def custom_name?(name)
|
|
30
|
+
!name.to_s.empty? && !name.to_s.match?(SLUG)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] "#name" for a named group DM, else nil
|
|
34
|
+
def label(name)
|
|
35
|
+
"##{name}" if custom_name?(name)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -68,6 +68,8 @@ module Slk
|
|
|
68
68
|
public
|
|
69
69
|
|
|
70
70
|
# Resolves a channel ID to a formatted label (@username or #channel)
|
|
71
|
+
# Group DMs get "#name" when named, else "@other, members", never the
|
|
72
|
+
# mpdm-* slug that other lookups may have put in the channel cache.
|
|
71
73
|
# @param workspace [Models::Workspace] The workspace to look up in
|
|
72
74
|
# @param channel_id [String] The channel ID
|
|
73
75
|
# @return [String] Formatted label like "@username" or "#channel"
|
|
@@ -97,8 +99,11 @@ module Slk
|
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def resolve_channel_label(workspace, channel_id)
|
|
102
|
+
known = group_dm_label.known(workspace, channel_id)
|
|
103
|
+
return known if known
|
|
104
|
+
|
|
100
105
|
cached_name = cache_store.get_channel_name(workspace.name, channel_id)
|
|
101
|
-
return "##{cached_name}" if cached_name
|
|
106
|
+
return "##{cached_name}" if cached_name && !cached_name.match?(Support::GroupDmName::SLUG)
|
|
102
107
|
|
|
103
108
|
fetch_channel_label(workspace, channel_id)
|
|
104
109
|
end
|
|
@@ -108,16 +113,27 @@ module Slk
|
|
|
108
113
|
response = conversations.info(channel: channel_id)
|
|
109
114
|
return "##{channel_id}" unless response['ok'] && response['channel']
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
return
|
|
116
|
+
channel = response['channel']
|
|
117
|
+
return group_dm_label.label_from(workspace, channel_id, channel) if channel['is_mpim']
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
"##{name}"
|
|
119
|
+
named_channel_label(workspace, channel_id, channel['name'])
|
|
116
120
|
rescue ApiError => e
|
|
117
121
|
debug("Channel info lookup failed for #{channel_id}: #{e.message}")
|
|
118
122
|
"##{channel_id}"
|
|
119
123
|
end
|
|
120
124
|
|
|
125
|
+
def named_channel_label(workspace, channel_id, name)
|
|
126
|
+
return "##{channel_id}" unless name
|
|
127
|
+
|
|
128
|
+
cache_store.set_channel(workspace.name, name, channel_id)
|
|
129
|
+
"##{name}"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# One per command run, so a group DM is looked up once for all headers.
|
|
133
|
+
def group_dm_label
|
|
134
|
+
@group_dm_label ||= Services::GroupDmLabel.new(runner: runner, on_debug: ->(msg) { debug(msg) })
|
|
135
|
+
end
|
|
136
|
+
|
|
121
137
|
def name_from_user_profile(msg)
|
|
122
138
|
return nil unless msg['user_profile']
|
|
123
139
|
|
data/lib/slk/version.rb
CHANGED
data/lib/slk.rb
CHANGED
|
@@ -125,6 +125,8 @@ module Slk
|
|
|
125
125
|
autoload :HuddleState, 'slk/services/huddle_state'
|
|
126
126
|
autoload :WorkspaceFanout, 'slk/services/workspace_fanout'
|
|
127
127
|
autoload :HuddleChannelLabel, 'slk/services/huddle_channel_label'
|
|
128
|
+
autoload :GroupDmLabel, 'slk/services/group_dm_label'
|
|
129
|
+
autoload :ChannelLookup, 'slk/services/channel_lookup'
|
|
128
130
|
autoload :ThreadSubscription, 'slk/services/thread_subscription'
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -209,6 +211,7 @@ module Slk
|
|
|
209
211
|
autoload :HelpFormatter, 'slk/support/help_formatter'
|
|
210
212
|
autoload :ErrorLogger, 'slk/support/error_logger'
|
|
211
213
|
autoload :UserResolver, 'slk/support/user_resolver'
|
|
214
|
+
autoload :GroupDmName, 'slk/support/group_dm_name'
|
|
212
215
|
autoload :TextWrapper, 'slk/support/text_wrapper'
|
|
213
216
|
autoload :InteractivePrompt, 'slk/support/interactive_prompt'
|
|
214
217
|
autoload :DateParser, 'slk/support/date_parser'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Boehs
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/slk/services/activity_enricher.rb
|
|
110
110
|
- lib/slk/services/api_client.rb
|
|
111
111
|
- lib/slk/services/cache_store.rb
|
|
112
|
+
- lib/slk/services/channel_lookup.rb
|
|
112
113
|
- lib/slk/services/configuration.rb
|
|
113
114
|
- lib/slk/services/deactivation_scanner.rb
|
|
114
115
|
- lib/slk/services/emoji_downloader.rb
|
|
@@ -116,6 +117,7 @@ files:
|
|
|
116
117
|
- lib/slk/services/encryption.rb
|
|
117
118
|
- lib/slk/services/file_downloader.rb
|
|
118
119
|
- lib/slk/services/gemoji_sync.rb
|
|
120
|
+
- lib/slk/services/group_dm_label.rb
|
|
119
121
|
- lib/slk/services/huddle_channel_label.rb
|
|
120
122
|
- lib/slk/services/huddle_fetch.rb
|
|
121
123
|
- lib/slk/services/huddle_scanner.rb
|
|
@@ -149,6 +151,7 @@ files:
|
|
|
149
151
|
- lib/slk/support/date_parser.rb
|
|
150
152
|
- lib/slk/support/decimal_timestamp.rb
|
|
151
153
|
- lib/slk/support/error_logger.rb
|
|
154
|
+
- lib/slk/support/group_dm_name.rb
|
|
152
155
|
- lib/slk/support/help_formatter.rb
|
|
153
156
|
- lib/slk/support/image_preview.rb
|
|
154
157
|
- lib/slk/support/inline_images.rb
|