slk 0.4.2 → 0.6.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.
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Disambiguates between multiple matching users. Prompts at a TTY; raises
6
+ # ApiError in non-interactive contexts so callers don't silently get the
7
+ # wrong user when name resolution is ambiguous.
8
+ class UserPicker
9
+ def initialize(stdin: $stdin, prompt_io: $stderr)
10
+ @stdin = stdin
11
+ @prompt_io = prompt_io
12
+ end
13
+
14
+ def pick(matches)
15
+ return matches.first['id'] if matches.size == 1
16
+
17
+ unless interactive?
18
+ raise ApiError,
19
+ "Ambiguous match (#{matches.size} users): #{ids(matches).join(', ')}. " \
20
+ 'Use --pick N or --all to disambiguate non-interactively.'
21
+ end
22
+
23
+ list(matches)
24
+ matches[read_index(matches.size)]['id']
25
+ end
26
+
27
+ private
28
+
29
+ def interactive?
30
+ @stdin.respond_to?(:tty?) && @stdin.tty?
31
+ end
32
+
33
+ def list(matches)
34
+ @prompt_io.puts('Multiple users match — pick one:')
35
+ matches.each_with_index { |u, i| @prompt_io.puts(" [#{i + 1}] #{describe(u)}") }
36
+ end
37
+
38
+ def describe(user)
39
+ profile = user['profile'] || {}
40
+ name = profile['real_name'] || profile['display_name'] || user['name']
41
+ suffix = profile['title'].to_s.empty? ? '' : " — #{profile['title']}"
42
+ "#{name} (#{user['id']})#{suffix}#{flag_suffix(user)}"
43
+ end
44
+
45
+ def flag_suffix(user)
46
+ flags = []
47
+ flags << 'deactivated' if user['deleted']
48
+ flags << 'bot' if user['is_bot']
49
+ flags.empty? ? '' : " [#{flags.join(', ')}]"
50
+ end
51
+
52
+ def ids(matches)
53
+ matches.map { |u| u['id'] }
54
+ end
55
+
56
+ def read_index(count)
57
+ loop do
58
+ @prompt_io.print("Choice [1-#{count}]: ")
59
+ choice = @stdin.gets&.strip
60
+ raise ApiError, 'No selection made' if choice.nil? || choice.empty?
61
+
62
+ n = Integer(choice, exception: false)
63
+ return n - 1 if n&.between?(1, count)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Services
5
+ # Resolves the positional target for `slk who` into one or more user_ids.
6
+ class WhoTargetResolver
7
+ def initialize(workspace:, cache_store:, api_client:, output:, options:)
8
+ @workspace = workspace
9
+ @cache_store = cache_store
10
+ @api_client = api_client
11
+ @output = output
12
+ @options = options
13
+ end
14
+
15
+ def resolve(target)
16
+ return [self_user_id] if target.nil? || target == 'me'
17
+ return [target] if target.match?(/\A[UW][A-Z0-9]+\z/)
18
+
19
+ ids = resolve_by_name(target.delete_prefix('@'))
20
+ ids || (raise ApiError, "Could not resolve user: #{target}")
21
+ end
22
+
23
+ private
24
+
25
+ def resolve_by_name(name)
26
+ matches = lookup.find_all_by_name(name)
27
+ return select(matches) if matches.any?
28
+
29
+ cached = @cache_store.get_user_id_by_name(@workspace.name, name)
30
+ cached ? [cached] : nil
31
+ end
32
+
33
+ def select(matches)
34
+ return matches.map { |u| u['id'] } if @options[:all]
35
+ return [pick_by_index(matches)] if @options[:pick]
36
+
37
+ [UserPicker.new.pick(matches)]
38
+ end
39
+
40
+ def pick_by_index(matches)
41
+ idx = @options[:pick]
42
+ raise ApiError, "--pick #{idx} out of range (got #{matches.size} matches)" unless idx&.between?(1, matches.size)
43
+
44
+ matches[idx - 1]['id']
45
+ end
46
+
47
+ def lookup
48
+ UserLookup.new(
49
+ cache_store: @cache_store, workspace: @workspace,
50
+ api_client: @api_client, on_debug: ->(msg) { @output.debug(msg) }
51
+ )
52
+ end
53
+
54
+ def self_user_id
55
+ cached = @cache_store.get_meta(@workspace.name, 'self_user_id')
56
+ return cached if cached
57
+
58
+ user_id = Api::Client.new(@api_client, @workspace).auth_test['user_id']
59
+ @cache_store.set_meta(@workspace.name, 'self_user_id', user_id) if user_id
60
+ user_id
61
+ end
62
+ end
63
+ end
64
+ end
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.4.2'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/slk.rb CHANGED
@@ -11,7 +11,35 @@ require 'io/console'
11
11
  # Slack CLI - A command-line interface for Slack
12
12
  module Slk
13
13
  class Error < StandardError; end
14
- class ApiError < Error; end
14
+
15
+ # Errors from any Slack-API-shaped failure: HTTP errors, network errors,
16
+ # logical Slack errors (user_not_found, missing_scope, etc.), JSON parse
17
+ # failures. The optional `code` symbol lets callers match specific cases
18
+ # (e.g. `e.code == :user_not_found`) without parsing message strings.
19
+ #
20
+ # Codes used:
21
+ # :network_error, :http_error, :unauthorized, :invalid_json,
22
+ # :ratelimited, plus any literal Slack `error` value (`:user_not_found`,
23
+ # `:missing_scope`, `:account_inactive`, etc.).
24
+ class ApiError < Error
25
+ attr_reader :code
26
+
27
+ def initialize(message, code: nil)
28
+ super(message)
29
+ @code = code
30
+ end
31
+ end
32
+
33
+ # Slack rate-limit error. Carries Retry-After in seconds when present.
34
+ class RateLimitError < ApiError
35
+ attr_reader :retry_after
36
+
37
+ def initialize(message, retry_after: nil)
38
+ super(message, code: :ratelimited)
39
+ @retry_after = retry_after
40
+ end
41
+ end
42
+
15
43
  class ConfigError < Error; end
16
44
  class EncryptionError < Error; end
17
45
  class TokenStoreError < Error; end
@@ -34,6 +62,8 @@ module Slk
34
62
  autoload :Preset, 'slk/models/preset'
35
63
  autoload :SearchResult, 'slk/models/search_result'
36
64
  autoload :SavedItem, 'slk/models/saved_item'
65
+ autoload :Profile, 'slk/models/profile'
66
+ autoload :ProfileField, 'slk/models/profile_field'
37
67
  end
38
68
 
39
69
  # Application services for configuration, caching, and API communication
@@ -49,13 +79,20 @@ module Slk
49
79
  autoload :ReactionEnricher, 'slk/services/reaction_enricher'
50
80
  autoload :GemojiSync, 'slk/services/gemoji_sync'
51
81
  autoload :EmojiDownloader, 'slk/services/emoji_downloader'
82
+ autoload :FileDownloader, 'slk/services/file_downloader'
52
83
  autoload :EmojiSearcher, 'slk/services/emoji_searcher'
53
84
  autoload :ActivityEnricher, 'slk/services/activity_enricher'
54
85
  autoload :UnreadMarker, 'slk/services/unread_marker'
55
86
  autoload :TargetResolver, 'slk/services/target_resolver'
56
87
  autoload :SetupWizard, 'slk/services/setup_wizard'
57
88
  autoload :UserLookup, 'slk/services/user_lookup'
89
+ autoload :UserMatcher, 'slk/services/user_matcher'
90
+ autoload :UserPicker, 'slk/services/user_picker'
91
+ autoload :WhoTargetResolver, 'slk/services/who_target_resolver'
58
92
  autoload :MessageResolver, 'slk/services/message_resolver'
93
+ autoload :ProfileBuilder, 'slk/services/profile_builder'
94
+ autoload :ProfileResolver, 'slk/services/profile_resolver'
95
+ autoload :MetaCache, 'slk/services/meta_cache'
59
96
  end
60
97
 
61
98
  # Output formatters for messages, durations, and emoji
@@ -74,6 +111,9 @@ module Slk
74
111
  autoload :SearchFormatter, 'slk/formatters/search_formatter'
75
112
  autoload :SavedItemFormatter, 'slk/formatters/saved_item_formatter'
76
113
  autoload :TextProcessor, 'slk/formatters/text_processor'
114
+ autoload :ProfileFormatter, 'slk/formatters/profile_formatter'
115
+ autoload :ProfileFieldRenderer, 'slk/formatters/profile_field_renderer'
116
+ autoload :ProfileRows, 'slk/formatters/profile_rows'
77
117
  end
78
118
 
79
119
  # CLI commands implementing user-facing functionality
@@ -95,6 +135,9 @@ module Slk
95
135
  autoload :Config, 'slk/commands/config'
96
136
  autoload :Help, 'slk/commands/help'
97
137
  autoload :Later, 'slk/commands/later'
138
+ autoload :Debug, 'slk/commands/debug'
139
+ autoload :Who, 'slk/commands/who'
140
+ autoload :Org, 'slk/commands/org'
98
141
  end
99
142
 
100
143
  # Thin wrappers around Slack API endpoints
@@ -110,6 +153,7 @@ module Slk
110
153
  autoload :Activity, 'slk/api/activity'
111
154
  autoload :Search, 'slk/api/search'
112
155
  autoload :Saved, 'slk/api/saved'
156
+ autoload :Team, 'slk/api/team'
113
157
  end
114
158
 
115
159
  # Utility classes for paths, parsing, and helpers
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.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric Boehs
@@ -34,6 +34,7 @@ files:
34
34
  - lib/slk/api/emoji.rb
35
35
  - lib/slk/api/saved.rb
36
36
  - lib/slk/api/search.rb
37
+ - lib/slk/api/team.rb
37
38
  - lib/slk/api/threads.rb
38
39
  - lib/slk/api/usergroups.rb
39
40
  - lib/slk/api/users.rb
@@ -43,11 +44,13 @@ files:
43
44
  - lib/slk/commands/cache.rb
44
45
  - lib/slk/commands/catchup.rb
45
46
  - lib/slk/commands/config.rb
47
+ - lib/slk/commands/debug.rb
46
48
  - lib/slk/commands/dnd.rb
47
49
  - lib/slk/commands/emoji.rb
48
50
  - lib/slk/commands/help.rb
49
51
  - lib/slk/commands/later.rb
50
52
  - lib/slk/commands/messages.rb
53
+ - lib/slk/commands/org.rb
51
54
  - lib/slk/commands/presence.rb
52
55
  - lib/slk/commands/preset.rb
53
56
  - lib/slk/commands/search.rb
@@ -55,6 +58,7 @@ files:
55
58
  - lib/slk/commands/status.rb
56
59
  - lib/slk/commands/thread.rb
57
60
  - lib/slk/commands/unread.rb
61
+ - lib/slk/commands/who.rb
58
62
  - lib/slk/commands/workspaces.rb
59
63
  - lib/slk/formatters/activity_formatter.rb
60
64
  - lib/slk/formatters/attachment_formatter.rb
@@ -66,6 +70,9 @@ files:
66
70
  - lib/slk/formatters/mention_replacer.rb
67
71
  - lib/slk/formatters/message_formatter.rb
68
72
  - lib/slk/formatters/output.rb
73
+ - lib/slk/formatters/profile_field_renderer.rb
74
+ - lib/slk/formatters/profile_formatter.rb
75
+ - lib/slk/formatters/profile_rows.rb
69
76
  - lib/slk/formatters/reaction_formatter.rb
70
77
  - lib/slk/formatters/saved_item_formatter.rb
71
78
  - lib/slk/formatters/search_formatter.rb
@@ -74,6 +81,8 @@ files:
74
81
  - lib/slk/models/duration.rb
75
82
  - lib/slk/models/message.rb
76
83
  - lib/slk/models/preset.rb
84
+ - lib/slk/models/profile.rb
85
+ - lib/slk/models/profile_field.rb
77
86
  - lib/slk/models/reaction.rb
78
87
  - lib/slk/models/saved_item.rb
79
88
  - lib/slk/models/search_result.rb
@@ -88,9 +97,13 @@ files:
88
97
  - lib/slk/services/emoji_downloader.rb
89
98
  - lib/slk/services/emoji_searcher.rb
90
99
  - lib/slk/services/encryption.rb
100
+ - lib/slk/services/file_downloader.rb
91
101
  - lib/slk/services/gemoji_sync.rb
92
102
  - lib/slk/services/message_resolver.rb
103
+ - lib/slk/services/meta_cache.rb
93
104
  - lib/slk/services/preset_store.rb
105
+ - lib/slk/services/profile_builder.rb
106
+ - lib/slk/services/profile_resolver.rb
94
107
  - lib/slk/services/reaction_enricher.rb
95
108
  - lib/slk/services/setup_wizard.rb
96
109
  - lib/slk/services/target_resolver.rb
@@ -99,6 +112,9 @@ files:
99
112
  - lib/slk/services/token_store.rb
100
113
  - lib/slk/services/unread_marker.rb
101
114
  - lib/slk/services/user_lookup.rb
115
+ - lib/slk/services/user_matcher.rb
116
+ - lib/slk/services/user_picker.rb
117
+ - lib/slk/services/who_target_resolver.rb
102
118
  - lib/slk/support/date_parser.rb
103
119
  - lib/slk/support/error_logger.rb
104
120
  - lib/slk/support/help_formatter.rb
@@ -132,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
148
  - !ruby/object:Gem::Version
133
149
  version: '0'
134
150
  requirements: []
135
- rubygems_version: 4.0.3
151
+ rubygems_version: 4.0.10
136
152
  specification_version: 4
137
153
  summary: A command-line interface for Slack
138
154
  test_files: []