slk 0.1.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE +21 -0
  4. data/README.md +190 -0
  5. data/bin/slk +7 -0
  6. data/lib/slack_cli/api/activity.rb +28 -0
  7. data/lib/slack_cli/api/bots.rb +32 -0
  8. data/lib/slack_cli/api/client.rb +49 -0
  9. data/lib/slack_cli/api/conversations.rb +52 -0
  10. data/lib/slack_cli/api/dnd.rb +40 -0
  11. data/lib/slack_cli/api/emoji.rb +21 -0
  12. data/lib/slack_cli/api/threads.rb +44 -0
  13. data/lib/slack_cli/api/usergroups.rb +25 -0
  14. data/lib/slack_cli/api/users.rb +101 -0
  15. data/lib/slack_cli/cli.rb +118 -0
  16. data/lib/slack_cli/commands/activity.rb +292 -0
  17. data/lib/slack_cli/commands/base.rb +175 -0
  18. data/lib/slack_cli/commands/cache.rb +116 -0
  19. data/lib/slack_cli/commands/catchup.rb +484 -0
  20. data/lib/slack_cli/commands/config.rb +159 -0
  21. data/lib/slack_cli/commands/dnd.rb +143 -0
  22. data/lib/slack_cli/commands/emoji.rb +412 -0
  23. data/lib/slack_cli/commands/help.rb +76 -0
  24. data/lib/slack_cli/commands/messages.rb +317 -0
  25. data/lib/slack_cli/commands/presence.rb +107 -0
  26. data/lib/slack_cli/commands/preset.rb +239 -0
  27. data/lib/slack_cli/commands/status.rb +194 -0
  28. data/lib/slack_cli/commands/thread.rb +62 -0
  29. data/lib/slack_cli/commands/unread.rb +312 -0
  30. data/lib/slack_cli/commands/workspaces.rb +151 -0
  31. data/lib/slack_cli/formatters/duration_formatter.rb +28 -0
  32. data/lib/slack_cli/formatters/emoji_replacer.rb +143 -0
  33. data/lib/slack_cli/formatters/mention_replacer.rb +154 -0
  34. data/lib/slack_cli/formatters/message_formatter.rb +429 -0
  35. data/lib/slack_cli/formatters/output.rb +89 -0
  36. data/lib/slack_cli/models/channel.rb +52 -0
  37. data/lib/slack_cli/models/duration.rb +85 -0
  38. data/lib/slack_cli/models/message.rb +217 -0
  39. data/lib/slack_cli/models/preset.rb +73 -0
  40. data/lib/slack_cli/models/reaction.rb +54 -0
  41. data/lib/slack_cli/models/status.rb +57 -0
  42. data/lib/slack_cli/models/user.rb +56 -0
  43. data/lib/slack_cli/models/workspace.rb +52 -0
  44. data/lib/slack_cli/runner.rb +123 -0
  45. data/lib/slack_cli/services/api_client.rb +149 -0
  46. data/lib/slack_cli/services/cache_store.rb +198 -0
  47. data/lib/slack_cli/services/configuration.rb +74 -0
  48. data/lib/slack_cli/services/encryption.rb +51 -0
  49. data/lib/slack_cli/services/preset_store.rb +112 -0
  50. data/lib/slack_cli/services/reaction_enricher.rb +87 -0
  51. data/lib/slack_cli/services/token_store.rb +117 -0
  52. data/lib/slack_cli/support/error_logger.rb +28 -0
  53. data/lib/slack_cli/support/help_formatter.rb +139 -0
  54. data/lib/slack_cli/support/inline_images.rb +62 -0
  55. data/lib/slack_cli/support/slack_url_parser.rb +78 -0
  56. data/lib/slack_cli/support/user_resolver.rb +114 -0
  57. data/lib/slack_cli/support/xdg_paths.rb +37 -0
  58. data/lib/slack_cli/version.rb +5 -0
  59. data/lib/slack_cli.rb +91 -0
  60. metadata +103 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '09f740d5f0c6a9290031f4e94edff1bb118c85c80df51ccc4ef98b1dea6800a2'
4
+ data.tar.gz: 298d8188fd6bf43a07c8672523d9df253eff44dfe9481a0801d01062b161c8fd
5
+ SHA512:
6
+ metadata.gz: dca5ad190d8155a4312bbafc39def037d1dfc9401170a58cc9adc2f22d6bbb4645efc7bcfd383e2be5d29b31c4f794da518f182df62182c47445fe3deec5d625
7
+ data.tar.gz: cba53cbbf73ed98c220bc984150792bd44e832992be70e62e62d425e2f0a6885f1414a1fe027d1d07ad449c1bef374f12aa2cc12814fda4cfe5bde9844df9f46
data/CHANGELOG.md ADDED
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-01-14
9
+
10
+ Initial release of the Ruby rewrite. Pure Ruby, no external dependencies.
11
+
12
+ ### Added
13
+
14
+ - **Commands**
15
+ - `status` - Get or set your Slack status with emoji and duration
16
+ - `presence` - Toggle between active/away presence
17
+ - `dnd` - Manage Do Not Disturb (enable, disable, with duration)
18
+ - `messages` - Read channel or DM messages with reactions and threads
19
+ - `thread` - View message threads directly from URL
20
+ - `unread` - View and clear unread messages across workspaces
21
+ - `catchup` - Quick summary of mentions and DMs
22
+ - `activity` - View recent workspace activity (mentions, reactions, threads)
23
+ - `preset` - Define and apply status presets (status + presence + DND)
24
+ - `workspaces` - Manage multiple Slack workspaces
25
+ - `cache` - Manage user/channel name cache
26
+ - `emoji` - Download and search workspace custom emoji
27
+ - `config` - Interactive setup and configuration
28
+
29
+ - **Features**
30
+ - Multi-workspace support with easy switching (`-w` flag or `--all`)
31
+ - Encrypted token storage using `age` with SSH keys
32
+ - XDG-compliant configuration directories
33
+ - HTTP connection reuse for better performance
34
+ - Inline emoji images in supported terminals (iTerm2, tmux)
35
+ - Reaction timestamps showing when users reacted
36
+ - Block Kit message rendering
37
+ - User and channel mention resolution
38
+ - Verbose mode (`-v`) for API call debugging
39
+ - JSON output mode (`--json`) for scripting
40
+
41
+ - **Developer Experience**
42
+ - 542 tests with 1082 assertions
43
+ - Pure Ruby stdlib - no gem dependencies
44
+ - Ruby 3.2+ with modern features (Data.define, pattern matching)
45
+
46
+ [0.1.0]: https://github.com/ericboehs/slack-cli/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eric Boehs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # slk - Slack CLI
2
+
3
+ A command-line interface for Slack. Manage your status, presence, DND, read messages, and more from the terminal.
4
+
5
+ **Pure Ruby. No dependencies.**
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gem install slk
11
+ ```
12
+
13
+ Requires Ruby 3.2+.
14
+
15
+ ## Setup
16
+
17
+ Run the setup wizard:
18
+
19
+ ```bash
20
+ slk config setup
21
+ ```
22
+
23
+ You'll need a Slack token. Get one from:
24
+ - **User token (xoxp-)**: https://api.slack.com/apps → OAuth & Permissions
25
+ - **Bot token (xoxb-)**: Create a Slack App with bot scopes
26
+ - **Session token (xoxc-)**: Extract from browser (requires cookie too)
27
+
28
+ ## Usage
29
+
30
+ ### Status
31
+
32
+ ```bash
33
+ slk status # Show current status
34
+ slk status "Working from home" :house: # Set status with emoji
35
+ slk status "In a meeting" :calendar: 1h # Set status for 1 hour
36
+ slk status clear # Clear status
37
+ ```
38
+
39
+ ### Presence
40
+
41
+ ```bash
42
+ slk presence # Show current presence
43
+ slk presence away # Set to away
44
+ slk presence active # Set to active
45
+ ```
46
+
47
+ ### Do Not Disturb
48
+
49
+ ```bash
50
+ slk dnd # Show DND status
51
+ slk dnd 1h # Enable DND for 1 hour
52
+ slk dnd on 30m # Enable DND for 30 minutes
53
+ slk dnd off # Disable DND
54
+ ```
55
+
56
+ ### Messages
57
+
58
+ ```bash
59
+ slk messages #general # Read channel messages
60
+ slk messages @username # Read DM with user
61
+ slk messages #general -n 50 # Show 50 messages
62
+ slk messages #general --json # Output as JSON
63
+ ```
64
+
65
+ ### Activity
66
+
67
+ ```bash
68
+ slk activity # Show recent activity feed
69
+ slk activity -n 50 # Show 50 items
70
+ slk activity -m # Show message previews
71
+ slk activity --reactions # Filter: reactions only
72
+ slk activity --mentions # Filter: mentions only
73
+ slk activity --threads # Filter: thread replies only
74
+ ```
75
+
76
+ Displays your recent activity feed including:
77
+ - Reactions to your messages
78
+ - Mentions (@user, @channel, @here, etc.)
79
+ - Thread replies
80
+ - Bot messages (reminders, notifications)
81
+
82
+ Use `--show-messages` (or `-m`) to preview the actual message content for each activity.
83
+
84
+ ### Unread
85
+
86
+ ```bash
87
+ slk unread # Show unread counts
88
+ slk unread clear # Mark all as read
89
+ slk unread clear #general # Mark channel as read
90
+ ```
91
+
92
+ ### Catchup (Interactive Triage)
93
+
94
+ ```bash
95
+ slk catchup # Interactively review unread channels
96
+ slk catchup --batch # Non-interactive, mark all as read
97
+ ```
98
+
99
+ ### Presets
100
+
101
+ ```bash
102
+ slk preset list # List all presets
103
+ slk preset meeting # Apply preset
104
+ slk preset add # Add new preset (interactive)
105
+ slk meeting # Shortcut: use preset name as command
106
+ ```
107
+
108
+ Built-in presets: `meeting`, `lunch`, `focus`, `brb`, `clear`
109
+
110
+ ### Workspaces
111
+
112
+ ```bash
113
+ slk workspaces list # List configured workspaces
114
+ slk workspaces add # Add a workspace
115
+ slk workspaces primary # Show/set primary workspace
116
+ ```
117
+
118
+ ### Cache Management
119
+
120
+ ```bash
121
+ slk cache status # Show cache status
122
+ slk cache populate # Pre-populate user cache
123
+ slk cache clear # Clear all caches
124
+ ```
125
+
126
+ ### Global Options
127
+
128
+ ```bash
129
+ -w, --workspace NAME # Use specific workspace
130
+ --all # Apply to all workspaces
131
+ -v, --verbose # Show debug output
132
+ -q, --quiet # Suppress output
133
+ --json # Output as JSON (where supported)
134
+ ```
135
+
136
+ ## Multi-Workspace
137
+
138
+ Configure multiple workspaces and switch between them:
139
+
140
+ ```bash
141
+ slk workspaces add # Add another workspace
142
+ slk status -w work # Check status on 'work' workspace
143
+ slk status "OOO" --all # Set status on all workspaces
144
+ ```
145
+
146
+ ## Token Encryption
147
+
148
+ Optionally encrypt your tokens with [age](https://github.com/FiloSottile/age) using an SSH key:
149
+
150
+ ```bash
151
+ slk config set ssh_key ~/.ssh/id_ed25519
152
+ ```
153
+
154
+ Tokens will be stored encrypted in `~/.config/slack-cli/tokens.age`.
155
+
156
+ ## Configuration
157
+
158
+ Files are stored in XDG-compliant locations:
159
+
160
+ - **Config**: `~/.config/slack-cli/`
161
+ - `config.json` - Settings
162
+ - `tokens.json` or `tokens.age` - Workspace tokens
163
+ - `presets.json` - Status presets
164
+ - **Cache**: `~/.cache/slack-cli/`
165
+ - `users-{workspace}.json` - User cache
166
+ - `channels-{workspace}.json` - Channel cache
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ # Clone the repo
172
+ git clone https://github.com/ericboehs/slack-cli.git
173
+ cd slack-cli
174
+
175
+ # Run from source
176
+ ruby -Ilib bin/slk --version
177
+
178
+ # Run tests
179
+ rake test
180
+
181
+ # Build gem
182
+ gem build slk.gemspec
183
+
184
+ # Install locally
185
+ gem install ./slk-0.1.0.gem
186
+ ```
187
+
188
+ ## License
189
+
190
+ MIT License. See [LICENSE](LICENSE) for details.
data/bin/slk ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "slack_cli"
6
+
7
+ exit SlackCli::CLI.new(ARGV).run
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Activity
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ 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
+ }
21
+ params[:types] = types if types
22
+ params[:cursor] = cursor if cursor
23
+
24
+ @api.post_form(@workspace, 'activity.feed', params)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Bots
6
+ def initialize(api_client, workspace, on_debug: nil)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ @on_debug = on_debug
10
+ end
11
+
12
+ # Look up bot information by ID
13
+ # @param bot_id [String] Bot ID starting with "B"
14
+ # @return [Hash, nil] Bot info hash or nil if not found
15
+ def info(bot_id)
16
+ response = @api.post_form(@workspace, "bots.info", { bot: bot_id })
17
+ response["bot"] if response["ok"]
18
+ rescue ApiError => e
19
+ @on_debug&.call("Bot lookup failed for #{bot_id}: #{e.message}")
20
+ nil
21
+ end
22
+
23
+ # Get bot name by ID
24
+ # @param bot_id [String] Bot ID starting with "B"
25
+ # @return [String, nil] Bot name or nil if not found
26
+ def get_name(bot_id)
27
+ bot = info(bot_id)
28
+ bot&.dig("name")
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Client
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ def counts
12
+ @api.post(@workspace, "client.counts")
13
+ end
14
+
15
+ def auth_test
16
+ @api.post(@workspace, "auth.test")
17
+ end
18
+
19
+ def team_id
20
+ @team_id ||= auth_test["team_id"]
21
+ end
22
+
23
+ def unread_channels
24
+ response = counts
25
+ channels = response.dig("channels") || []
26
+
27
+ channels.select { |ch| (ch["mention_count"] || 0) > 0 || ch["has_unreads"] }
28
+ end
29
+
30
+ def unread_dms
31
+ response = counts
32
+ dms = response.dig("ims") || []
33
+ mpims = response.dig("mpims") || []
34
+
35
+ (dms + mpims).select { |dm| (dm["mention_count"] || 0) > 0 || dm["has_unreads"] }
36
+ end
37
+
38
+ def total_unread_count
39
+ response = counts
40
+
41
+ channel_count = (response.dig("channels") || []).sum { |c| c["mention_count"] || 0 }
42
+ dm_count = (response.dig("ims") || []).sum { |d| d["mention_count"] || 0 }
43
+ mpim_count = (response.dig("mpims") || []).sum { |m| m["mention_count"] || 0 }
44
+
45
+ channel_count + dm_count + mpim_count
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Conversations
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ def list(cursor: nil, limit: 1000, types: "public_channel,private_channel")
12
+ params = { limit: limit, types: types }
13
+ params[:cursor] = cursor if cursor
14
+ @api.post(@workspace, "conversations.list", params)
15
+ end
16
+
17
+ def history(channel:, limit: 20, cursor: nil, oldest: nil, latest: nil)
18
+ params = { channel: channel, limit: limit }
19
+ params[:cursor] = cursor if cursor
20
+ params[:oldest] = oldest if oldest
21
+ params[:latest] = latest if latest
22
+ @api.post(@workspace, "conversations.history", params)
23
+ end
24
+
25
+ def replies(channel:, ts:, limit: 100, cursor: nil)
26
+ params = { channel: channel, ts: ts, limit: limit }
27
+ params[:cursor] = cursor if cursor
28
+ # Use form encoding - some workspaces (Enterprise Grid) require it
29
+ @api.post_form(@workspace, "conversations.replies", params)
30
+ end
31
+
32
+ def open(users:)
33
+ user_list = Array(users).join(",")
34
+ @api.post(@workspace, "conversations.open", { users: user_list })
35
+ end
36
+
37
+ def mark(channel:, ts:)
38
+ @api.post(@workspace, "conversations.mark", { channel: channel, ts: ts })
39
+ end
40
+
41
+ def info(channel:)
42
+ @api.post_form(@workspace, "conversations.info", { channel: channel })
43
+ end
44
+
45
+ def members(channel:, cursor: nil, limit: 100)
46
+ params = { channel: channel, limit: limit }
47
+ params[:cursor] = cursor if cursor
48
+ @api.post(@workspace, "conversations.members", params)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Dnd
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ def info
12
+ @api.post(@workspace, "dnd.info")
13
+ end
14
+
15
+ def set_snooze(duration)
16
+ minutes = duration.to_minutes
17
+ @api.post(@workspace, "dnd.setSnooze", { num_minutes: minutes })
18
+ end
19
+
20
+ def end_snooze
21
+ @api.post(@workspace, "dnd.endSnooze")
22
+ end
23
+
24
+ def snoozing?
25
+ info["snooze_enabled"] == true
26
+ end
27
+
28
+ def snooze_remaining
29
+ data = info
30
+ return nil unless data["snooze_enabled"]
31
+
32
+ endtime = data["snooze_endtime"]
33
+ return nil unless endtime
34
+
35
+ remaining = endtime - Time.now.to_i
36
+ remaining > 0 ? Models::Duration.new(seconds: remaining) : nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Emoji
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ def list
12
+ @api.post(@workspace, "emoji.list")
13
+ end
14
+
15
+ def custom_emoji
16
+ response = list
17
+ response["emoji"] || {}
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Threads
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ # Get unread threads
12
+ # @param limit [Integer] Max threads to return
13
+ # @return [Hash] Response with threads and total_unread_replies
14
+ def get_view(limit: 20)
15
+ @api.post(@workspace, "subscriptions.thread.getView", { limit: limit })
16
+ end
17
+
18
+ # Mark a thread as read
19
+ # @param channel [String] Channel ID
20
+ # @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
+ })
28
+ end
29
+
30
+ # Get unread thread count
31
+ # @return [Integer] Number of unread thread replies
32
+ def unread_count
33
+ response = get_view(limit: 1)
34
+ response["total_unread_replies"] || 0
35
+ end
36
+
37
+ # Check if there are unread threads
38
+ # @return [Boolean]
39
+ def has_unreads?
40
+ unread_count > 0
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Usergroups
6
+ def initialize(api_client, workspace)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ end
10
+
11
+ def list
12
+ @api.post(@workspace, 'usergroups.list')
13
+ end
14
+
15
+ def get_handle(subteam_id)
16
+ response = list
17
+ return nil unless response['ok']
18
+
19
+ usergroups = response['usergroups'] || []
20
+ group = usergroups.find { |g| g['id'] == subteam_id }
21
+ group&.dig('handle')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SlackCli
4
+ module Api
5
+ class Users
6
+ def initialize(api_client, workspace, on_debug: nil)
7
+ @api = api_client
8
+ @workspace = workspace
9
+ @on_debug = on_debug
10
+ end
11
+
12
+ def get_profile
13
+ response = @api.post(@workspace, "users.profile.get")
14
+ response["profile"]
15
+ end
16
+
17
+ def get_status
18
+ profile = get_profile
19
+ Models::Status.new(
20
+ text: profile["status_text"] || "",
21
+ emoji: profile["status_emoji"] || "",
22
+ expiration: profile["status_expiration"] || 0
23
+ )
24
+ end
25
+
26
+ def set_status(text:, emoji: nil, duration: nil)
27
+ expiration = duration&.to_expiration || 0
28
+
29
+ @api.post(@workspace, "users.profile.set", {
30
+ profile: {
31
+ status_text: text,
32
+ status_emoji: emoji || "",
33
+ status_expiration: expiration
34
+ }
35
+ })
36
+ end
37
+
38
+ def clear_status
39
+ set_status(text: "", emoji: "", duration: nil)
40
+ end
41
+
42
+ def get_presence
43
+ response = @api.post(@workspace, "users.getPresence")
44
+ {
45
+ presence: response["presence"],
46
+ manual_away: response["manual_away"],
47
+ online: response["online"]
48
+ }
49
+ end
50
+
51
+ def set_presence(presence)
52
+ @api.post(@workspace, "users.setPresence", { presence: presence })
53
+ end
54
+
55
+ def list(cursor: nil, limit: 1000)
56
+ params = { limit: limit }
57
+ params[:cursor] = cursor if cursor
58
+ @api.post(@workspace, "users.list", params)
59
+ end
60
+
61
+ def info(user_id)
62
+ @api.post_form(@workspace, "users.info", { user: user_id })
63
+ end
64
+
65
+ def get_prefs
66
+ @api.post(@workspace, "users.prefs.get")
67
+ end
68
+
69
+ def muted_channels
70
+ prefs = get_prefs
71
+
72
+ # First try the legacy muted_channels format (comma-separated string)
73
+ muted = prefs.dig("prefs", "muted_channels")
74
+ if muted.is_a?(String) && !muted.empty?
75
+ return muted.split(",").reject(&:empty?)
76
+ end
77
+
78
+ # New format: muted channels are in all_notifications_prefs JSON string
79
+ # Structure: {"channels": {"C123": {"muted": true}, ...}}
80
+ notifications_prefs = prefs.dig("prefs", "all_notifications_prefs")
81
+ if notifications_prefs.is_a?(String) && !notifications_prefs.empty?
82
+ begin
83
+ parsed = JSON.parse(notifications_prefs)
84
+ channels = parsed["channels"] || {}
85
+ return channels.select { |_id, opts| opts["muted"] == true }.keys
86
+ rescue JSON::ParserError => e
87
+ @on_debug&.call("Failed to parse notification prefs: #{e.message}")
88
+ end
89
+ end
90
+
91
+ []
92
+ end
93
+
94
+ def conversations(cursor: nil, limit: 1000)
95
+ params = { limit: limit, types: "public_channel,private_channel,mpim,im" }
96
+ params[:cursor] = cursor if cursor
97
+ @api.post_form(@workspace, "users.conversations", params)
98
+ end
99
+ end
100
+ end
101
+ end