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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Support
|
|
5
5
|
# Formats help text with auto-aligned columns
|
|
6
6
|
#
|
|
@@ -47,32 +47,27 @@ module SlackCli
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def render
|
|
50
|
-
lines =
|
|
51
|
-
lines
|
|
52
|
-
lines
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
lines << @description
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
@notes.each do |note|
|
|
59
|
-
lines << note
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
lines << "" if @description || @notes.any?
|
|
50
|
+
lines = build_header
|
|
51
|
+
lines.concat(build_sections)
|
|
52
|
+
lines.pop if lines.last == ''
|
|
53
|
+
lines.join("\n")
|
|
54
|
+
end
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
lines << "#{section.title}:"
|
|
66
|
-
lines.concat(section.render)
|
|
67
|
-
lines << ""
|
|
68
|
-
end
|
|
56
|
+
private
|
|
69
57
|
|
|
70
|
-
|
|
71
|
-
lines
|
|
58
|
+
def build_header
|
|
59
|
+
lines = ["USAGE: #{@usage}", '']
|
|
60
|
+
lines << @description if @description
|
|
61
|
+
@notes.each { |note| lines << note }
|
|
62
|
+
lines << '' if @description || @notes.any?
|
|
63
|
+
lines
|
|
64
|
+
end
|
|
72
65
|
|
|
73
|
-
|
|
66
|
+
def build_sections
|
|
67
|
+
@sections.flat_map { |section| ["#{section.title}:", *section.render, ''] }
|
|
74
68
|
end
|
|
75
69
|
|
|
70
|
+
# Represents a section within help output (OPTIONS, EXAMPLES, etc.)
|
|
76
71
|
class Section
|
|
77
72
|
attr_reader :title
|
|
78
73
|
|
|
@@ -109,30 +104,27 @@ module SlackCli
|
|
|
109
104
|
def render
|
|
110
105
|
return [] if @items.empty?
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
max_left
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
" #{left.ljust(max_left + padding)}#{right}"
|
|
128
|
-
else
|
|
129
|
-
" #{left}"
|
|
130
|
-
end
|
|
131
|
-
else
|
|
132
|
-
" #{left.ljust(max_left + padding)}#{right}"
|
|
133
|
-
end
|
|
107
|
+
max_left = calculate_max_left_width
|
|
108
|
+
@items.map { |type, left, right| format_item(type, left, right, max_left) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def calculate_max_left_width
|
|
114
|
+
@items.reject { |type, _, _| type == :text }.map { |_, left, _| left.length }.max || 0
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def format_item(type, left, right, max_left)
|
|
118
|
+
case type
|
|
119
|
+
when :text then " #{left}"
|
|
120
|
+
when :example then format_example(left, right, max_left)
|
|
121
|
+
else " #{left.ljust(max_left + 2)}#{right}"
|
|
134
122
|
end
|
|
135
123
|
end
|
|
124
|
+
|
|
125
|
+
def format_example(left, right, max_left)
|
|
126
|
+
right ? " #{left.ljust(max_left + 2)}#{right}" : " #{left}"
|
|
127
|
+
end
|
|
136
128
|
end
|
|
137
129
|
end
|
|
138
130
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Support
|
|
5
5
|
# Shared module for inline image display in iTerm2/WezTerm/Mintty terminals
|
|
6
6
|
# Includes special handling for tmux passthrough sequences
|
|
@@ -9,40 +9,49 @@ module SlackCli
|
|
|
9
9
|
def inline_images_supported?
|
|
10
10
|
# iTerm2, WezTerm, Mintty support inline images
|
|
11
11
|
# LC_TERMINAL persists through tmux/ssh
|
|
12
|
-
|
|
13
|
-
ENV[
|
|
14
|
-
ENV[
|
|
15
|
-
ENV[
|
|
16
|
-
ENV["TERM"] == "mintty"
|
|
12
|
+
['iTerm.app', 'WezTerm'].include?(ENV.fetch('TERM_PROGRAM', nil)) ||
|
|
13
|
+
ENV['LC_TERMINAL'] == 'iTerm2' ||
|
|
14
|
+
ENV['LC_TERMINAL'] == 'WezTerm' ||
|
|
15
|
+
ENV['TERM'] == 'mintty'
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
# Check if running inside tmux
|
|
20
19
|
def in_tmux?
|
|
21
20
|
# tmux sets TERM to screen-* or tmux-*
|
|
22
|
-
ENV[
|
|
21
|
+
ENV['TERM']&.include?('screen') || ENV['TERM']&.start_with?('tmux')
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
# Print an inline image using iTerm2 protocol
|
|
26
25
|
# In tmux, uses passthrough sequence and cursor positioning
|
|
27
26
|
def print_inline_image(path, height: 1)
|
|
28
|
-
|
|
27
|
+
data = read_image_data(path)
|
|
28
|
+
return unless data
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
encoded = [data].pack('m0')
|
|
31
|
+
in_tmux? ? print_tmux_image(encoded, height) : print_iterm_image(encoded, height)
|
|
32
|
+
end
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
def read_image_data(path)
|
|
35
|
+
return nil unless File.exist?(path)
|
|
36
|
+
|
|
37
|
+
File.binread(path)
|
|
38
|
+
rescue IOError, SystemCallError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def print_tmux_image(encoded, height)
|
|
43
|
+
fmt = "\ePtmux;\e\e]1337;File=inline=1;preserveAspectRatio=0;" \
|
|
44
|
+
"size=%<size>d;height=%<height>d:%<data>s\a\e\\\n "
|
|
45
|
+
printf fmt, size: encoded.length, height: height, data: encoded
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print_iterm_image(encoded, height)
|
|
49
|
+
printf "\e]1337;File=inline=1;height=%<height>d:%<data>s\a", height: height, data: encoded
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
# Print inline image with name on same line
|
|
44
53
|
# Handles tmux cursor positioning to keep image and text on same line
|
|
45
|
-
def print_inline_image_with_text(path, text, height: 1)
|
|
54
|
+
def print_inline_image_with_text(path, text, height: 1) # rubocop:disable Naming/PredicateMethod
|
|
46
55
|
return false unless inline_images_supported? && File.exist?(path)
|
|
47
56
|
|
|
48
57
|
print_inline_image(path, height: height)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Support
|
|
5
|
+
# Interactive terminal prompt utilities
|
|
6
|
+
module InteractivePrompt
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Read a single character from the terminal
|
|
10
|
+
def read_single_char
|
|
11
|
+
if $stdin.tty?
|
|
12
|
+
$stdin.raw(&:readchar)
|
|
13
|
+
else
|
|
14
|
+
$stdin.gets&.chomp
|
|
15
|
+
end
|
|
16
|
+
rescue Interrupt
|
|
17
|
+
'q'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Display a prompt and read a single character
|
|
21
|
+
def prompt_for_action(prompt)
|
|
22
|
+
print "\n#{prompt} > "
|
|
23
|
+
input = read_single_char
|
|
24
|
+
puts
|
|
25
|
+
input
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Support
|
|
5
|
+
# Parses Slack message URLs into workspace, channel, and timestamp
|
|
5
6
|
class SlackUrlParser
|
|
6
7
|
# Patterns for Slack URLs
|
|
7
8
|
# Channel IDs: C=channel, G=group DM, D=direct message
|
|
@@ -30,41 +31,38 @@ module SlackCli
|
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def parse(input)
|
|
33
|
-
return nil unless input.to_s.include?(
|
|
34
|
+
return nil unless input.to_s.include?('slack.com')
|
|
34
35
|
|
|
35
36
|
URL_PATTERNS.each do |pattern|
|
|
36
37
|
match = input.match(pattern)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
workspace = match[1]
|
|
40
|
-
channel_id = match[2]
|
|
41
|
-
msg_ts = match[3] ? format_ts(match[3]) : nil
|
|
42
|
-
thread_ts = match[4]
|
|
43
|
-
|
|
44
|
-
return Result.new(
|
|
45
|
-
workspace: workspace,
|
|
46
|
-
channel_id: channel_id,
|
|
47
|
-
msg_ts: msg_ts,
|
|
48
|
-
thread_ts: thread_ts
|
|
49
|
-
)
|
|
38
|
+
return build_result(match) if match
|
|
50
39
|
end
|
|
51
40
|
|
|
52
41
|
nil
|
|
53
42
|
end
|
|
54
43
|
|
|
55
44
|
def slack_url?(input)
|
|
56
|
-
input.to_s.include?(
|
|
45
|
+
input.to_s.include?('slack.com/archives')
|
|
57
46
|
end
|
|
58
47
|
|
|
59
48
|
private
|
|
60
49
|
|
|
50
|
+
def build_result(match)
|
|
51
|
+
Result.new(
|
|
52
|
+
workspace: match[1],
|
|
53
|
+
channel_id: match[2],
|
|
54
|
+
msg_ts: match[3] ? format_ts(match[3]) : nil,
|
|
55
|
+
thread_ts: match[4]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
61
59
|
# Convert Slack URL timestamp format to API format
|
|
62
60
|
# URL: p1234567890123456 -> API: 1234567890.123456
|
|
63
61
|
def format_ts(url_ts)
|
|
64
62
|
return nil unless url_ts
|
|
65
63
|
|
|
66
64
|
# Remove 'p' prefix if present
|
|
67
|
-
ts = url_ts.sub(/^p/,
|
|
65
|
+
ts = url_ts.sub(/^p/, '')
|
|
68
66
|
|
|
69
67
|
# Insert decimal point
|
|
70
68
|
if ts.length > 6
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Support
|
|
5
|
+
# Text wrapping utilities with ANSI escape code awareness
|
|
6
|
+
module TextWrapper
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Calculate visible length of text (excluding ANSI escape codes)
|
|
10
|
+
def visible_length(text)
|
|
11
|
+
text.gsub(/\e\[[0-9;]*m/, '').length
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Wrap text to width, handling first line differently and preserving existing newlines
|
|
15
|
+
def wrap(text, first_line_width, continuation_width)
|
|
16
|
+
result = []
|
|
17
|
+
text.each_line do |paragraph|
|
|
18
|
+
process_paragraph(paragraph.chomp, result, first_line_width, continuation_width)
|
|
19
|
+
end
|
|
20
|
+
result.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def process_paragraph(paragraph, result, first_line_width, continuation_width)
|
|
24
|
+
if paragraph.empty?
|
|
25
|
+
result << ''
|
|
26
|
+
else
|
|
27
|
+
current_first_width = result.empty? ? first_line_width : continuation_width
|
|
28
|
+
result << wrap_paragraph(paragraph, current_first_width, continuation_width)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Wrap a single paragraph (no internal newlines)
|
|
33
|
+
def wrap_paragraph(text, first_width, rest_width)
|
|
34
|
+
state = { lines: [], current_line: '', current_width: first_width, rest_width: rest_width }
|
|
35
|
+
text.split(/(\s+)/).each { |word| process_word(word, state) }
|
|
36
|
+
state[:lines] << state[:current_line] unless state[:current_line].empty?
|
|
37
|
+
state[:lines].join("\n")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def process_word(word, state)
|
|
41
|
+
if state[:current_line].empty?
|
|
42
|
+
state[:current_line] = word
|
|
43
|
+
elsif visible_length(state[:current_line]) + visible_length(word) <= state[:current_width]
|
|
44
|
+
state[:current_line] += word
|
|
45
|
+
else
|
|
46
|
+
start_new_line(word, state)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def start_new_line(word, state)
|
|
51
|
+
state[:lines] << state[:current_line]
|
|
52
|
+
state[:current_line] = word.lstrip
|
|
53
|
+
state[:current_width] = state[:rest_width]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Support
|
|
5
|
+
# Shared logic for resolving user and channel names from Slack data.
|
|
6
|
+
# Include this module in commands that need to look up user/channel names.
|
|
7
|
+
#
|
|
8
|
+
# == Required Interface
|
|
9
|
+
# Including classes must provide these methods:
|
|
10
|
+
# - runner: Returns the Runner instance for API access
|
|
11
|
+
# - cache_store: Returns the CacheStore for name lookups
|
|
12
|
+
# - debug(message): Logs debug messages (can be a no-op)
|
|
13
|
+
module UserResolver
|
|
14
|
+
# Resolves a DM channel ID to the user's display name
|
|
15
|
+
# @param workspace [Models::Workspace] The workspace to look up in
|
|
16
|
+
# @param channel_id [String] The DM channel ID (starts with D)
|
|
17
|
+
# @param conversations [Api::Conversations] API client for conversation info
|
|
18
|
+
# @return [String] User name or channel ID if not found
|
|
19
|
+
def resolve_dm_user_name(workspace, channel_id, conversations)
|
|
20
|
+
user_id = get_dm_user_id(channel_id, conversations)
|
|
21
|
+
return channel_id unless user_id
|
|
22
|
+
|
|
23
|
+
lookup_user_name(workspace, user_id) || user_id
|
|
24
|
+
rescue ApiError => e
|
|
25
|
+
debug("DM info lookup failed for #{channel_id}: #{e.message}")
|
|
26
|
+
channel_id
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def get_dm_user_id(channel_id, conversations)
|
|
32
|
+
info = conversations.info(channel: channel_id)
|
|
33
|
+
return nil unless info['ok'] && info['channel']
|
|
34
|
+
|
|
35
|
+
info['channel']['user']
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def lookup_user_name(workspace, user_id)
|
|
39
|
+
cached = cache_store.get_user(workspace.name, user_id)
|
|
40
|
+
return cached if cached
|
|
41
|
+
|
|
42
|
+
fetch_and_cache_user_name(workspace, user_id)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def fetch_and_cache_user_name(workspace, user_id)
|
|
46
|
+
users_api = runner.users_api(workspace.name)
|
|
47
|
+
user_info = users_api.info(user_id)
|
|
48
|
+
return nil unless user_info['ok'] && user_info['user']
|
|
49
|
+
|
|
50
|
+
name = extract_name_from_user_info(user_info['user'])
|
|
51
|
+
return nil unless name
|
|
52
|
+
|
|
53
|
+
cache_store.set_user(workspace.name, user_id, name, persist: true)
|
|
54
|
+
name
|
|
55
|
+
rescue ApiError => e
|
|
56
|
+
debug("User lookup failed for #{user_id}: #{e.message}")
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_name_from_user_info(user)
|
|
61
|
+
profile = user['profile'] || {}
|
|
62
|
+
name = profile['display_name']
|
|
63
|
+
name = profile['real_name'] if name.to_s.empty?
|
|
64
|
+
name = user['name'] if name.to_s.empty?
|
|
65
|
+
name unless name.to_s.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
public
|
|
69
|
+
|
|
70
|
+
# Resolves a channel ID to a formatted label (@username or #channel)
|
|
71
|
+
# @param workspace [Models::Workspace] The workspace to look up in
|
|
72
|
+
# @param channel_id [String] The channel ID
|
|
73
|
+
# @return [String] Formatted label like "@username" or "#channel"
|
|
74
|
+
def resolve_conversation_label(workspace, channel_id)
|
|
75
|
+
return resolve_dm_label(workspace, channel_id) if channel_id.start_with?('D')
|
|
76
|
+
|
|
77
|
+
resolve_channel_label(workspace, channel_id)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Extracts the user name from a message hash
|
|
81
|
+
# @param msg [Hash] The message data from API
|
|
82
|
+
# @param workspace [Models::Workspace] The workspace context
|
|
83
|
+
# @return [String] User name, user_id as fallback, or "unknown" if neither found
|
|
84
|
+
def extract_user_from_message(msg, workspace)
|
|
85
|
+
name_from_user_profile(msg) ||
|
|
86
|
+
name_from_username(msg) ||
|
|
87
|
+
name_from_cache(msg, workspace) ||
|
|
88
|
+
msg['user'] || msg['bot_id'] || 'unknown'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def resolve_dm_label(workspace, channel_id)
|
|
94
|
+
conversations = runner.conversations_api(workspace.name)
|
|
95
|
+
user_name = resolve_dm_user_name(workspace, channel_id, conversations)
|
|
96
|
+
"@#{user_name}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def resolve_channel_label(workspace, channel_id)
|
|
100
|
+
cached_name = cache_store.get_channel_name(workspace.name, channel_id)
|
|
101
|
+
return "##{cached_name}" if cached_name
|
|
102
|
+
|
|
103
|
+
fetch_channel_label(workspace, channel_id)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def fetch_channel_label(workspace, channel_id)
|
|
107
|
+
conversations = runner.conversations_api(workspace.name)
|
|
108
|
+
response = conversations.info(channel: channel_id)
|
|
109
|
+
return "##{channel_id}" unless response['ok'] && response['channel']
|
|
110
|
+
|
|
111
|
+
name = response['channel']['name']
|
|
112
|
+
return "##{channel_id}" unless name
|
|
113
|
+
|
|
114
|
+
cache_store.set_channel(workspace.name, name, channel_id)
|
|
115
|
+
"##{name}"
|
|
116
|
+
rescue ApiError => e
|
|
117
|
+
debug("Channel info lookup failed for #{channel_id}: #{e.message}")
|
|
118
|
+
"##{channel_id}"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def name_from_user_profile(msg)
|
|
122
|
+
return nil unless msg['user_profile']
|
|
123
|
+
|
|
124
|
+
name = msg['user_profile']['display_name']
|
|
125
|
+
name = msg['user_profile']['real_name'] if name.to_s.empty?
|
|
126
|
+
name unless name.to_s.empty?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def name_from_username(msg)
|
|
130
|
+
msg['username'] unless msg['username'].to_s.empty?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def name_from_cache(msg, workspace)
|
|
134
|
+
user_id = msg['user'] || msg['bot_id']
|
|
135
|
+
return nil unless user_id
|
|
136
|
+
|
|
137
|
+
cache_store.get_user(workspace.name, user_id)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Slk
|
|
4
4
|
module Support
|
|
5
|
+
# XDG-compliant paths for config and cache directories
|
|
5
6
|
class XdgPaths
|
|
6
7
|
def config_dir
|
|
7
8
|
@config_dir ||= File.join(
|
|
8
|
-
ENV.fetch(
|
|
9
|
-
|
|
9
|
+
ENV.fetch('XDG_CONFIG_HOME', File.join(Dir.home, '.config')),
|
|
10
|
+
'slk'
|
|
10
11
|
)
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def cache_dir
|
|
14
15
|
@cache_dir ||= File.join(
|
|
15
|
-
ENV.fetch(
|
|
16
|
-
|
|
16
|
+
ENV.fetch('XDG_CACHE_HOME', File.join(Dir.home, '.cache')),
|
|
17
|
+
'slk'
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
|
data/lib/slk/version.rb
ADDED
data/lib/slk.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'optparse'
|
|
8
|
+
require 'time'
|
|
9
|
+
require 'io/console'
|
|
10
|
+
|
|
11
|
+
# Slack CLI - A command-line interface for Slack
|
|
12
|
+
module Slk
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
class ApiError < Error; end
|
|
15
|
+
class ConfigError < Error; end
|
|
16
|
+
class EncryptionError < Error; end
|
|
17
|
+
class TokenStoreError < Error; end
|
|
18
|
+
class WorkspaceNotFoundError < ConfigError; end
|
|
19
|
+
class PresetNotFoundError < ConfigError; end
|
|
20
|
+
|
|
21
|
+
autoload :VERSION, 'slk/version'
|
|
22
|
+
autoload :CLI, 'slk/cli'
|
|
23
|
+
autoload :Runner, 'slk/runner'
|
|
24
|
+
|
|
25
|
+
# Data models for Slack entities
|
|
26
|
+
module Models
|
|
27
|
+
autoload :Duration, 'slk/models/duration'
|
|
28
|
+
autoload :Workspace, 'slk/models/workspace'
|
|
29
|
+
autoload :Status, 'slk/models/status'
|
|
30
|
+
autoload :Message, 'slk/models/message'
|
|
31
|
+
autoload :Reaction, 'slk/models/reaction'
|
|
32
|
+
autoload :User, 'slk/models/user'
|
|
33
|
+
autoload :Channel, 'slk/models/channel'
|
|
34
|
+
autoload :Preset, 'slk/models/preset'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Application services for configuration, caching, and API communication
|
|
38
|
+
module Services
|
|
39
|
+
autoload :ApiClient, 'slk/services/api_client'
|
|
40
|
+
autoload :Configuration, 'slk/services/configuration'
|
|
41
|
+
autoload :TokenStore, 'slk/services/token_store'
|
|
42
|
+
autoload :CacheStore, 'slk/services/cache_store'
|
|
43
|
+
autoload :PresetStore, 'slk/services/preset_store'
|
|
44
|
+
autoload :Encryption, 'slk/services/encryption'
|
|
45
|
+
autoload :ReactionEnricher, 'slk/services/reaction_enricher'
|
|
46
|
+
autoload :GemojiSync, 'slk/services/gemoji_sync'
|
|
47
|
+
autoload :EmojiDownloader, 'slk/services/emoji_downloader'
|
|
48
|
+
autoload :EmojiSearcher, 'slk/services/emoji_searcher'
|
|
49
|
+
autoload :ActivityEnricher, 'slk/services/activity_enricher'
|
|
50
|
+
autoload :UnreadMarker, 'slk/services/unread_marker'
|
|
51
|
+
autoload :TargetResolver, 'slk/services/target_resolver'
|
|
52
|
+
autoload :SetupWizard, 'slk/services/setup_wizard'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Output formatters for messages, durations, and emoji
|
|
56
|
+
module Formatters
|
|
57
|
+
autoload :Output, 'slk/formatters/output'
|
|
58
|
+
autoload :DurationFormatter, 'slk/formatters/duration_formatter'
|
|
59
|
+
autoload :MentionReplacer, 'slk/formatters/mention_replacer'
|
|
60
|
+
autoload :EmojiReplacer, 'slk/formatters/emoji_replacer'
|
|
61
|
+
autoload :MessageFormatter, 'slk/formatters/message_formatter'
|
|
62
|
+
autoload :ReactionFormatter, 'slk/formatters/reaction_formatter'
|
|
63
|
+
autoload :JsonMessageFormatter, 'slk/formatters/json_message_formatter'
|
|
64
|
+
autoload :ActivityFormatter, 'slk/formatters/activity_formatter'
|
|
65
|
+
autoload :AttachmentFormatter, 'slk/formatters/attachment_formatter'
|
|
66
|
+
autoload :BlockFormatter, 'slk/formatters/block_formatter'
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# CLI commands implementing user-facing functionality
|
|
70
|
+
module Commands
|
|
71
|
+
autoload :Base, 'slk/commands/base'
|
|
72
|
+
autoload :Status, 'slk/commands/status'
|
|
73
|
+
autoload :Presence, 'slk/commands/presence'
|
|
74
|
+
autoload :Dnd, 'slk/commands/dnd'
|
|
75
|
+
autoload :Messages, 'slk/commands/messages'
|
|
76
|
+
autoload :Thread, 'slk/commands/thread'
|
|
77
|
+
autoload :Unread, 'slk/commands/unread'
|
|
78
|
+
autoload :Catchup, 'slk/commands/catchup'
|
|
79
|
+
autoload :Activity, 'slk/commands/activity'
|
|
80
|
+
autoload :Preset, 'slk/commands/preset'
|
|
81
|
+
autoload :Workspaces, 'slk/commands/workspaces'
|
|
82
|
+
autoload :Cache, 'slk/commands/cache'
|
|
83
|
+
autoload :Emoji, 'slk/commands/emoji'
|
|
84
|
+
autoload :Config, 'slk/commands/config'
|
|
85
|
+
autoload :Help, 'slk/commands/help'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Thin wrappers around Slack API endpoints
|
|
89
|
+
module Api
|
|
90
|
+
autoload :Users, 'slk/api/users'
|
|
91
|
+
autoload :Conversations, 'slk/api/conversations'
|
|
92
|
+
autoload :Dnd, 'slk/api/dnd'
|
|
93
|
+
autoload :Emoji, 'slk/api/emoji'
|
|
94
|
+
autoload :Client, 'slk/api/client'
|
|
95
|
+
autoload :Bots, 'slk/api/bots'
|
|
96
|
+
autoload :Threads, 'slk/api/threads'
|
|
97
|
+
autoload :Usergroups, 'slk/api/usergroups'
|
|
98
|
+
autoload :Activity, 'slk/api/activity'
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Utility classes for paths, parsing, and helpers
|
|
102
|
+
module Support
|
|
103
|
+
autoload :XdgPaths, 'slk/support/xdg_paths'
|
|
104
|
+
autoload :SlackUrlParser, 'slk/support/slack_url_parser'
|
|
105
|
+
autoload :InlineImages, 'slk/support/inline_images'
|
|
106
|
+
autoload :HelpFormatter, 'slk/support/help_formatter'
|
|
107
|
+
autoload :ErrorLogger, 'slk/support/error_logger'
|
|
108
|
+
autoload :UserResolver, 'slk/support/user_resolver'
|
|
109
|
+
autoload :TextWrapper, 'slk/support/text_wrapper'
|
|
110
|
+
autoload :InteractivePrompt, 'slk/support/interactive_prompt'
|
|
111
|
+
end
|
|
112
|
+
end
|