slk 0.13.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +21 -0
  4. data/lib/slk/api/users.rb +19 -5
  5. data/lib/slk/cli.rb +1 -0
  6. data/lib/slk/commands/activity.rb +6 -0
  7. data/lib/slk/commands/catchup.rb +29 -11
  8. data/lib/slk/commands/debug.rb +15 -4
  9. data/lib/slk/commands/help.rb +1 -0
  10. data/lib/slk/commands/huddles.rb +94 -0
  11. data/lib/slk/commands/messages.rb +20 -3
  12. data/lib/slk/commands/thread.rb +1 -2
  13. data/lib/slk/commands/unread.rb +34 -19
  14. data/lib/slk/commands/who.rb +22 -2
  15. data/lib/slk/formatters/huddle_formatter.rb +54 -0
  16. data/lib/slk/formatters/json_message_formatter.rb +1 -1
  17. data/lib/slk/formatters/mention_replacer.rb +9 -3
  18. data/lib/slk/formatters/profile_formatter.rb +9 -0
  19. data/lib/slk/formatters/profile_rows.rb +14 -1
  20. data/lib/slk/formatters/search_formatter.rb +4 -0
  21. data/lib/slk/models/huddle.rb +17 -0
  22. data/lib/slk/models/profile.rb +16 -1
  23. data/lib/slk/models/search_result.rb +3 -0
  24. data/lib/slk/services/activity_enricher.rb +33 -13
  25. data/lib/slk/services/api_client.rb +58 -2
  26. data/lib/slk/services/cache_store.rb +13 -2
  27. data/lib/slk/services/channel_lookup.rb +78 -0
  28. data/lib/slk/services/group_dm_label.rb +83 -0
  29. data/lib/slk/services/huddle_channel_label.rb +62 -0
  30. data/lib/slk/services/huddle_fetch.rb +90 -0
  31. data/lib/slk/services/huddle_scanner.rb +101 -0
  32. data/lib/slk/services/huddle_state.rb +41 -0
  33. data/lib/slk/services/profile_builder.rb +1 -0
  34. data/lib/slk/services/profile_resolver.rb +1 -1
  35. data/lib/slk/services/sent_channel_label.rb +4 -0
  36. data/lib/slk/services/target_resolver.rb +2 -13
  37. data/lib/slk/services/unread_marker.rb +8 -5
  38. data/lib/slk/services/workspace_fanout.rb +28 -0
  39. data/lib/slk/support/group_dm_name.rb +39 -0
  40. data/lib/slk/support/user_resolver.rb +21 -5
  41. data/lib/slk/version.rb +1 -1
  42. data/lib/slk.rb +13 -0
  43. metadata +12 -1
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Loads active huddles for one or more workspaces.
6
+ #
7
+ # A roster scan is a large download. Workspaces do not share a rate-limit
8
+ # bucket, and one HTTP connection can only read one of them at a time, so
9
+ # more than one workspace is fetched concurrently, each on its own client.
10
+ class HuddleFetch
11
+ def initialize(runner:, output:)
12
+ @runner = runner
13
+ @output = output
14
+ @counts = {}
15
+ @mutex = Mutex.new
16
+ end
17
+
18
+ def call(workspaces)
19
+ @parallel = workspaces.size > 1 && @runner.api_client.respond_to?(:isolated)
20
+ fetched = WorkspaceFanout.call(workspaces, parallel: @parallel) do |workspace|
21
+ [workspace, fetch(workspace)]
22
+ end
23
+ fetched.map { |workspace, huddles| report(workspace, huddles) }
24
+ ensure
25
+ @output.clear_progress
26
+ end
27
+
28
+ private
29
+
30
+ def fetch(workspace)
31
+ client = scan_client
32
+ users = Api::Users.new(client, workspace, on_debug: debug_logger)
33
+ HuddleScanner.group(collect(workspace, users))
34
+ ensure
35
+ close_client(client)
36
+ end
37
+
38
+ def collect(workspace, users)
39
+ kept = []
40
+ users.list_each do |page, total|
41
+ kept.concat(HuddleScanner.keep(page))
42
+ note(workspace.name, total)
43
+ end
44
+ kept
45
+ end
46
+
47
+ def report(workspace, huddles)
48
+ labeled = huddles.map { |huddle| huddle.with(channel: labeler.label(workspace, huddle.channel_id)) }
49
+ Models::WorkspaceHuddles.new(workspace: workspace.name, huddles: labeled)
50
+ end
51
+
52
+ def scan_client
53
+ return @runner.api_client.isolated if @parallel
54
+
55
+ @runner.api_client
56
+ end
57
+
58
+ # The shared client belongs to the rest of the command. An isolated one
59
+ # exists only for this scan and should not keep its socket open.
60
+ def close_client(client)
61
+ return if client.nil? || client.equal?(@runner.api_client)
62
+
63
+ client.close if client.respond_to?(:close)
64
+ end
65
+
66
+ def note(name, total)
67
+ @mutex.synchronize do
68
+ @counts[name] = total
69
+ @output.progress(progress_line)
70
+ end
71
+ end
72
+
73
+ def progress_line
74
+ return "#{@counts.keys.first}: #{@counts.values.first} members" if @counts.one?
75
+
76
+ @counts.map { |name, total| "#{name}: #{total}" }.join(' · ')
77
+ end
78
+
79
+ def labeler
80
+ @labeler ||= HuddleChannelLabel.new(
81
+ runner: @runner, cache_store: @runner.cache_store, on_debug: debug_logger
82
+ )
83
+ end
84
+
85
+ def debug_logger
86
+ @debug_logger ||= ->(message) { @output.debug(message) }
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Groups a users.list roster into active huddles.
6
+ #
7
+ # Slack has no list-huddles method. A profile's huddle_state is
8
+ # `in_a_huddle` while they are in one, and huddle_state_call_id is the
9
+ # shared key. A missing call id is not evidence two people share a call,
10
+ # so those profiles stay separate. Disagreeing channel ids are dropped
11
+ # rather than labeling the call with one of them.
12
+ class HuddleScanner
13
+ IN_HUDDLE = 'in_a_huddle'
14
+
15
+ def self.group(members)
16
+ new(members).group
17
+ end
18
+
19
+ # People who are not in a huddle are useless to the scan and expensive
20
+ # to keep. Drop them as each page arrives.
21
+ def self.keep(members)
22
+ members.select { |member| in_huddle?(member) && member['id'] }
23
+ end
24
+
25
+ def self.in_huddle?(member)
26
+ member.dig('profile', 'huddle_state') == IN_HUDDLE
27
+ end
28
+
29
+ def initialize(members)
30
+ @members = members
31
+ end
32
+
33
+ def group
34
+ huddles.sort_by { |huddle| sort_key(huddle) }
35
+ end
36
+
37
+ private
38
+
39
+ def huddles
40
+ active.group_by { |member| group_key(member) }.map { |key, people| build(key, people) }
41
+ end
42
+
43
+ def active
44
+ self.class.keep(@members)
45
+ end
46
+
47
+ def group_key(member)
48
+ call_id(member) || [:solo, member['id']]
49
+ end
50
+
51
+ def build(key, people)
52
+ Models::Huddle.new(
53
+ call_id: call_id_for(key),
54
+ channel_id: shared_channel_id(people),
55
+ channel: nil,
56
+ participants: sorted_participants(people)
57
+ )
58
+ end
59
+
60
+ def call_id_for(key)
61
+ key.is_a?(Array) ? nil : key
62
+ end
63
+
64
+ def shared_channel_id(people)
65
+ ids = people.filter_map { |member| channel_id(member) }.uniq
66
+ ids.first if ids.one?
67
+ end
68
+
69
+ def sorted_participants(people)
70
+ people.map { |member| participant(member) }.sort_by { |person| [person.name.downcase, person.id] }
71
+ end
72
+
73
+ def participant(member)
74
+ Models::HuddleParticipant.new(id: member['id'], name: participant_name(member))
75
+ end
76
+
77
+ def participant_name(member)
78
+ profile = member['profile'] || {}
79
+ names = [profile['display_name'], profile['real_name'], member['real_name'], member['name'], member['id']]
80
+ names.find { |value| !value.to_s.empty? }.to_s
81
+ end
82
+
83
+ def call_id(member)
84
+ present(member.dig('profile', 'huddle_state_call_id'))
85
+ end
86
+
87
+ def channel_id(member)
88
+ present(member.dig('profile', 'huddle_state_channel_id'))
89
+ end
90
+
91
+ def present(value)
92
+ text = value.to_s
93
+ text.empty? ? nil : text
94
+ end
95
+
96
+ def sort_key(huddle)
97
+ [-huddle.participants.size, huddle.participants.first.name.downcase, huddle.call_id.to_s]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Huddle fields from a profile hash, or from a live users.info.
6
+ #
7
+ # users.info and users.profile.get are cached for an hour. Huddle state
8
+ # changes in minutes, so a card must not trust the cached copy.
9
+ module HuddleState
10
+ EMPTY = { huddle_state: nil, huddle_channel_id: nil, huddle_call_id: nil }.freeze
11
+
12
+ module_function
13
+
14
+ def from(profile_data)
15
+ data = profile_data || {}
16
+ {
17
+ huddle_state: blank_to_nil(data['huddle_state']),
18
+ huddle_channel_id: blank_to_nil(data['huddle_state_channel_id']),
19
+ huddle_call_id: blank_to_nil(data['huddle_state_call_id']),
20
+ huddle_channel: nil
21
+ }
22
+ end
23
+
24
+ def apply(profile, users_api, user_id, on_debug: nil)
25
+ Models::Profile.new(**profile.to_h, **read(users_api, user_id, on_debug: on_debug))
26
+ end
27
+
28
+ def read(users_api, user_id, on_debug: nil)
29
+ from(users_api.info(user_id).dig('user', 'profile'))
30
+ rescue ApiError => e
31
+ on_debug&.call("huddle state for #{user_id} failed: #{e.message}")
32
+ EMPTY.merge(huddle_channel: nil)
33
+ end
34
+
35
+ def blank_to_nil(value)
36
+ text = value.to_s
37
+ text.empty? ? nil : text
38
+ end
39
+ end
40
+ end
41
+ end
@@ -17,6 +17,7 @@ module Slk
17
17
  Models::Profile.new(
18
18
  **identity(profile_data, info_data),
19
19
  **status(profile_data),
20
+ **HuddleState.from(profile_data),
20
21
  **tz(info_data),
21
22
  **flags(info_data, team_id, workspace_team_id),
22
23
  team_id: team_id,
@@ -93,7 +93,7 @@ module Slk
93
93
  end
94
94
 
95
95
  def attach_extras(profile, user_id)
96
- profile = attach_home_team_name(profile)
96
+ profile = HuddleState.apply(attach_home_team_name(profile), @users_api, user_id, on_debug: @on_debug)
97
97
  presence = fetch_presence(user_id)
98
98
  presence ? Models::Profile.new(**profile.to_h, presence: presence) : profile
99
99
  end
@@ -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
- cached = @cache.get_channel_id(workspace.name, name)
70
- return cached if cached
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
- channels: mark_channels(counts['channels'] || [], muted: options[:muted]),
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
- def mark_channels(channels, muted: false)
48
- muted_ids = muted ? [] : @users.muted_channels
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 !muted && muted_ids.include?(channel['id'])
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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Runs one block per item. With parallel, the blocks overlap and the
6
+ # results come back in the original order. A failure waits for the other
7
+ # threads to finish, then raises, so an in-flight request is not abandoned
8
+ # mid-read.
9
+ module WorkspaceFanout
10
+ module_function
11
+
12
+ def call(items, parallel: false, &block)
13
+ return items.map(&block) unless parallel && items.size > 1
14
+
15
+ threads = items.map { |item| start(item, &block) }
16
+ threads.map(&:value)
17
+ ensure
18
+ Array(threads).each(&:join)
19
+ end
20
+
21
+ # The caller raises the thread's exception. Leaving the default on
22
+ # prints a second traceback for a failure the command already reports.
23
+ def start(item, &block)
24
+ Thread.new { block.call(item) }.tap { |thread| thread.report_on_exception = false }
25
+ end
26
+ end
27
+ end
28
+ 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
- name = response['channel']['name']
112
- return "##{channel_id}" unless name
116
+ channel = response['channel']
117
+ return group_dm_label.label_from(workspace, channel_id, channel) if channel['is_mpim']
113
118
 
114
- cache_store.set_channel(workspace.name, name, channel_id)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slk
4
- VERSION = '0.13.0'
4
+ VERSION = '0.15.0'
5
5
  end
data/lib/slk.rb CHANGED
@@ -79,6 +79,9 @@ module Slk
79
79
  autoload :Tenure, 'slk/models/tenure'
80
80
  autoload :SearchResult, 'slk/models/search_result'
81
81
  autoload :SavedItem, 'slk/models/saved_item'
82
+ autoload :Huddle, 'slk/models/huddle'
83
+ autoload :HuddleParticipant, 'slk/models/huddle'
84
+ autoload :WorkspaceHuddles, 'slk/models/huddle'
82
85
  autoload :Profile, 'slk/models/profile'
83
86
  autoload :ProfileField, 'slk/models/profile_field'
84
87
  end
@@ -117,6 +120,13 @@ module Slk
117
120
  autoload :SentConversations, 'slk/services/sent_conversations'
118
121
  autoload :SentChanges, 'slk/services/sent_changes'
119
122
  autoload :SentChannelLabel, 'slk/services/sent_channel_label'
123
+ autoload :HuddleScanner, 'slk/services/huddle_scanner'
124
+ autoload :HuddleFetch, 'slk/services/huddle_fetch'
125
+ autoload :HuddleState, 'slk/services/huddle_state'
126
+ autoload :WorkspaceFanout, 'slk/services/workspace_fanout'
127
+ autoload :HuddleChannelLabel, 'slk/services/huddle_channel_label'
128
+ autoload :GroupDmLabel, 'slk/services/group_dm_label'
129
+ autoload :ChannelLookup, 'slk/services/channel_lookup'
120
130
  autoload :ThreadSubscription, 'slk/services/thread_subscription'
121
131
  end
122
132
 
@@ -141,6 +151,7 @@ module Slk
141
151
  autoload :ProfileFormatter, 'slk/formatters/profile_formatter'
142
152
  autoload :ProfileFieldRenderer, 'slk/formatters/profile_field_renderer'
143
153
  autoload :ProfileRows, 'slk/formatters/profile_rows'
154
+ autoload :HuddleFormatter, 'slk/formatters/huddle_formatter'
144
155
  autoload :DeactivationFormatter, 'slk/formatters/deactivation_formatter'
145
156
  autoload :DeactivationCsv, 'slk/formatters/deactivation_csv'
146
157
  autoload :CsvWriter, 'slk/formatters/csv_writer'
@@ -168,6 +179,7 @@ module Slk
168
179
  autoload :Later, 'slk/commands/later'
169
180
  autoload :Debug, 'slk/commands/debug'
170
181
  autoload :Who, 'slk/commands/who'
182
+ autoload :Huddles, 'slk/commands/huddles'
171
183
  autoload :Org, 'slk/commands/org'
172
184
  autoload :Deactivations, 'slk/commands/deactivations'
173
185
  end
@@ -199,6 +211,7 @@ module Slk
199
211
  autoload :HelpFormatter, 'slk/support/help_formatter'
200
212
  autoload :ErrorLogger, 'slk/support/error_logger'
201
213
  autoload :UserResolver, 'slk/support/user_resolver'
214
+ autoload :GroupDmName, 'slk/support/group_dm_name'
202
215
  autoload :TextWrapper, 'slk/support/text_wrapper'
203
216
  autoload :InteractivePrompt, 'slk/support/interactive_prompt'
204
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.13.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Boehs
@@ -50,6 +50,7 @@ files:
50
50
  - lib/slk/commands/dnd.rb
51
51
  - lib/slk/commands/emoji.rb
52
52
  - lib/slk/commands/help.rb
53
+ - lib/slk/commands/huddles.rb
53
54
  - lib/slk/commands/later.rb
54
55
  - lib/slk/commands/messages.rb
55
56
  - lib/slk/commands/org.rb
@@ -71,6 +72,7 @@ files:
71
72
  - lib/slk/formatters/deactivation_formatter.rb
72
73
  - lib/slk/formatters/duration_formatter.rb
73
74
  - lib/slk/formatters/emoji_replacer.rb
75
+ - lib/slk/formatters/huddle_formatter.rb
74
76
  - lib/slk/formatters/json_message_formatter.rb
75
77
  - lib/slk/formatters/json_status_formatter.rb
76
78
  - lib/slk/formatters/markdown_output.rb
@@ -89,6 +91,7 @@ files:
89
91
  - lib/slk/models/deactivation.rb
90
92
  - lib/slk/models/dnd_state.rb
91
93
  - lib/slk/models/duration.rb
94
+ - lib/slk/models/huddle.rb
92
95
  - lib/slk/models/message.rb
93
96
  - lib/slk/models/preset.rb
94
97
  - lib/slk/models/profile.rb
@@ -106,6 +109,7 @@ files:
106
109
  - lib/slk/services/activity_enricher.rb
107
110
  - lib/slk/services/api_client.rb
108
111
  - lib/slk/services/cache_store.rb
112
+ - lib/slk/services/channel_lookup.rb
109
113
  - lib/slk/services/configuration.rb
110
114
  - lib/slk/services/deactivation_scanner.rb
111
115
  - lib/slk/services/emoji_downloader.rb
@@ -113,6 +117,11 @@ files:
113
117
  - lib/slk/services/encryption.rb
114
118
  - lib/slk/services/file_downloader.rb
115
119
  - lib/slk/services/gemoji_sync.rb
120
+ - lib/slk/services/group_dm_label.rb
121
+ - lib/slk/services/huddle_channel_label.rb
122
+ - lib/slk/services/huddle_fetch.rb
123
+ - lib/slk/services/huddle_scanner.rb
124
+ - lib/slk/services/huddle_state.rb
116
125
  - lib/slk/services/message_resolver.rb
117
126
  - lib/slk/services/meta_cache.rb
118
127
  - lib/slk/services/preset_store.rb
@@ -136,11 +145,13 @@ files:
136
145
  - lib/slk/services/user_matcher.rb
137
146
  - lib/slk/services/user_picker.rb
138
147
  - lib/slk/services/who_target_resolver.rb
148
+ - lib/slk/services/workspace_fanout.rb
139
149
  - lib/slk/support/attachment_fetching.rb
140
150
  - lib/slk/support/check_in_time.rb
141
151
  - lib/slk/support/date_parser.rb
142
152
  - lib/slk/support/decimal_timestamp.rb
143
153
  - lib/slk/support/error_logger.rb
154
+ - lib/slk/support/group_dm_name.rb
144
155
  - lib/slk/support/help_formatter.rb
145
156
  - lib/slk/support/image_preview.rb
146
157
  - lib/slk/support/inline_images.rb