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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -1
- data/README.md +5 -5
- data/bin/slk +3 -3
- data/lib/{slack_cli → slk}/api/activity.rb +10 -11
- data/lib/{slack_cli → slk}/api/bots.rb +5 -4
- data/lib/slk/api/client.rb +51 -0
- data/lib/{slack_cli → slk}/api/conversations.rb +14 -13
- data/lib/slk/api/dnd.rb +41 -0
- data/lib/{slack_cli → slk}/api/emoji.rb +4 -3
- data/lib/{slack_cli → slk}/api/threads.rb +13 -12
- data/lib/{slack_cli → slk}/api/usergroups.rb +2 -1
- data/lib/slk/api/users.rb +105 -0
- data/lib/slk/cli.rb +157 -0
- data/lib/slk/commands/activity.rb +152 -0
- data/lib/{slack_cli → slk}/commands/base.rb +67 -41
- data/lib/slk/commands/cache.rb +141 -0
- data/lib/slk/commands/catchup.rb +411 -0
- data/lib/slk/commands/config.rb +114 -0
- data/lib/slk/commands/dnd.rb +172 -0
- data/lib/slk/commands/emoji.rb +352 -0
- data/lib/slk/commands/help.rb +97 -0
- data/lib/slk/commands/messages.rb +299 -0
- data/lib/slk/commands/presence.rb +109 -0
- data/lib/slk/commands/preset.rb +231 -0
- data/lib/slk/commands/status.rb +223 -0
- data/lib/slk/commands/thread.rb +72 -0
- data/lib/slk/commands/unread.rb +305 -0
- data/lib/slk/commands/workspaces.rb +168 -0
- data/lib/slk/formatters/activity_formatter.rb +148 -0
- data/lib/slk/formatters/attachment_formatter.rb +65 -0
- data/lib/slk/formatters/block_formatter.rb +57 -0
- data/lib/{slack_cli → slk}/formatters/duration_formatter.rb +6 -5
- data/lib/slk/formatters/emoji_replacer.rb +141 -0
- data/lib/slk/formatters/json_message_formatter.rb +95 -0
- data/lib/slk/formatters/mention_replacer.rb +158 -0
- data/lib/slk/formatters/message_formatter.rb +174 -0
- data/lib/{slack_cli → slk}/formatters/output.rb +7 -6
- data/lib/slk/formatters/reaction_formatter.rb +87 -0
- data/lib/{slack_cli → slk}/models/channel.rb +12 -10
- data/lib/slk/models/duration.rb +94 -0
- data/lib/slk/models/message.rb +242 -0
- data/lib/slk/models/preset.rb +78 -0
- data/lib/{slack_cli → slk}/models/reaction.rb +6 -6
- data/lib/{slack_cli → slk}/models/status.rb +6 -6
- data/lib/slk/models/user.rb +55 -0
- data/lib/slk/models/workspace.rb +54 -0
- data/lib/{slack_cli → slk}/runner.rb +22 -19
- data/lib/slk/services/activity_enricher.rb +124 -0
- data/lib/slk/services/api_client.rb +145 -0
- data/lib/{slack_cli → slk}/services/cache_store.rb +20 -17
- data/lib/{slack_cli → slk}/services/configuration.rb +9 -8
- data/lib/slk/services/emoji_downloader.rb +103 -0
- data/lib/slk/services/emoji_searcher.rb +72 -0
- data/lib/{slack_cli → slk}/services/encryption.rb +11 -14
- data/lib/slk/services/gemoji_sync.rb +97 -0
- data/lib/{slack_cli → slk}/services/preset_store.rb +34 -33
- data/lib/slk/services/reaction_enricher.rb +82 -0
- data/lib/slk/services/setup_wizard.rb +131 -0
- data/lib/slk/services/target_resolver.rb +108 -0
- data/lib/{slack_cli → slk}/services/token_store.rb +11 -10
- data/lib/slk/services/unread_marker.rb +101 -0
- data/lib/{slack_cli → slk}/support/error_logger.rb +2 -1
- data/lib/{slack_cli → slk}/support/help_formatter.rb +36 -44
- data/lib/{slack_cli → slk}/support/inline_images.rb +28 -19
- data/lib/slk/support/interactive_prompt.rb +29 -0
- data/lib/{slack_cli → slk}/support/slack_url_parser.rb +15 -17
- data/lib/slk/support/text_wrapper.rb +57 -0
- data/lib/slk/support/user_resolver.rb +141 -0
- data/lib/{slack_cli → slk}/support/xdg_paths.rb +6 -5
- data/lib/slk/version.rb +5 -0
- data/lib/slk.rb +112 -0
- metadata +80 -59
- data/lib/slack_cli/api/client.rb +0 -49
- data/lib/slack_cli/api/dnd.rb +0 -40
- data/lib/slack_cli/api/users.rb +0 -101
- data/lib/slack_cli/cli.rb +0 -118
- data/lib/slack_cli/commands/activity.rb +0 -292
- data/lib/slack_cli/commands/cache.rb +0 -116
- data/lib/slack_cli/commands/catchup.rb +0 -484
- data/lib/slack_cli/commands/config.rb +0 -159
- data/lib/slack_cli/commands/dnd.rb +0 -143
- data/lib/slack_cli/commands/emoji.rb +0 -412
- data/lib/slack_cli/commands/help.rb +0 -76
- data/lib/slack_cli/commands/messages.rb +0 -317
- data/lib/slack_cli/commands/presence.rb +0 -107
- data/lib/slack_cli/commands/preset.rb +0 -239
- data/lib/slack_cli/commands/status.rb +0 -194
- data/lib/slack_cli/commands/thread.rb +0 -62
- data/lib/slack_cli/commands/unread.rb +0 -312
- data/lib/slack_cli/commands/workspaces.rb +0 -151
- data/lib/slack_cli/formatters/emoji_replacer.rb +0 -143
- data/lib/slack_cli/formatters/mention_replacer.rb +0 -154
- data/lib/slack_cli/formatters/message_formatter.rb +0 -429
- data/lib/slack_cli/models/duration.rb +0 -85
- data/lib/slack_cli/models/message.rb +0 -217
- data/lib/slack_cli/models/preset.rb +0 -73
- data/lib/slack_cli/models/user.rb +0 -56
- data/lib/slack_cli/models/workspace.rb +0 -52
- data/lib/slack_cli/services/api_client.rb +0 -149
- data/lib/slack_cli/services/reaction_enricher.rb +0 -87
- data/lib/slack_cli/support/user_resolver.rb +0 -114
- data/lib/slack_cli/version.rb +0 -5
- data/lib/slack_cli.rb +0 -91
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f633ce48448a37653aed188faa126b5f5d009a3c393128602ee5cb3ac01ff469
|
|
4
|
+
data.tar.gz: a9a342b35df6960696c800e6007c3cdab422edd0d3132e82e3bd7f91d4a8aeb4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
173
|
-
cd
|
|
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(
|
|
5
|
-
require
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
|
|
5
|
+
require 'slk'
|
|
6
6
|
|
|
7
|
-
exit
|
|
7
|
+
exit Slk::CLI.new(ARGV).run
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
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
|
|
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,
|
|
17
|
-
response[
|
|
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(
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
23
|
+
@api.post(@workspace, 'conversations.history', params)
|
|
23
24
|
end
|
|
24
25
|
|
|
25
|
-
def replies(channel:,
|
|
26
|
-
params = { channel: channel, ts:
|
|
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,
|
|
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,
|
|
34
|
+
user_list = Array(users).join(',')
|
|
35
|
+
@api.post(@workspace, 'conversations.open', { users: user_list })
|
|
35
36
|
end
|
|
36
37
|
|
|
37
|
-
def mark(channel:,
|
|
38
|
-
@api.post(@workspace,
|
|
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,
|
|
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,
|
|
49
|
+
@api.post(@workspace, 'conversations.members', params)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
end
|
data/lib/slk/api/dnd.rb
ADDED
|
@@ -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
|
|
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,
|
|
13
|
+
@api.post(@workspace, 'emoji.list')
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def custom_emoji
|
|
16
17
|
response = list
|
|
17
|
-
response[
|
|
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
|
|
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,
|
|
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
|
|
22
|
-
def mark(channel:, thread_ts:,
|
|
23
|
-
@api.post_form(@workspace,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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[
|
|
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
|
|
40
|
-
unread_count
|
|
40
|
+
def unreads?
|
|
41
|
+
unread_count.positive?
|
|
41
42
|
end
|
|
42
43
|
end
|
|
43
44
|
end
|
|
@@ -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
|