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
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # Formats Slack messages as JSON for structured output
6
+ class JsonMessageFormatter
7
+ def initialize(cache_store:)
8
+ @cache = cache_store
9
+ end
10
+
11
+ # Format a message as a JSON-serializable hash
12
+ def format(message, workspace: nil, options: {})
13
+ result = build_base_result(message)
14
+ result[:reactions] = format_reactions(message.reactions, workspace, options)
15
+
16
+ add_user_name(result, message, workspace, options)
17
+ add_channel_info(result, workspace, options)
18
+
19
+ result
20
+ end
21
+
22
+ private
23
+
24
+ def build_base_result(message)
25
+ {
26
+ ts: message.ts,
27
+ user_id: message.user_id,
28
+ text: message.text,
29
+ reply_count: message.reply_count,
30
+ thread_ts: message.thread_ts,
31
+ attachments: message.attachments,
32
+ files: message.files
33
+ }
34
+ end
35
+
36
+ def format_reactions(reactions, workspace, options)
37
+ reactions.map do |r|
38
+ reaction_hash = { name: r.name, count: r.count }
39
+ reaction_hash[:users] = format_reaction_users(r, workspace, options)
40
+ reaction_hash
41
+ end
42
+ end
43
+
44
+ def format_reaction_users(reaction, workspace, options)
45
+ workspace_name = workspace&.name
46
+
47
+ reaction.users.map do |user_id|
48
+ user_hash = { id: user_id }
49
+ add_user_reaction_name(user_hash, user_id, workspace_name, options)
50
+ add_user_reaction_timestamp(user_hash, reaction, user_id)
51
+ user_hash
52
+ end
53
+ end
54
+
55
+ def add_user_reaction_name(user_hash, user_id, workspace_name, options)
56
+ return if options[:no_names]
57
+ return unless workspace_name
58
+
59
+ cached_name = @cache.get_user(workspace_name, user_id)
60
+ user_hash[:name] = cached_name if cached_name
61
+ end
62
+
63
+ def add_user_reaction_timestamp(user_hash, reaction, user_id)
64
+ return unless reaction.timestamps?
65
+
66
+ timestamp = reaction.timestamp_for(user_id)
67
+ return unless timestamp
68
+
69
+ user_hash[:reacted_at] = timestamp
70
+ user_hash[:reacted_at_iso8601] = Time.at(timestamp.to_f).iso8601
71
+ end
72
+
73
+ def add_user_name(result, message, workspace, options)
74
+ return if options[:no_names]
75
+
76
+ workspace_name = workspace&.name
77
+ return unless workspace_name
78
+
79
+ user_name = @cache.get_user(workspace_name, message.user_id)
80
+ result[:user_name] = user_name if user_name
81
+ end
82
+
83
+ def add_channel_info(result, workspace, options)
84
+ return unless options[:channel_id]
85
+
86
+ result[:channel_id] = options[:channel_id]
87
+ workspace_name = workspace&.name
88
+ return unless workspace_name
89
+
90
+ channel_name = @cache.get_channel_name(workspace_name, options[:channel_id])
91
+ result[:channel_name] = channel_name if channel_name
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # Replaces Slack mention syntax with readable @names and #channels
6
+ # rubocop:disable Metrics/ClassLength
7
+ class MentionReplacer
8
+ USER_MENTION_REGEX = /<@([UW][A-Z0-9]+)(?:\|([^>]+))?>/
9
+ CHANNEL_MENTION_REGEX = /<#([A-Z0-9]+)(?:\|([^>]*))?>/
10
+ SUBTEAM_MENTION_REGEX = /<!subteam\^([A-Z0-9]+)(?:\|@?([^>]+))?>/
11
+ LINK_REGEX = %r{<(https?://[^|>]+)(?:\|([^>]+))?>}
12
+ SPECIAL_MENTIONS = {
13
+ '<!here>' => '@here',
14
+ '<!channel>' => '@channel',
15
+ '<!everyone>' => '@everyone'
16
+ }.freeze
17
+
18
+ def initialize(cache_store:, api_client: nil, on_debug: nil)
19
+ @cache = cache_store
20
+ @api = api_client
21
+ @on_debug = on_debug
22
+ end
23
+
24
+ def replace(text, workspace)
25
+ result = text.dup
26
+ result = replace_user_mentions(result, workspace)
27
+ result = replace_channel_mentions(result, workspace)
28
+ result = replace_subteam_mentions(result, workspace)
29
+ result = replace_links(result)
30
+ replace_special_mentions(result)
31
+ end
32
+
33
+ private
34
+
35
+ def replace_user_mentions(text, workspace)
36
+ text.gsub(USER_MENTION_REGEX) do
37
+ user_id = ::Regexp.last_match(1)
38
+ display_name = ::Regexp.last_match(2)
39
+ name = display_name_or_lookup(display_name, workspace, user_id, :user)
40
+ "@#{name}"
41
+ end
42
+ end
43
+
44
+ def replace_channel_mentions(text, workspace)
45
+ text.gsub(CHANNEL_MENTION_REGEX) do
46
+ channel_id = ::Regexp.last_match(1)
47
+ channel_name = ::Regexp.last_match(2)
48
+ name = display_name_or_lookup(channel_name, workspace, channel_id, :channel)
49
+ "##{name}"
50
+ end
51
+ end
52
+
53
+ def replace_subteam_mentions(text, workspace)
54
+ text.gsub(SUBTEAM_MENTION_REGEX) do
55
+ subteam_id = ::Regexp.last_match(1)
56
+ handle = ::Regexp.last_match(2)
57
+ name = display_name_or_lookup(handle, workspace, subteam_id, :subteam)
58
+ "@#{name}"
59
+ end
60
+ end
61
+
62
+ def replace_links(text)
63
+ text.gsub(LINK_REGEX) { ::Regexp.last_match(2) || ::Regexp.last_match(1) }
64
+ end
65
+
66
+ def replace_special_mentions(text)
67
+ SPECIAL_MENTIONS.each { |pattern, replacement| text.gsub!(pattern, replacement) }
68
+ text
69
+ end
70
+
71
+ def display_name_or_lookup(display_name, workspace, id, type)
72
+ return display_name unless display_name.to_s.empty?
73
+
74
+ lookup_by_type(workspace, id, type) || id
75
+ end
76
+
77
+ def lookup_by_type(workspace, id, type)
78
+ case type
79
+ when :user then lookup_user_name(workspace, id)
80
+ when :channel then lookup_channel_name(workspace, id)
81
+ when :subteam then lookup_subteam_handle(workspace, id)
82
+ end
83
+ end
84
+
85
+ def lookup_user_name(workspace, user_id)
86
+ cached = @cache.get_user(workspace.name, user_id)
87
+ return cached if cached
88
+
89
+ fetch_user_name_from_api(workspace, user_id)
90
+ end
91
+
92
+ def fetch_user_name_from_api(workspace, user_id)
93
+ return nil unless @api
94
+
95
+ response = Api::Users.new(@api, workspace).info(user_id)
96
+ return nil unless response['ok'] && response['user']
97
+
98
+ name = extract_user_display_name(response['user'])
99
+ cache_user_name(workspace, user_id, name)
100
+ name
101
+ rescue ApiError => e
102
+ @on_debug&.call("User lookup failed for #{user_id}: #{e.message}")
103
+ nil
104
+ end
105
+
106
+ def cache_user_name(workspace, user_id, name)
107
+ @cache.set_user(workspace.name, user_id, name, persist: true) unless name.to_s.empty?
108
+ end
109
+
110
+ def extract_user_display_name(user)
111
+ profile = user['profile'] || {}
112
+ profile['display_name'].then { |n| n.to_s.empty? ? nil : n } ||
113
+ profile['real_name'].then { |n| n.to_s.empty? ? nil : n } ||
114
+ user['name'].then { |n| n.to_s.empty? ? nil : n }
115
+ end
116
+
117
+ def lookup_channel_name(workspace, channel_id)
118
+ cached = @cache.get_channel_name(workspace.name, channel_id)
119
+ return cached if cached
120
+
121
+ fetch_channel_name_from_api(workspace, channel_id)
122
+ end
123
+
124
+ def fetch_channel_name_from_api(workspace, channel_id)
125
+ return nil unless @api
126
+
127
+ response = Api::Conversations.new(@api, workspace).info(channel: channel_id)
128
+ return nil unless response['ok'] && response['channel']
129
+
130
+ name = response['channel']['name']
131
+ @cache.set_channel(workspace.name, name, channel_id) if name
132
+ name
133
+ rescue ApiError => e
134
+ @on_debug&.call("Channel lookup failed for #{channel_id}: #{e.message}")
135
+ nil
136
+ end
137
+
138
+ def lookup_subteam_handle(workspace, subteam_id)
139
+ cached = @cache.get_subteam(workspace.name, subteam_id)
140
+ return cached if cached
141
+
142
+ fetch_subteam_handle_from_api(workspace, subteam_id)
143
+ end
144
+
145
+ def fetch_subteam_handle_from_api(workspace, subteam_id)
146
+ return nil unless @api
147
+
148
+ handle = Api::Usergroups.new(@api, workspace).get_handle(subteam_id)
149
+ @cache.set_subteam(workspace.name, subteam_id, handle) if handle
150
+ handle
151
+ rescue ApiError => e
152
+ @on_debug&.call("Subteam lookup failed for #{subteam_id}: #{e.message}")
153
+ nil
154
+ end
155
+ end
156
+ # rubocop:enable Metrics/ClassLength
157
+ end
158
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # Formats Slack messages for terminal display or JSON output
6
+ # rubocop:disable Metrics/ClassLength
7
+ class MessageFormatter
8
+ # rubocop:disable Metrics/ParameterLists
9
+ def initialize(output:, mention_replacer:, emoji_replacer:, cache_store:, api_client: nil, on_debug: nil)
10
+ @output = output
11
+ @mentions = mention_replacer
12
+ @emoji = emoji_replacer
13
+ @cache = cache_store
14
+ @api_client = api_client
15
+ @on_debug = on_debug
16
+ @reaction_formatter = build_reaction_formatter(output, emoji_replacer, cache_store)
17
+ @json_formatter = JsonMessageFormatter.new(cache_store: cache_store)
18
+ end
19
+ # rubocop:enable Metrics/ParameterLists
20
+
21
+ def build_reaction_formatter(output, emoji_replacer, cache_store)
22
+ ReactionFormatter.new(output: output, emoji_replacer: emoji_replacer, cache_store: cache_store)
23
+ end
24
+
25
+ def format(message, workspace:, options: {})
26
+ username = resolve_username(message, workspace, options)
27
+ timestamp = format_timestamp(message.timestamp)
28
+ text = process_text(message.text, workspace, options)
29
+
30
+ header = build_header(timestamp, username)
31
+ display_text = build_display_text(text, message, header, options)
32
+ main_line = "#{header} #{display_text}"
33
+
34
+ build_output_lines(main_line, message, workspace, options, display_text)
35
+ end
36
+
37
+ def format_simple(message, workspace:, options: {})
38
+ username = resolve_username(message, workspace, options)
39
+ timestamp = format_timestamp(message.timestamp)
40
+ text = process_text(message.text, workspace, options)
41
+
42
+ reaction_text = ''
43
+ unless options[:no_reactions] || message.reactions.empty?
44
+ reaction_text = format_reaction_inline(message, options)
45
+ end
46
+
47
+ "#{@output.blue("[#{timestamp}]")} #{@output.bold(username)}: #{text}#{reaction_text}"
48
+ end
49
+
50
+ def format_reaction_inline(message, options)
51
+ @reaction_formatter.format_inline(message.reactions, options)
52
+ end
53
+
54
+ def format_json(message, workspace: nil, options: {})
55
+ @json_formatter.format(message, workspace: workspace, options: options)
56
+ end
57
+
58
+ private
59
+
60
+ def build_header(timestamp, username)
61
+ "#{@output.blue("[#{timestamp}]")} #{@output.bold(username)}:"
62
+ end
63
+
64
+ def build_display_text(text, message, header, options)
65
+ display_text = text.strip
66
+ header_width = Support::TextWrapper.visible_length("#{header.gsub(/\e\[[0-9;]*m/, '')} ")
67
+
68
+ display_text = wrap_display_text(display_text, header_width, options[:width])
69
+ display_text = add_file_placeholder(message, options) if display_text.empty?
70
+
71
+ display_text
72
+ end
73
+
74
+ def wrap_display_text(text, header_width, width)
75
+ return text if text.empty? || !width || width <= header_width
76
+
77
+ first_line_width = width - header_width
78
+ Support::TextWrapper.wrap(text, first_line_width, width)
79
+ end
80
+
81
+ def add_file_placeholder(message, options)
82
+ return '' unless message.files? && !options[:no_files]
83
+
84
+ first_file = message.files.first
85
+ file_name = first_file['name'] || 'file'
86
+ @output.blue("[File: #{file_name}]")
87
+ end
88
+
89
+ def build_output_lines(main_line, message, workspace, options, display_text)
90
+ lines = [main_line]
91
+ text_processor = ->(txt) { process_text(txt, workspace, options) }
92
+
93
+ BlockFormatter.new(text_processor: text_processor)
94
+ .format(message.blocks, message.text, lines, options)
95
+ AttachmentFormatter.new(output: @output, text_processor: text_processor)
96
+ .format(message.attachments, lines, options)
97
+ format_files(message, lines, options, skip_first: display_text.include?('[File:'))
98
+ format_reactions(message, lines, workspace, options)
99
+ format_thread_indicator(message, lines, options)
100
+
101
+ lines.join("\n")
102
+ end
103
+
104
+ def resolve_username(message, workspace, options = {})
105
+ return message.user_id if options[:no_names]
106
+ return message.embedded_username if message.embedded_username
107
+
108
+ cached = @cache.get_user(workspace.name, message.user_id)
109
+ return cached if cached
110
+
111
+ lookup_bot_if_needed(message, workspace) || message.user_id
112
+ end
113
+
114
+ def lookup_bot_if_needed(message, workspace)
115
+ return unless message.user_id.start_with?('B') && @api_client
116
+
117
+ lookup_bot_name(workspace, message.user_id)
118
+ end
119
+
120
+ def lookup_bot_name(workspace, bot_id)
121
+ bots_api = Api::Bots.new(@api_client, workspace, on_debug: @on_debug)
122
+ name = bots_api.get_name(bot_id)
123
+ @cache.set_user(workspace.name, bot_id, name, persist: true) if name
124
+ name
125
+ end
126
+
127
+ def format_timestamp(time)
128
+ time.strftime('%Y-%m-%d %H:%M')
129
+ end
130
+
131
+ def process_text(text, workspace, options)
132
+ result = decode_html_entities(text.dup)
133
+ result = @mentions.replace(result, workspace)
134
+ result = @emoji.replace(result, workspace) unless options[:no_emoji]
135
+ result
136
+ end
137
+
138
+ def decode_html_entities(text)
139
+ text.gsub('&amp;', '&').gsub('&lt;', '<').gsub('&gt;', '>')
140
+ end
141
+
142
+ def format_files(message, lines, options, skip_first: false)
143
+ return if options[:no_files]
144
+
145
+ files = files_to_display(message.files, skip_first)
146
+ files.each { |file| lines << @output.blue("[File: #{file['name'] || 'file'}]") }
147
+ end
148
+
149
+ def files_to_display(files, skip_first)
150
+ return [] if files.empty?
151
+
152
+ skip_first ? (files[1..] || []) : files
153
+ end
154
+
155
+ def format_reactions(message, lines, workspace, options)
156
+ return if message.reactions.empty? || options[:no_reactions]
157
+
158
+ if options[:reaction_timestamps] && message.reactions.any?(&:timestamps?)
159
+ lines.concat(@reaction_formatter.format_with_timestamps(message.reactions, workspace, options))
160
+ else
161
+ lines << @reaction_formatter.format_summary(message.reactions, options)
162
+ end
163
+ end
164
+
165
+ def format_thread_indicator(message, lines, options)
166
+ return unless message.thread? && !options[:in_thread] && !options[:no_threads]
167
+
168
+ reply_text = message.reply_count == 1 ? '1 reply' : "#{message.reply_count} replies"
169
+ lines << @output.cyan("[#{reply_text}]")
170
+ end
171
+ end
172
+ # rubocop:enable Metrics/ClassLength
173
+ end
174
+ end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Formatters
5
+ # Terminal output with ANSI color support
5
6
  class Output
6
7
  COLORS = {
7
8
  red: "\e[0;31m",
@@ -25,7 +26,7 @@ module SlackCli
25
26
  @quiet = quiet
26
27
  end
27
28
 
28
- def puts(message = "")
29
+ def puts(message = '')
29
30
  @io.puts(message) unless @quiet
30
31
  end
31
32
 
@@ -34,15 +35,15 @@ module SlackCli
34
35
  end
35
36
 
36
37
  def error(message)
37
- @err.puts(colorize("#{red("Error:")} #{message}"))
38
+ @err.puts(colorize("#{red('Error:')} #{message}"))
38
39
  end
39
40
 
40
41
  def warn(message)
41
- @err.puts(colorize("#{yellow("Warning:")} #{message}")) unless @quiet
42
+ @err.puts(colorize("#{yellow('Warning:')} #{message}")) unless @quiet
42
43
  end
43
44
 
44
45
  def success(message)
45
- puts(colorize("#{green("")} #{message}"))
46
+ puts(colorize("#{green('')} #{message}"))
46
47
  end
47
48
 
48
49
  def info(message)
@@ -52,7 +53,7 @@ module SlackCli
52
53
  def debug(message)
53
54
  return unless @verbose
54
55
 
55
- @err.puts(colorize("#{gray("[debug]")} #{message}"))
56
+ @err.puts(colorize("#{gray('[debug]')} #{message}"))
56
57
  end
57
58
 
58
59
  # Color helpers
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slk
4
+ module Formatters
5
+ # Formats reaction data for terminal display
6
+ class ReactionFormatter
7
+ def initialize(output:, emoji_replacer:, cache_store:)
8
+ @output = output
9
+ @emoji = emoji_replacer
10
+ @cache = cache_store
11
+ end
12
+
13
+ # Format reactions inline for simple display: " [2 👍, 1 ❤️]"
14
+ def format_inline(reactions, options = {})
15
+ parts = reactions.map do |r|
16
+ emoji = resolve_emoji(r.name, options)
17
+ "#{r.count} #{emoji}"
18
+ end
19
+ " [#{parts.join(', ')}]"
20
+ end
21
+
22
+ # Format reactions as a single line: "[2 👍 1 ❤️]"
23
+ def format_summary(reactions, options = {})
24
+ reaction_text = reactions.map do |r|
25
+ emoji = resolve_emoji(r.name, options)
26
+ "#{r.count} #{emoji}"
27
+ end.join(' ')
28
+
29
+ @output.yellow("[#{reaction_text}]")
30
+ end
31
+
32
+ # Format reactions with timestamps, one per line
33
+ def format_with_timestamps(reactions, workspace, options = {})
34
+ workspace_name = workspace&.name
35
+ lines = []
36
+
37
+ reactions.each do |reaction|
38
+ emoji = resolve_emoji(reaction.name, options)
39
+ user_strings = format_user_timestamps(reaction, workspace_name, options)
40
+ lines << @output.yellow(" ↳ #{emoji} #{user_strings.join(', ')}")
41
+ end
42
+
43
+ lines
44
+ end
45
+
46
+ private
47
+
48
+ def resolve_emoji(name, options)
49
+ if options[:no_emoji]
50
+ ":#{name}:"
51
+ else
52
+ @emoji.lookup_emoji(name) || ":#{name}:"
53
+ end
54
+ end
55
+
56
+ def format_user_timestamps(reaction, workspace_name, options)
57
+ reaction.users.map do |user_id|
58
+ username = resolve_user(user_id, workspace_name, options)
59
+ timestamp = reaction.timestamp_for(user_id)
60
+
61
+ if timestamp
62
+ time_str = format_time(timestamp)
63
+ "#{username} (#{time_str})"
64
+ else
65
+ username
66
+ end
67
+ end
68
+ end
69
+
70
+ def resolve_user(user_id, workspace_name, options)
71
+ return user_id if options[:no_names]
72
+
73
+ if workspace_name
74
+ cached = @cache.get_user(workspace_name, user_id)
75
+ return cached if cached
76
+ end
77
+
78
+ user_id
79
+ end
80
+
81
+ def format_time(slack_timestamp)
82
+ time = Time.at(slack_timestamp.to_f)
83
+ time.strftime('%-I:%M %p') # e.g., "2:45 PM"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,19 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SlackCli
3
+ module Slk
4
4
  module Models
5
5
  Channel = Data.define(:id, :name, :is_private, :is_im, :is_mpim, :is_archived) do
6
6
  def self.from_api(data)
7
7
  new(
8
- id: data["id"],
9
- name: data["name"] || data["name_normalized"],
10
- is_private: data["is_private"] || false,
11
- is_im: data["is_im"] || false,
12
- is_mpim: data["is_mpim"] || false,
13
- is_archived: data["is_archived"] || false
8
+ id: data['id'],
9
+ name: data['name'] || data['name_normalized'],
10
+ is_private: data['is_private'] || false,
11
+ is_im: data['is_im'] || false,
12
+ is_mpim: data['is_mpim'] || false,
13
+ is_archived: data['is_archived'] || false
14
14
  )
15
15
  end
16
16
 
17
+ # rubocop:disable Metrics/ParameterLists
17
18
  def initialize(id:, name: nil, is_private: false, is_im: false, is_mpim: false, is_archived: false)
18
19
  super(
19
20
  id: id.to_s.freeze,
@@ -24,6 +25,7 @@ module SlackCli
24
25
  is_archived: is_archived
25
26
  )
26
27
  end
28
+ # rubocop:enable Metrics/ParameterLists
27
29
 
28
30
  def dm?
29
31
  is_im || is_mpim
@@ -37,9 +39,9 @@ module SlackCli
37
39
  return name if name
38
40
 
39
41
  case id[0]
40
- when "C" then "#channel"
41
- when "G" then "#private"
42
- when "D" then "DM"
42
+ when 'C' then '#channel'
43
+ when 'G' then '#private'
44
+ when 'D' then 'DM'
43
45
  else id
44
46
  end
45
47
  end