slk 0.1.0 → 0.2.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 (104) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/README.md +5 -5
  4. data/bin/slk +3 -3
  5. data/lib/{slack_cli → slk}/api/activity.rb +10 -11
  6. data/lib/{slack_cli → slk}/api/bots.rb +5 -4
  7. data/lib/slk/api/client.rb +51 -0
  8. data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
  9. data/lib/slk/api/dnd.rb +41 -0
  10. data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
  11. data/lib/{slack_cli → slk}/api/threads.rb +13 -12
  12. data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
  13. data/lib/slk/api/users.rb +105 -0
  14. data/lib/slk/cli.rb +157 -0
  15. data/lib/slk/commands/activity.rb +152 -0
  16. data/lib/{slack_cli → slk}/commands/base.rb +67 -41
  17. data/lib/slk/commands/cache.rb +141 -0
  18. data/lib/slk/commands/catchup.rb +411 -0
  19. data/lib/slk/commands/config.rb +114 -0
  20. data/lib/slk/commands/dnd.rb +172 -0
  21. data/lib/slk/commands/emoji.rb +352 -0
  22. data/lib/slk/commands/help.rb +97 -0
  23. data/lib/slk/commands/messages.rb +299 -0
  24. data/lib/slk/commands/presence.rb +109 -0
  25. data/lib/slk/commands/preset.rb +231 -0
  26. data/lib/slk/commands/status.rb +223 -0
  27. data/lib/slk/commands/thread.rb +72 -0
  28. data/lib/slk/commands/unread.rb +305 -0
  29. data/lib/slk/commands/workspaces.rb +168 -0
  30. data/lib/slk/formatters/activity_formatter.rb +148 -0
  31. data/lib/slk/formatters/attachment_formatter.rb +65 -0
  32. data/lib/slk/formatters/block_formatter.rb +57 -0
  33. data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
  34. data/lib/slk/formatters/emoji_replacer.rb +141 -0
  35. data/lib/slk/formatters/json_message_formatter.rb +95 -0
  36. data/lib/slk/formatters/mention_replacer.rb +158 -0
  37. data/lib/slk/formatters/message_formatter.rb +174 -0
  38. data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
  39. data/lib/slk/formatters/reaction_formatter.rb +87 -0
  40. data/lib/{slack_cli → slk}/models/channel.rb +12 -10
  41. data/lib/slk/models/duration.rb +94 -0
  42. data/lib/slk/models/message.rb +242 -0
  43. data/lib/slk/models/preset.rb +78 -0
  44. data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
  45. data/lib/{slack_cli → slk}/models/status.rb +6 -6
  46. data/lib/slk/models/user.rb +55 -0
  47. data/lib/slk/models/workspace.rb +54 -0
  48. data/lib/{slack_cli → slk}/runner.rb +22 -19
  49. data/lib/slk/services/activity_enricher.rb +124 -0
  50. data/lib/slk/services/api_client.rb +145 -0
  51. data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
  52. data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
  53. data/lib/slk/services/emoji_downloader.rb +103 -0
  54. data/lib/slk/services/emoji_searcher.rb +72 -0
  55. data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
  56. data/lib/slk/services/gemoji_sync.rb +97 -0
  57. data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
  58. data/lib/slk/services/reaction_enricher.rb +82 -0
  59. data/lib/slk/services/setup_wizard.rb +131 -0
  60. data/lib/slk/services/target_resolver.rb +108 -0
  61. data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
  62. data/lib/slk/services/unread_marker.rb +101 -0
  63. data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
  64. data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
  65. data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
  66. data/lib/slk/support/interactive_prompt.rb +29 -0
  67. data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
  68. data/lib/slk/support/text_wrapper.rb +57 -0
  69. data/lib/slk/support/user_resolver.rb +141 -0
  70. data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
  71. data/lib/slk/version.rb +5 -0
  72. data/lib/slk.rb +112 -0
  73. metadata +80 -59
  74. data/lib/slack_cli/api/client.rb +0 -49
  75. data/lib/slack_cli/api/dnd.rb +0 -40
  76. data/lib/slack_cli/api/users.rb +0 -101
  77. data/lib/slack_cli/cli.rb +0 -118
  78. data/lib/slack_cli/commands/activity.rb +0 -292
  79. data/lib/slack_cli/commands/cache.rb +0 -116
  80. data/lib/slack_cli/commands/catchup.rb +0 -484
  81. data/lib/slack_cli/commands/config.rb +0 -159
  82. data/lib/slack_cli/commands/dnd.rb +0 -143
  83. data/lib/slack_cli/commands/emoji.rb +0 -412
  84. data/lib/slack_cli/commands/help.rb +0 -76
  85. data/lib/slack_cli/commands/messages.rb +0 -317
  86. data/lib/slack_cli/commands/presence.rb +0 -107
  87. data/lib/slack_cli/commands/preset.rb +0 -239
  88. data/lib/slack_cli/commands/status.rb +0 -194
  89. data/lib/slack_cli/commands/thread.rb +0 -62
  90. data/lib/slack_cli/commands/unread.rb +0 -312
  91. data/lib/slack_cli/commands/workspaces.rb +0 -151
  92. data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
  93. data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
  94. data/lib/slack_cli/formatters/message_formatter.rb +0 -429
  95. data/lib/slack_cli/models/duration.rb +0 -85
  96. data/lib/slack_cli/models/message.rb +0 -217
  97. data/lib/slack_cli/models/preset.rb +0 -73
  98. data/lib/slack_cli/models/user.rb +0 -56
  99. data/lib/slack_cli/models/workspace.rb +0 -52
  100. data/lib/slack_cli/services/api_client.rb +0 -149
  101. data/lib/slack_cli/services/reaction_enricher.rb +0 -87
  102. data/lib/slack_cli/support/user_resolver.rb +0 -114
  103. data/lib/slack_cli/version.rb +0 -5
  104. data/lib/slack_cli.rb +0 -91
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09f740d5f0c6a9290031f4e94edff1bb118c85c80df51ccc4ef98b1dea6800a2'
4
- data.tar.gz: 298d8188fd6bf43a07c8672523d9df253eff44dfe9481a0801d01062b161c8fd
3
+ metadata.gz: f633ce48448a37653aed188faa126b5f5d009a3c393128602ee5cb3ac01ff469
4
+ data.tar.gz: a9a342b35df6960696c800e6007c3cdab422edd0d3132e82e3bd7f91d4a8aeb4
5
5
  SHA512:
6
- metadata.gz: dca5ad190d8155a4312bbafc39def037d1dfc9401170a58cc9adc2f22d6bbb4645efc7bcfd383e2be5d29b31c4f794da518f182df62182c47445fe3deec5d625
7
- data.tar.gz: cba53cbbf73ed98c220bc984150792bd44e832992be70e62e62d425e2f0a6885f1414a1fe027d1d07ad449c1bef374f12aa2cc12814fda4cfe5bde9844df9f46
6
+ metadata.gz: 3422fcd166300be8a976c6996107c9e95623a1ee24d6850c365c8da8f14e57e81d089902405b027fc5552006ad38468a326b79a8fdfc49916abef875f36c5b6a
7
+ data.tar.gz: 23def7efe41b31edbe1791e8baf603676d4250ff04d92d0ef90932ae8682bcf3a6babdc9c514f40f64e980703afc9d1057bb70ab9a46380b8a038520ba9de160
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ <!-- TODO: Remove post_install_message from slk.gemspec before releasing 0.3.0 -->
11
+
12
+ ## [0.2.0] - 2025-01-15
13
+
14
+ ### Added
15
+
16
+ - `--workspace-emoji` flag for messages command to display custom workspace emoji as inline images (experimental, requires iTerm2/WezTerm/Mintty)
17
+ - JSON output now includes resolved user and channel names for `messages`, `activity`, and `unread` commands
18
+
19
+ ### Changed
20
+
21
+ - Config/cache directories renamed from `slack-cli` to `slk`
22
+ - Repository renamed from `slack-cli` to `slk`
23
+
24
+ ### Fixed
25
+
26
+ - `error()` helper now returns exit code 1 for proper shell exit status
27
+
8
28
  ## [0.1.0] - 2025-01-14
9
29
 
10
30
  Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
@@ -43,4 +63,5 @@ Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
43
63
  - Pure Ruby stdlib - no gem dependencies
44
64
  - Ruby 3.2+ with modern features (Data.define, pattern matching)
45
65
 
46
- [0.1.0]: https://github.com/ericboehs/slack-cli/releases/tag/v0.1.0
66
+ [0.2.0]: https://github.com/ericboehs/slk/releases/tag/v0.2.0
67
+ [0.1.0]: https://github.com/ericboehs/slk/releases/tag/v0.1.0
data/README.md CHANGED
@@ -151,17 +151,17 @@ Optionally encrypt your tokens with [age](https://github.com/FiloSottile/age) us
151
151
  slk config set ssh_key ~/.ssh/id_ed25519
152
152
  ```
153
153
 
154
- Tokens will be stored encrypted in `~/.config/slack-cli/tokens.age`.
154
+ Tokens will be stored encrypted in `~/.config/slk/tokens.age`.
155
155
 
156
156
  ## Configuration
157
157
 
158
158
  Files are stored in XDG-compliant locations:
159
159
 
160
- - **Config**: `~/.config/slack-cli/`
160
+ - **Config**: `~/.config/slk/`
161
161
  - `config.json` - Settings
162
162
  - `tokens.json` or `tokens.age` - Workspace tokens
163
163
  - `presets.json` - Status presets
164
- - **Cache**: `~/.cache/slack-cli/`
164
+ - **Cache**: `~/.cache/slk/`
165
165
  - `users-{workspace}.json` - User cache
166
166
  - `channels-{workspace}.json` - Channel cache
167
167
 
@@ -169,8 +169,8 @@ Files are stored in XDG-compliant locations:
169
169
 
170
170
  ```bash
171
171
  # Clone the repo
172
- git clone https://github.com/ericboehs/slack-cli.git
173
- cd slack-cli
172
+ git clone https://github.com/ericboehs/slk.git
173
+ cd slk
174
174
 
175
175
  # Run from source
176
176
  ruby -Ilib bin/slk --version
data/bin/slk CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
- require "slack_cli"
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+ require 'slk'
6
6
 
7
- exit SlackCli::CLI.new(ARGV).run
7
+ exit Slk::CLI.new(ARGV).run
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for the Slack activity.feed API endpoint
5
6
  class Activity
6
7
  def initialize(api_client, workspace)
7
8
  @api = api_client
@@ -9,20 +10,18 @@ module SlackCli
9
10
  end
10
11
 
11
12
  def feed(limit: 50, types: nil, cursor: nil, mode: 'priority_reads_and_unreads_v1')
12
- params = {
13
- mode: mode,
14
- limit: limit.to_s,
15
- archive_only: 'false',
16
- snooze_only: 'false',
17
- unread_only: 'false',
18
- priority_only: 'false',
19
- is_activity_inbox: 'false'
20
- }
13
+ params = build_feed_params(mode, limit)
21
14
  params[:types] = types if types
22
15
  params[:cursor] = cursor if cursor
23
-
24
16
  @api.post_form(@workspace, 'activity.feed', params)
25
17
  end
18
+
19
+ private
20
+
21
+ def build_feed_params(mode, limit)
22
+ { mode: mode, limit: limit.to_s, archive_only: 'false', snooze_only: 'false',
23
+ unread_only: 'false', priority_only: 'false', is_activity_inbox: 'false' }
24
+ end
26
25
  end
27
26
  end
28
27
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for Slack bots.info API endpoint
5
6
  class Bots
6
7
  def initialize(api_client, workspace, on_debug: nil)
7
8
  @api = api_client
@@ -13,8 +14,8 @@ module SlackCli
13
14
  # @param bot_id [String] Bot ID starting with "B"
14
15
  # @return [Hash, nil] Bot info hash or nil if not found
15
16
  def info(bot_id)
16
- response = @api.post_form(@workspace, "bots.info", { bot: bot_id })
17
- response["bot"] if response["ok"]
17
+ response = @api.post_form(@workspace, 'bots.info', { bot: bot_id })
18
+ response['bot'] if response['ok']
18
19
  rescue ApiError => e
19
20
  @on_debug&.call("Bot lookup failed for #{bot_id}: #{e.message}")
20
21
  nil
@@ -25,7 +26,7 @@ module SlackCli
25
26
  # @return [String, nil] Bot name or nil if not found
26
27
  def get_name(bot_id)
27
28
  bot = info(bot_id)
28
- bot&.dig("name")
29
+ bot&.dig('name')
29
30
  end
30
31
  end
31
32
  end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Api
5
+ # Wrapper for Slack client.counts and auth.test API endpoints
6
+ class Client
7
+ def initialize(api_client, workspace)
8
+ @api = api_client
9
+ @workspace = workspace
10
+ end
11
+
12
+ def counts
13
+ @api.post(@workspace, 'client.counts')
14
+ end
15
+
16
+ def auth_test
17
+ @api.post(@workspace, 'auth.test')
18
+ end
19
+
20
+ def team_id
21
+ @team_id ||= auth_test['team_id']
22
+ end
23
+
24
+ def unread_channels
25
+ response = counts
26
+ channels = response['channels'] || []
27
+
28
+ channels.select { |ch| (ch['mention_count'] || 0).positive? || ch['has_unreads'] }
29
+ end
30
+
31
+ def unread_dms
32
+ response = counts
33
+ dms = response['ims'] || []
34
+ mpims = response['mpims'] || []
35
+
36
+ (dms + mpims).select { |dm| (dm['mention_count'] || 0).positive? || dm['has_unreads'] }
37
+ end
38
+
39
+ def total_unread_count
40
+ response = counts
41
+ sum_mentions(response, 'channels') + sum_mentions(response, 'ims') + sum_mentions(response, 'mpims')
42
+ end
43
+
44
+ private
45
+
46
+ def sum_mentions(response, key)
47
+ (response[key] || []).sum { |item| item['mention_count'] || 0 }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,17 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for Slack conversations.* API endpoints
5
6
  class Conversations
6
7
  def initialize(api_client, workspace)
7
8
  @api = api_client
8
9
  @workspace = workspace
9
10
  end
10
11
 
11
- def list(cursor: nil, limit: 1000, types: "public_channel,private_channel")
12
+ def list(cursor: nil, limit: 1000, types: 'public_channel,private_channel')
12
13
  params = { limit: limit, types: types }
13
14
  params[:cursor] = cursor if cursor
14
- @api.post(@workspace, "conversations.list", params)
15
+ @api.post(@workspace, 'conversations.list', params)
15
16
  end
16
17
 
17
18
  def history(channel:, limit: 20, cursor: nil, oldest: nil, latest: nil)
@@ -19,33 +20,33 @@ module SlackCli
19
20
  params[:cursor] = cursor if cursor
20
21
  params[:oldest] = oldest if oldest
21
22
  params[:latest] = latest if latest
22
- @api.post(@workspace, "conversations.history", params)
23
+ @api.post(@workspace, 'conversations.history', params)
23
24
  end
24
25
 
25
- def replies(channel:, ts:, limit: 100, cursor: nil)
26
- params = { channel: channel, ts: ts, limit: limit }
26
+ def replies(channel:, timestamp:, limit: 100, cursor: nil)
27
+ params = { channel: channel, ts: timestamp, limit: limit }
27
28
  params[:cursor] = cursor if cursor
28
29
  # Use form encoding - some workspaces (Enterprise Grid) require it
29
- @api.post_form(@workspace, "conversations.replies", params)
30
+ @api.post_form(@workspace, 'conversations.replies', params)
30
31
  end
31
32
 
32
33
  def open(users:)
33
- user_list = Array(users).join(",")
34
- @api.post(@workspace, "conversations.open", { users: user_list })
34
+ user_list = Array(users).join(',')
35
+ @api.post(@workspace, 'conversations.open', { users: user_list })
35
36
  end
36
37
 
37
- def mark(channel:, ts:)
38
- @api.post(@workspace, "conversations.mark", { channel: channel, ts: ts })
38
+ def mark(channel:, timestamp:)
39
+ @api.post(@workspace, 'conversations.mark', { channel: channel, ts: timestamp })
39
40
  end
40
41
 
41
42
  def info(channel:)
42
- @api.post_form(@workspace, "conversations.info", { channel: channel })
43
+ @api.post_form(@workspace, 'conversations.info', { channel: channel })
43
44
  end
44
45
 
45
46
  def members(channel:, cursor: nil, limit: 100)
46
47
  params = { channel: channel, limit: limit }
47
48
  params[:cursor] = cursor if cursor
48
- @api.post(@workspace, "conversations.members", params)
49
+ @api.post(@workspace, 'conversations.members', params)
49
50
  end
50
51
  end
51
52
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Api
5
+ # Wrapper for Slack dnd.* (Do Not Disturb) API endpoints
6
+ class Dnd
7
+ def initialize(api_client, workspace)
8
+ @api = api_client
9
+ @workspace = workspace
10
+ end
11
+
12
+ def info
13
+ @api.post(@workspace, 'dnd.info')
14
+ end
15
+
16
+ def set_snooze(duration) # rubocop:disable Naming/AccessorMethodName
17
+ minutes = duration.to_minutes
18
+ @api.post(@workspace, 'dnd.setSnooze', { num_minutes: minutes })
19
+ end
20
+
21
+ def end_snooze
22
+ @api.post(@workspace, 'dnd.endSnooze')
23
+ end
24
+
25
+ def snoozing?
26
+ info['snooze_enabled'] == true
27
+ end
28
+
29
+ def snooze_remaining
30
+ data = info
31
+ return nil unless data['snooze_enabled']
32
+
33
+ endtime = data['snooze_endtime']
34
+ return nil unless endtime
35
+
36
+ remaining = endtime - Time.now.to_i
37
+ remaining.positive? ? Models::Duration.new(seconds: remaining) : nil
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for Slack emoji.list API endpoint
5
6
  class Emoji
6
7
  def initialize(api_client, workspace)
7
8
  @api = api_client
@@ -9,12 +10,12 @@ module SlackCli
9
10
  end
10
11
 
11
12
  def list
12
- @api.post(@workspace, "emoji.list")
13
+ @api.post(@workspace, 'emoji.list')
13
14
  end
14
15
 
15
16
  def custom_emoji
16
17
  response = list
17
- response["emoji"] || {}
18
+ response['emoji'] || {}
18
19
  end
19
20
  end
20
21
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for Slack thread subscription API endpoints
5
6
  class Threads
6
7
  def initialize(api_client, workspace)
7
8
  @api = api_client
@@ -12,32 +13,32 @@ module SlackCli
12
13
  # @param limit [Integer] Max threads to return
13
14
  # @return [Hash] Response with threads and total_unread_replies
14
15
  def get_view(limit: 20)
15
- @api.post(@workspace, "subscriptions.thread.getView", { limit: limit })
16
+ @api.post(@workspace, 'subscriptions.thread.getView', { limit: limit })
16
17
  end
17
18
 
18
19
  # Mark a thread as read
19
20
  # @param channel [String] Channel ID
20
21
  # @param thread_ts [String] Thread timestamp
21
- # @param ts [String] Latest reply timestamp to mark as read
22
- def mark(channel:, thread_ts:, ts:)
23
- @api.post_form(@workspace, "subscriptions.thread.mark", {
24
- channel: channel,
25
- thread_ts: thread_ts,
26
- ts: ts
27
- })
22
+ # @param timestamp [String] Latest reply timestamp to mark as read
23
+ def mark(channel:, thread_ts:, timestamp:)
24
+ @api.post_form(@workspace, 'subscriptions.thread.mark', {
25
+ channel: channel,
26
+ thread_ts: thread_ts,
27
+ ts: timestamp
28
+ })
28
29
  end
29
30
 
30
31
  # Get unread thread count
31
32
  # @return [Integer] Number of unread thread replies
32
33
  def unread_count
33
34
  response = get_view(limit: 1)
34
- response["total_unread_replies"] || 0
35
+ response['total_unread_replies'] || 0
35
36
  end
36
37
 
37
38
  # Check if there are unread threads
38
39
  # @return [Boolean]
39
- def has_unreads?
40
- unread_count > 0
40
+ def unreads?
41
+ unread_count.positive?
41
42
  end
42
43
  end
43
44
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Api
5
+ # Wrapper for Slack usergroups.list API endpoint
5
6
  class Usergroups
6
7
  def initialize(api_client, workspace)
7
8
  @api = api_client
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Api
5
+ # Wrapper for Slack users.* API endpoints
6
+ class Users
7
+ def initialize(api_client, workspace, on_debug: nil)
8
+ @api = api_client
9
+ @workspace = workspace
10
+ @on_debug = on_debug
11
+ end
12
+
13
+ def get_profile # rubocop:disable Naming/AccessorMethodName
14
+ response = @api.post(@workspace, 'users.profile.get')
15
+ response['profile']
16
+ end
17
+
18
+ def get_status # rubocop:disable Naming/AccessorMethodName
19
+ profile = get_profile
20
+ Models::Status.new(
21
+ text: profile['status_text'] || '',
22
+ emoji: profile['status_emoji'] || '',
23
+ expiration: profile['status_expiration'] || 0
24
+ )
25
+ end
26
+
27
+ def set_status(text:, emoji: nil, duration: nil)
28
+ expiration = duration&.to_expiration || 0
29
+
30
+ @api.post(@workspace, 'users.profile.set', {
31
+ profile: {
32
+ status_text: text,
33
+ status_emoji: emoji || '',
34
+ status_expiration: expiration
35
+ }
36
+ })
37
+ end
38
+
39
+ def clear_status
40
+ set_status(text: '', emoji: '', duration: nil)
41
+ end
42
+
43
+ def get_presence # rubocop:disable Naming/AccessorMethodName
44
+ response = @api.post(@workspace, 'users.getPresence')
45
+ {
46
+ presence: response['presence'],
47
+ manual_away: response['manual_away'],
48
+ online: response['online']
49
+ }
50
+ end
51
+
52
+ def set_presence(presence) # rubocop:disable Naming/AccessorMethodName
53
+ @api.post(@workspace, 'users.setPresence', { presence: presence })
54
+ end
55
+
56
+ def list(cursor: nil, limit: 1000)
57
+ params = { limit: limit }
58
+ params[:cursor] = cursor if cursor
59
+ @api.post(@workspace, 'users.list', params)
60
+ end
61
+
62
+ def info(user_id)
63
+ @api.post_form(@workspace, 'users.info', { user: user_id })
64
+ end
65
+
66
+ def get_prefs # rubocop:disable Naming/AccessorMethodName
67
+ @api.post(@workspace, 'users.prefs.get')
68
+ end
69
+
70
+ def muted_channels
71
+ prefs = get_prefs
72
+ parse_legacy_muted_channels(prefs) || parse_new_muted_channels(prefs) || []
73
+ end
74
+
75
+ private
76
+
77
+ def parse_legacy_muted_channels(prefs)
78
+ muted = prefs.dig('prefs', 'muted_channels')
79
+ return nil unless muted.is_a?(String) && !muted.empty?
80
+
81
+ muted.split(',').reject(&:empty?)
82
+ end
83
+
84
+ def parse_new_muted_channels(prefs)
85
+ notifications_prefs = prefs.dig('prefs', 'all_notifications_prefs')
86
+ return nil unless notifications_prefs.is_a?(String) && !notifications_prefs.empty?
87
+
88
+ parsed = JSON.parse(notifications_prefs)
89
+ channels = parsed['channels'] || {}
90
+ channels.select { |_id, opts| opts['muted'] == true }.keys
91
+ rescue JSON::ParserError => e
92
+ @on_debug&.call("Failed to parse notification prefs: #{e.message}")
93
+ nil
94
+ end
95
+
96
+ public
97
+
98
+ def conversations(cursor: nil, limit: 1000)
99
+ params = { limit: limit, types: 'public_channel,private_channel,mpim,im' }
100
+ params[:cursor] = cursor if cursor
101
+ @api.post_form(@workspace, 'users.conversations', params)
102
+ end
103
+ end
104
+ end
105
+ end
data/lib/slk/cli.rb ADDED
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ # Command-line interface entry point that dispatches to commands
5
+ # rubocop:disable Metrics/ClassLength
6
+ class CLI
7
+ COMMANDS = {
8
+ 'status' => Commands::Status,
9
+ 'presence' => Commands::Presence,
10
+ 'dnd' => Commands::Dnd,
11
+ 'messages' => Commands::Messages,
12
+ 'thread' => Commands::Thread,
13
+ 'unread' => Commands::Unread,
14
+ 'catchup' => Commands::Catchup,
15
+ 'activity' => Commands::Activity,
16
+ 'preset' => Commands::Preset,
17
+ 'workspaces' => Commands::Workspaces,
18
+ 'cache' => Commands::Cache,
19
+ 'emoji' => Commands::Emoji,
20
+ 'config' => Commands::Config,
21
+ 'help' => Commands::Help
22
+ }.freeze
23
+
24
+ def initialize(argv, output: nil)
25
+ @argv = argv.dup
26
+ @output = output || Formatters::Output.new
27
+ end
28
+
29
+ def run
30
+ command_name, *args = @argv
31
+
32
+ return show_help if help_requested?(command_name)
33
+ return show_version if version_requested?(command_name)
34
+
35
+ dispatch_command(command_name, args)
36
+ rescue Interrupt
37
+ handle_interrupt
38
+ rescue StandardError => e
39
+ handle_error(e)
40
+ end
41
+
42
+ private
43
+
44
+ def help_requested?(command_name)
45
+ command_name.nil? || command_name == '--help' || command_name == '-h'
46
+ end
47
+
48
+ def version_requested?(command_name)
49
+ ['--version', '-V', 'version'].include?(command_name)
50
+ end
51
+
52
+ def show_help
53
+ run_command('help', [])
54
+ end
55
+
56
+ def show_version
57
+ @output.puts "slk v#{VERSION}"
58
+ 0
59
+ end
60
+
61
+ def dispatch_command(command_name, args)
62
+ if COMMANDS[command_name]
63
+ run_command(command_name, args)
64
+ elsif preset_exists?(command_name)
65
+ run_command('preset', [command_name] + args)
66
+ else
67
+ show_unknown_command(command_name)
68
+ end
69
+ rescue ConfigError, EncryptionError, ApiError => e
70
+ handle_known_error(e)
71
+ end
72
+
73
+ def show_unknown_command(command_name)
74
+ @output.error("Unknown command: #{command_name}")
75
+ @output.puts
76
+ @output.puts "Run 'slk help' for available commands."
77
+ 1
78
+ end
79
+
80
+ def handle_known_error(error)
81
+ label = error_label(error)
82
+ @output.error(label ? "#{label}: #{error.message}" : error.message)
83
+ log_error(error)
84
+ 1
85
+ end
86
+
87
+ def error_label(error)
88
+ case error
89
+ when EncryptionError then 'Encryption error'
90
+ when ApiError then 'API error'
91
+ end
92
+ end
93
+
94
+ def handle_interrupt
95
+ @output.puts
96
+ @output.puts 'Interrupted.'
97
+ 130
98
+ end
99
+
100
+ def handle_error(error)
101
+ @output.error("Unexpected error: #{error.message}")
102
+ log_path = log_error(error)
103
+ @output.puts "Details logged to: #{log_path}" if log_path
104
+ 1
105
+ end
106
+
107
+ def run_command(name, args)
108
+ command_class = COMMANDS[name]
109
+ return 1 unless command_class
110
+
111
+ runner = build_runner(args)
112
+ execute_command(command_class, args, runner)
113
+ ensure
114
+ runner&.api_client&.close
115
+ end
116
+
117
+ def build_runner(args)
118
+ verbose = args.include?('-v') || args.include?('--verbose')
119
+ output = Formatters::Output.new(verbose: verbose)
120
+ runner = Runner.new(output: output)
121
+ setup_verbose_logging(runner, output) if verbose
122
+ runner
123
+ end
124
+
125
+ def setup_verbose_logging(runner, output)
126
+ runner.api_client.on_request = lambda { |method, count|
127
+ output.debug("[API ##{count}] #{method}")
128
+ }
129
+ end
130
+
131
+ def execute_command(command_class, args, runner)
132
+ command = command_class.new(args, runner: runner)
133
+ result = command.execute
134
+ log_api_call_count(runner) if verbose_mode?(args)
135
+ result
136
+ end
137
+
138
+ def verbose_mode?(args)
139
+ args.include?('-v') || args.include?('--verbose')
140
+ end
141
+
142
+ def log_api_call_count(runner)
143
+ return unless runner.api_client.call_count.positive?
144
+
145
+ runner.output.debug("Total API calls: #{runner.api_client.call_count}")
146
+ end
147
+
148
+ def preset_exists?(name)
149
+ Services::PresetStore.new.exists?(name)
150
+ end
151
+
152
+ def log_error(error)
153
+ Support::ErrorLogger.log(error)
154
+ end
155
+ end
156
+ # rubocop:enable Metrics/ClassLength
157
+ end