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
|
@@ -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('&', '&').gsub('<', '<').gsub('>', '>')
|
|
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
|
|
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(
|
|
38
|
+
@err.puts(colorize("#{red('Error:')} #{message}"))
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def warn(message)
|
|
41
|
-
@err.puts(colorize("#{yellow(
|
|
42
|
+
@err.puts(colorize("#{yellow('Warning:')} #{message}")) unless @quiet
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def success(message)
|
|
45
|
-
puts(colorize("#{green(
|
|
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(
|
|
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
|
|
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[
|
|
9
|
-
name: data[
|
|
10
|
-
is_private: data[
|
|
11
|
-
is_im: data[
|
|
12
|
-
is_mpim: data[
|
|
13
|
-
is_archived: data[
|
|
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
|
|
41
|
-
when
|
|
42
|
-
when
|
|
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
|