slk 0.4.0 → 0.4.2
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 +33 -0
- data/lib/slk/api/saved.rb +26 -0
- data/lib/slk/cli.rb +12 -1
- data/lib/slk/commands/activity.rb +7 -13
- data/lib/slk/commands/base.rb +2 -1
- data/lib/slk/commands/help.rb +2 -0
- data/lib/slk/commands/later.rb +297 -0
- data/lib/slk/commands/messages.rb +4 -3
- data/lib/slk/commands/thread.rb +27 -9
- data/lib/slk/formatters/activity_formatter.rb +4 -7
- data/lib/slk/formatters/markdown_output.rb +98 -0
- data/lib/slk/formatters/message_formatter.rb +10 -18
- data/lib/slk/formatters/reaction_formatter.rb +1 -1
- data/lib/slk/formatters/saved_item_formatter.rb +144 -0
- data/lib/slk/formatters/search_formatter.rb +4 -8
- data/lib/slk/formatters/text_processor.rb +48 -0
- data/lib/slk/models/saved_item.rb +128 -0
- data/lib/slk/runner.rb +22 -2
- data/lib/slk/services/message_resolver.rb +38 -0
- data/lib/slk/support/inline_images.rb +94 -10
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +7 -0
- metadata +8 -7
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Formatters
|
|
5
|
+
# Markdown output adapter with same interface as Output
|
|
6
|
+
# Produces markdown formatting instead of ANSI colors
|
|
7
|
+
class MarkdownOutput
|
|
8
|
+
attr_reader :verbose, :quiet
|
|
9
|
+
|
|
10
|
+
# Accept color: parameter for interface compatibility with Output (ignored for markdown)
|
|
11
|
+
def initialize(io: $stdout, err: $stderr, color: nil, verbose: false, quiet: false) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
|
+
@io = io
|
|
13
|
+
@err = err
|
|
14
|
+
@verbose = verbose
|
|
15
|
+
@quiet = quiet
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def puts(message = '')
|
|
19
|
+
@io.puts(message) unless @quiet
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def print(message)
|
|
23
|
+
@io.print(message) unless @quiet
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error(message)
|
|
27
|
+
@err.puts("**Error:** #{message}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def warn(message)
|
|
31
|
+
@err.puts("*Warning:* #{message}") unless @quiet
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def success(message)
|
|
35
|
+
puts("✓ #{message}")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def info(message)
|
|
39
|
+
puts(message)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def debug(message)
|
|
43
|
+
return unless @verbose
|
|
44
|
+
|
|
45
|
+
@err.puts("*[debug]* #{message}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Markdown formatting helpers - string interpolation handles nil conversion
|
|
49
|
+
# bold -> **text**
|
|
50
|
+
def bold(text)
|
|
51
|
+
"**#{text}**"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# red -> **text** (emphasis for errors)
|
|
55
|
+
def red(text)
|
|
56
|
+
"**#{text}**"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# green -> plain text (success doesn't need markup)
|
|
60
|
+
def green(text)
|
|
61
|
+
text.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# yellow -> *text* (italics for warnings)
|
|
65
|
+
def yellow(text)
|
|
66
|
+
"*#{text}*"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# blue -> `text` (code for timestamps)
|
|
70
|
+
def blue(text)
|
|
71
|
+
"`#{text}`"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# magenta -> *text* (italics for secondary)
|
|
75
|
+
def magenta(text)
|
|
76
|
+
"*#{text}*"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# cyan -> `text` (code for metadata like timestamps)
|
|
80
|
+
def cyan(text)
|
|
81
|
+
"`#{text}`"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# gray -> *text* (italics for secondary info)
|
|
85
|
+
def gray(text)
|
|
86
|
+
"*#{text}*"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def with_verbose(value)
|
|
90
|
+
self.class.new(io: @io, err: @err, verbose: value, quiet: @quiet)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def with_quiet(value)
|
|
94
|
+
self.class.new(io: @io, err: @err, verbose: @verbose, quiet: value)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -6,13 +6,16 @@ module Slk
|
|
|
6
6
|
# rubocop:disable Metrics/ClassLength
|
|
7
7
|
class MessageFormatter
|
|
8
8
|
# rubocop:disable Metrics/ParameterLists
|
|
9
|
-
def initialize(output:, mention_replacer:, emoji_replacer:, cache_store:, api_client: nil,
|
|
9
|
+
def initialize(output:, mention_replacer:, emoji_replacer:, cache_store:, api_client: nil,
|
|
10
|
+
text_processor: nil, on_debug: nil)
|
|
10
11
|
@output = output
|
|
11
|
-
@mentions = mention_replacer
|
|
12
|
-
@emoji = emoji_replacer
|
|
13
12
|
@cache = cache_store
|
|
14
13
|
@api_client = api_client
|
|
15
14
|
@on_debug = on_debug
|
|
15
|
+
@text_processor = text_processor || TextProcessor.new(
|
|
16
|
+
mention_replacer: mention_replacer,
|
|
17
|
+
emoji_replacer: emoji_replacer
|
|
18
|
+
)
|
|
16
19
|
@reaction_formatter = build_reaction_formatter(output, emoji_replacer, cache_store)
|
|
17
20
|
@json_formatter = JsonMessageFormatter.new(cache_store: cache_store)
|
|
18
21
|
end
|
|
@@ -25,7 +28,7 @@ module Slk
|
|
|
25
28
|
def format(message, workspace:, options: {})
|
|
26
29
|
username = resolve_username(message, workspace, options)
|
|
27
30
|
timestamp = format_timestamp(message.timestamp)
|
|
28
|
-
text =
|
|
31
|
+
text = @text_processor.process(message.text, workspace, options)
|
|
29
32
|
|
|
30
33
|
header = build_header(timestamp, username)
|
|
31
34
|
display_text = build_display_text(text, message, header, options)
|
|
@@ -37,7 +40,7 @@ module Slk
|
|
|
37
40
|
def format_simple(message, workspace:, options: {})
|
|
38
41
|
username = resolve_username(message, workspace, options)
|
|
39
42
|
timestamp = format_timestamp(message.timestamp)
|
|
40
|
-
text =
|
|
43
|
+
text = @text_processor.process(message.text, workspace, options)
|
|
41
44
|
|
|
42
45
|
reaction_text = ''
|
|
43
46
|
unless options[:no_reactions] || message.reactions.empty?
|
|
@@ -88,7 +91,7 @@ module Slk
|
|
|
88
91
|
|
|
89
92
|
def build_output_lines(main_line, message, workspace, options, display_text)
|
|
90
93
|
lines = [main_line]
|
|
91
|
-
text_processor = ->(txt) {
|
|
94
|
+
text_processor = ->(txt) { @text_processor.process(txt, workspace, options) }
|
|
92
95
|
|
|
93
96
|
BlockFormatter.new(text_processor: text_processor)
|
|
94
97
|
.format(message.blocks, message.text, lines, options)
|
|
@@ -118,18 +121,7 @@ module Slk
|
|
|
118
121
|
end
|
|
119
122
|
|
|
120
123
|
def format_timestamp(time)
|
|
121
|
-
time.strftime('%Y-%m-%d %H:%M')
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def process_text(text, workspace, options)
|
|
125
|
-
result = decode_html_entities(text.dup)
|
|
126
|
-
result = @mentions.replace(result, workspace)
|
|
127
|
-
result = @emoji.replace(result, workspace) unless options[:no_emoji]
|
|
128
|
-
result
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def decode_html_entities(text)
|
|
132
|
-
text.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
|
124
|
+
time.strftime('%Y-%m-%d %H:%M:%S')
|
|
133
125
|
end
|
|
134
126
|
|
|
135
127
|
def format_files(message, lines, options, skip_first: false)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Formatters
|
|
5
|
+
# Formats saved/later items for terminal display
|
|
6
|
+
class SavedItemFormatter
|
|
7
|
+
def initialize(output:, mention_replacer:, text_processor:, on_debug: nil)
|
|
8
|
+
@output = output
|
|
9
|
+
@mentions = mention_replacer
|
|
10
|
+
@text_processor = text_processor
|
|
11
|
+
@on_debug = on_debug
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Display a single saved item
|
|
15
|
+
# @param truncate [Boolean] if true, truncate to single line at width instead of wrapping
|
|
16
|
+
def display_item(item, workspace, message: nil, width: nil, truncate: false)
|
|
17
|
+
status_badge = format_status_badge(item)
|
|
18
|
+
due_info = format_due_info(item)
|
|
19
|
+
|
|
20
|
+
# First line: status badge and due info
|
|
21
|
+
header_parts = [status_badge, due_info].compact.reject(&:empty?)
|
|
22
|
+
@output.puts header_parts.join(' | ') unless header_parts.empty?
|
|
23
|
+
|
|
24
|
+
# Message content
|
|
25
|
+
display_message(message, workspace, width: width, truncate: truncate) if message
|
|
26
|
+
@output.puts # blank line between items
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# rubocop:disable Metrics/MethodLength
|
|
32
|
+
def format_status_badge(item)
|
|
33
|
+
badge = case item.state
|
|
34
|
+
when 'completed' then '[completed]'
|
|
35
|
+
when 'in_progress' then '[in_progress]'
|
|
36
|
+
when 'saved' then '[saved]'
|
|
37
|
+
else "[#{item.state}]"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Color based on overdue status
|
|
41
|
+
if item.overdue?
|
|
42
|
+
@output.red(badge)
|
|
43
|
+
elsif item.state == 'completed'
|
|
44
|
+
@output.green(badge)
|
|
45
|
+
elsif item.state == 'in_progress'
|
|
46
|
+
@output.yellow(badge)
|
|
47
|
+
else
|
|
48
|
+
@output.blue(badge)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:enable Metrics/MethodLength
|
|
53
|
+
|
|
54
|
+
def format_due_info(item)
|
|
55
|
+
return '' unless item.due_date?
|
|
56
|
+
|
|
57
|
+
time_diff = item.time_until_due
|
|
58
|
+
formatted = format_time_difference(time_diff)
|
|
59
|
+
|
|
60
|
+
if item.overdue?
|
|
61
|
+
"Due: #{@output.red(formatted)}"
|
|
62
|
+
else
|
|
63
|
+
"Due: #{formatted}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# rubocop:disable Metrics/MethodLength
|
|
68
|
+
def format_time_difference(seconds)
|
|
69
|
+
abs_seconds = seconds.abs
|
|
70
|
+
ago = seconds.negative?
|
|
71
|
+
|
|
72
|
+
formatted = if abs_seconds < 60
|
|
73
|
+
"#{abs_seconds}s"
|
|
74
|
+
elsif abs_seconds < 3600
|
|
75
|
+
"#{abs_seconds / 60}m"
|
|
76
|
+
elsif abs_seconds < 86_400
|
|
77
|
+
"#{abs_seconds / 3600}h"
|
|
78
|
+
else
|
|
79
|
+
"#{abs_seconds / 86_400}d"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
ago ? "#{formatted} ago" : "in #{formatted}"
|
|
83
|
+
end
|
|
84
|
+
# rubocop:enable Metrics/MethodLength
|
|
85
|
+
|
|
86
|
+
def display_message(message, workspace, width: nil, truncate: false)
|
|
87
|
+
username = resolve_message_author(message, workspace)
|
|
88
|
+
text = prepare_message_text(message, workspace)
|
|
89
|
+
header = " #{@output.bold(username)}: "
|
|
90
|
+
header_width = Support::TextWrapper.visible_length(header)
|
|
91
|
+
|
|
92
|
+
if truncate
|
|
93
|
+
display_truncated_message(header, text, width, header_width)
|
|
94
|
+
else
|
|
95
|
+
display_wrapped_message(header, text, width, header_width)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def display_truncated_message(header, text, width, header_width)
|
|
100
|
+
# Single line, truncated at width
|
|
101
|
+
first_line = text.lines.first&.strip || text
|
|
102
|
+
max_text_width = width ? [width - header_width, 10].max : 100
|
|
103
|
+
@output.puts "#{header}#{truncate_text(first_line, max_text_width)}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def display_wrapped_message(header, text, width, header_width)
|
|
107
|
+
# Full message with wrapping
|
|
108
|
+
wrapped = wrap_text(text, header_width, width)
|
|
109
|
+
lines = wrapped.lines
|
|
110
|
+
|
|
111
|
+
first_line = lines.first&.rstrip || wrapped
|
|
112
|
+
@output.puts "#{header}#{first_line}"
|
|
113
|
+
lines[1..].each { |line| @output.puts " #{line.rstrip}" } if lines.length > 1
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resolve_message_author(message, workspace)
|
|
117
|
+
if message['user']
|
|
118
|
+
@mentions.lookup_user_name(workspace, message['user']) || message['user']
|
|
119
|
+
elsif message['bot_id']
|
|
120
|
+
'Bot'
|
|
121
|
+
else
|
|
122
|
+
'Unknown'
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def prepare_message_text(message, workspace)
|
|
127
|
+
@text_processor.process(message['text'], workspace)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def wrap_text(text, indent_width, width)
|
|
131
|
+
return text if text.empty? || !width
|
|
132
|
+
|
|
133
|
+
# If header is wider than target, first line gets no wrap, subsequent lines wrap at width - 2
|
|
134
|
+
first_line_width = [width - indent_width, 10].max
|
|
135
|
+
continuation_width = [width - 2, 10].max
|
|
136
|
+
Support::TextWrapper.wrap(text, first_line_width, continuation_width)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def truncate_text(text, max_length)
|
|
140
|
+
text.length > max_length ? "#{text[0...max_length]}..." : text
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -4,10 +4,10 @@ module Slk
|
|
|
4
4
|
module Formatters
|
|
5
5
|
# Formats search results for terminal display
|
|
6
6
|
class SearchFormatter
|
|
7
|
-
def initialize(output:,
|
|
7
|
+
def initialize(output:, mention_replacer:, text_processor:)
|
|
8
8
|
@output = output
|
|
9
|
-
@emoji = emoji_replacer
|
|
10
9
|
@mentions = mention_replacer
|
|
10
|
+
@text_processor = text_processor
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# Display a list of search results
|
|
@@ -54,11 +54,7 @@ module Slk
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
def prepare_text(text, workspace, options)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
text = @emoji.replace(text) unless options[:no_emoji]
|
|
60
|
-
text = @mentions.replace(text, workspace) unless options[:no_mentions]
|
|
61
|
-
text
|
|
57
|
+
@text_processor.process(text, workspace, options)
|
|
62
58
|
end
|
|
63
59
|
|
|
64
60
|
def display_files(files)
|
|
@@ -68,7 +64,7 @@ module Slk
|
|
|
68
64
|
end
|
|
69
65
|
|
|
70
66
|
def format_time(time)
|
|
71
|
-
time.strftime('%Y-%m-%d %H:%M')
|
|
67
|
+
time.strftime('%Y-%m-%d %H:%M:%S')
|
|
72
68
|
end
|
|
73
69
|
end
|
|
74
70
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Formatters
|
|
5
|
+
# Centralized text processing for message display
|
|
6
|
+
# Handles HTML entity decoding, mention replacement, and emoji replacement
|
|
7
|
+
class TextProcessor
|
|
8
|
+
def initialize(mention_replacer:, emoji_replacer:)
|
|
9
|
+
@mentions = mention_replacer
|
|
10
|
+
@emoji = emoji_replacer
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Process raw Slack message text for display
|
|
14
|
+
# @param text [String] Raw message text from Slack API
|
|
15
|
+
# @param workspace [Models::Workspace] The workspace for name resolution
|
|
16
|
+
# @param options [Hash] Processing options
|
|
17
|
+
# @option options [Boolean] :no_emoji Skip emoji replacement
|
|
18
|
+
# @option options [Boolean] :no_mentions Skip mention replacement
|
|
19
|
+
# @return [String] Processed text ready for display
|
|
20
|
+
def process(text, workspace, options = {})
|
|
21
|
+
return '[No text]' if text.to_s.empty?
|
|
22
|
+
|
|
23
|
+
result = decode_html_entities(text.dup)
|
|
24
|
+
result = safe_replace_mentions(result, workspace) unless options[:no_mentions]
|
|
25
|
+
result = safe_replace_emoji(result, workspace) unless options[:no_emoji]
|
|
26
|
+
result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def decode_html_entities(text)
|
|
32
|
+
text.gsub('&', '&').gsub('<', '<').gsub('>', '>')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def safe_replace_mentions(text, workspace)
|
|
36
|
+
@mentions.replace(text, workspace)
|
|
37
|
+
rescue StandardError
|
|
38
|
+
text
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def safe_replace_emoji(text, workspace)
|
|
42
|
+
@emoji.replace(text, workspace)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
text
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Models
|
|
5
|
+
# State constants for saved items
|
|
6
|
+
SAVED_STATE_SAVED = 'saved'
|
|
7
|
+
SAVED_STATE_IN_PROGRESS = 'in_progress'
|
|
8
|
+
SAVED_STATE_COMPLETED = 'completed'
|
|
9
|
+
|
|
10
|
+
# Represents a saved/later item from Slack's saved.list API
|
|
11
|
+
# rubocop:disable Metrics/ParameterLists
|
|
12
|
+
SavedItem = Data.define(
|
|
13
|
+
:item_id,
|
|
14
|
+
:item_type,
|
|
15
|
+
:ts,
|
|
16
|
+
:state,
|
|
17
|
+
:date_created,
|
|
18
|
+
:date_due,
|
|
19
|
+
:date_completed,
|
|
20
|
+
:is_archived
|
|
21
|
+
) do
|
|
22
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
23
|
+
def self.from_api(data)
|
|
24
|
+
new(
|
|
25
|
+
item_id: data['item_id'] || data['channel_id'] || data['conversation_id'],
|
|
26
|
+
item_type: data['item_type'] || data['type'] || 'message',
|
|
27
|
+
ts: data['ts'] || data['message_ts'],
|
|
28
|
+
state: data['state'] || SAVED_STATE_SAVED,
|
|
29
|
+
date_created: parse_timestamp(data['date_created']),
|
|
30
|
+
date_due: parse_timestamp(data['date_due']),
|
|
31
|
+
date_completed: parse_timestamp(data['date_completed']),
|
|
32
|
+
is_archived: data['is_archived'] || false
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
36
|
+
|
|
37
|
+
def self.parse_timestamp(value)
|
|
38
|
+
return nil if value.nil? || value.to_i.zero?
|
|
39
|
+
|
|
40
|
+
value.to_i
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method :parse_timestamp
|
|
44
|
+
|
|
45
|
+
# rubocop:disable Naming/MethodParameterName
|
|
46
|
+
def initialize(
|
|
47
|
+
item_id:,
|
|
48
|
+
item_type: 'message',
|
|
49
|
+
ts: nil,
|
|
50
|
+
state: SAVED_STATE_SAVED,
|
|
51
|
+
date_created: nil,
|
|
52
|
+
date_due: nil,
|
|
53
|
+
date_completed: nil,
|
|
54
|
+
is_archived: false
|
|
55
|
+
)
|
|
56
|
+
super(
|
|
57
|
+
item_id: item_id.to_s.freeze,
|
|
58
|
+
item_type: item_type.to_s.freeze,
|
|
59
|
+
ts: ts&.to_s&.freeze,
|
|
60
|
+
state: state.to_s.freeze,
|
|
61
|
+
date_created: date_created,
|
|
62
|
+
date_due: date_due,
|
|
63
|
+
date_completed: date_completed,
|
|
64
|
+
is_archived: is_archived
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
# rubocop:enable Naming/MethodParameterName
|
|
68
|
+
|
|
69
|
+
# Channel ID alias for compatibility with message fetching
|
|
70
|
+
alias_method :channel_id, :item_id
|
|
71
|
+
|
|
72
|
+
# State predicates
|
|
73
|
+
def completed?
|
|
74
|
+
state == SAVED_STATE_COMPLETED
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def in_progress?
|
|
78
|
+
state == SAVED_STATE_IN_PROGRESS
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def saved?
|
|
82
|
+
state == SAVED_STATE_SAVED
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def archived?
|
|
86
|
+
is_archived
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Due date helpers
|
|
90
|
+
def due_date?
|
|
91
|
+
!date_due.nil?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def overdue?
|
|
95
|
+
return false unless due_date?
|
|
96
|
+
return false if completed?
|
|
97
|
+
|
|
98
|
+
date_due < Time.now.to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def due_time
|
|
102
|
+
return nil unless due_date?
|
|
103
|
+
|
|
104
|
+
Time.at(date_due)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def created_time
|
|
108
|
+
return nil unless date_created
|
|
109
|
+
|
|
110
|
+
Time.at(date_created)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def completed_time
|
|
114
|
+
return nil unless date_completed
|
|
115
|
+
|
|
116
|
+
Time.at(date_completed)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Time until/since due date
|
|
120
|
+
def time_until_due
|
|
121
|
+
return nil unless due_date?
|
|
122
|
+
|
|
123
|
+
date_due - Time.now.to_i
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
# rubocop:enable Metrics/ParameterLists
|
|
127
|
+
end
|
|
128
|
+
end
|
data/lib/slk/runner.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Slk
|
|
4
4
|
# Dependency injection container providing services to commands
|
|
5
|
+
# rubocop:disable Metrics/ClassLength
|
|
5
6
|
class Runner
|
|
6
7
|
attr_reader :output, :config, :token_store, :api_client, :cache_store, :preset_store
|
|
7
8
|
|
|
@@ -83,6 +84,17 @@ module Slk
|
|
|
83
84
|
Api::Search.new(@api_client, workspace(workspace_name))
|
|
84
85
|
end
|
|
85
86
|
|
|
87
|
+
def saved_api(workspace_name = nil)
|
|
88
|
+
Api::Saved.new(@api_client, workspace(workspace_name))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def message_resolver(workspace_name = nil)
|
|
92
|
+
Services::MessageResolver.new(
|
|
93
|
+
conversations_api: conversations_api(workspace_name),
|
|
94
|
+
on_debug: ->(msg) { @output.debug(msg) }
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
86
98
|
# Formatter helpers
|
|
87
99
|
def message_formatter
|
|
88
100
|
@message_formatter ||= Formatters::MessageFormatter.new(
|
|
@@ -107,6 +119,13 @@ module Slk
|
|
|
107
119
|
@emoji_replacer ||= Formatters::EmojiReplacer.new
|
|
108
120
|
end
|
|
109
121
|
|
|
122
|
+
def text_processor
|
|
123
|
+
@text_processor ||= Formatters::TextProcessor.new(
|
|
124
|
+
mention_replacer: mention_replacer,
|
|
125
|
+
emoji_replacer: emoji_replacer
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
110
129
|
def duration_formatter
|
|
111
130
|
@duration_formatter ||= Formatters::DurationFormatter.new
|
|
112
131
|
end
|
|
@@ -114,8 +133,8 @@ module Slk
|
|
|
114
133
|
def search_formatter
|
|
115
134
|
@search_formatter ||= Formatters::SearchFormatter.new(
|
|
116
135
|
output: @output,
|
|
117
|
-
|
|
118
|
-
|
|
136
|
+
mention_replacer: mention_replacer,
|
|
137
|
+
text_processor: text_processor
|
|
119
138
|
)
|
|
120
139
|
end
|
|
121
140
|
|
|
@@ -135,4 +154,5 @@ module Slk
|
|
|
135
154
|
@cache_store.on_warning = warning_handler
|
|
136
155
|
end
|
|
137
156
|
end
|
|
157
|
+
# rubocop:enable Metrics/ClassLength
|
|
138
158
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slk
|
|
4
|
+
module Services
|
|
5
|
+
# Resolves and fetches individual messages by timestamp from channels
|
|
6
|
+
# Used by activity feed, saved items, and other features that need to
|
|
7
|
+
# fetch message content by ts.
|
|
8
|
+
class MessageResolver
|
|
9
|
+
def initialize(conversations_api:, on_debug: nil)
|
|
10
|
+
@api = conversations_api
|
|
11
|
+
@on_debug = on_debug
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Fetch a single message by its timestamp from a channel
|
|
15
|
+
# @param channel_id [String] The channel ID
|
|
16
|
+
# @param message_ts [String] The message timestamp
|
|
17
|
+
# @return [Hash, nil] The message data or nil if not found
|
|
18
|
+
def fetch_by_ts(channel_id, message_ts)
|
|
19
|
+
response = fetch_message_history(channel_id, message_ts)
|
|
20
|
+
return nil unless response['ok'] && response['messages']&.any?
|
|
21
|
+
|
|
22
|
+
response['messages'].find { |msg| msg['ts'] == message_ts }
|
|
23
|
+
rescue ApiError => e
|
|
24
|
+
@on_debug&.call("Could not fetch message #{message_ts} from #{channel_id}: #{e.message}")
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def fetch_message_history(channel_id, message_ts)
|
|
31
|
+
# Use a narrow time window around the message timestamp
|
|
32
|
+
oldest_ts = (message_ts.to_f - 1).to_s
|
|
33
|
+
latest_ts = (message_ts.to_f + 1).to_s
|
|
34
|
+
@api.history(channel: channel_id, limit: 10, oldest: oldest_ts, latest: latest_ts)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|