slk 0.10.0 → 0.11.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 +24 -0
- data/lib/slk/api/conversations.rb +6 -2
- data/lib/slk/cli.rb +1 -0
- data/lib/slk/commands/help.rb +2 -0
- data/lib/slk/commands/search.rb +55 -48
- data/lib/slk/commands/sent.rb +288 -0
- data/lib/slk/formatters/attachment_formatter.rb +7 -3
- data/lib/slk/formatters/search_formatter.rb +25 -11
- data/lib/slk/formatters/sent_formatter.rb +193 -0
- data/lib/slk/models/search_result.rb +2 -0
- data/lib/slk/runner.rb +4 -0
- data/lib/slk/services/search_pages.rb +65 -0
- data/lib/slk/services/sent_changes.rb +293 -0
- data/lib/slk/services/sent_channel_label.rb +66 -0
- data/lib/slk/services/sent_conversations.rb +227 -0
- data/lib/slk/support/check_in_time.rb +73 -0
- data/lib/slk/version.rb +1 -1
- data/lib/slk.rb +7 -0
- metadata +9 -2
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
|
|
5
|
+
module Slk
|
|
6
|
+
module Services
|
|
7
|
+
# Expands indexed from:me hits into bounded channel windows, full DM
|
|
8
|
+
# histories and full threads. Search is the seed, not the context source.
|
|
9
|
+
# rubocop:disable Metrics/ClassLength
|
|
10
|
+
class SentConversations
|
|
11
|
+
Conversation = Data.define(:workspace, :channel_id, :channel_name, :channel_type, :type, :thread_ts,
|
|
12
|
+
:first_sent_ts, :messages, :last_speaker_is_me, :dropped_messages,
|
|
13
|
+
:self_user_id, :self_username)
|
|
14
|
+
DEFAULT_BEFORE = 5
|
|
15
|
+
DEFAULT_AFTER_MINUTES = 30
|
|
16
|
+
DEFAULT_MAX = 200
|
|
17
|
+
|
|
18
|
+
# A date span plus three independent window/output limits.
|
|
19
|
+
# rubocop:disable Metrics/ParameterLists
|
|
20
|
+
def initialize(runner:, start_date:, end_date:, before: DEFAULT_BEFORE,
|
|
21
|
+
after_minutes: DEFAULT_AFTER_MINUTES, max: DEFAULT_MAX)
|
|
22
|
+
@runner = runner
|
|
23
|
+
@start_date = start_date
|
|
24
|
+
@end_date = end_date
|
|
25
|
+
@before = before
|
|
26
|
+
@after_seconds = after_minutes * 60
|
|
27
|
+
@max = max
|
|
28
|
+
end
|
|
29
|
+
# rubocop:enable Metrics/ParameterLists
|
|
30
|
+
|
|
31
|
+
def collect(entries)
|
|
32
|
+
@self_ids = entries.to_h { |workspace, result| [workspace.name, self_id(workspace, result)] }
|
|
33
|
+
groups = entries.group_by { |workspace, result| [workspace.name, result.channel_id] }
|
|
34
|
+
conversations = groups.flat_map { |_key, group| collect_channel(group) }
|
|
35
|
+
# A thread parent can also appear in a channel window / DM history.
|
|
36
|
+
# Thread owns the message so the same (channel, ts) is never repeated.
|
|
37
|
+
ordered_conversations(deduplicate(conversations))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def ordered_conversations(conversations)
|
|
43
|
+
conversations.sort_by do |conversation|
|
|
44
|
+
# Keep Slack's fractional timestamp precise; identity breaks ties regardless of input order or platform.
|
|
45
|
+
[BigDecimal(conversation.first_sent_ts), conversation.workspace.name.to_s,
|
|
46
|
+
conversation.channel_id.to_s, conversation.thread_ts.to_s]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Partitioning a channel needs both the reply roots and unthreaded hits.
|
|
51
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
52
|
+
def collect_channel(group)
|
|
53
|
+
workspace, seed = group.first
|
|
54
|
+
hits = group.map(&:last)
|
|
55
|
+
@self_id = @self_ids[workspace.name]
|
|
56
|
+
@api = @runner.conversations_api(workspace.name)
|
|
57
|
+
threaded = hits.select { |hit| hit.thread_ts && hit.thread_ts != hit.ts }.group_by(&:thread_ts)
|
|
58
|
+
ordinary = hits.reject { |hit| threaded.key?(hit.thread_ts) || threaded.key?(hit.ts) }
|
|
59
|
+
threads = threaded.map do |root, results|
|
|
60
|
+
parent_hit = hits.find { |hit| hit.ts == root }
|
|
61
|
+
thread_hits = parent_hit ? [parent_hit, *results] : results
|
|
62
|
+
thread_conversation(workspace, seed, root, thread_hits)
|
|
63
|
+
end
|
|
64
|
+
spans = if seed.dm?
|
|
65
|
+
ordinary.empty? ? [] : [dm_conversation(workspace, seed, ordinary)]
|
|
66
|
+
else
|
|
67
|
+
channel_conversations(workspace, seed, ordinary)
|
|
68
|
+
end
|
|
69
|
+
threads + spans
|
|
70
|
+
end
|
|
71
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
72
|
+
|
|
73
|
+
def self_id(workspace, result)
|
|
74
|
+
return result.user_id if result.user_id.to_s.match?(/\A[UW][A-Z0-9]+\z/i)
|
|
75
|
+
|
|
76
|
+
cached = @runner.cache_store.get_meta(workspace.name, 'self_user_id')
|
|
77
|
+
return cached if cached
|
|
78
|
+
|
|
79
|
+
id = @runner.client_api(workspace.name).auth_test['user_id']
|
|
80
|
+
raise ApiError, "Cannot identify sender in #{workspace.name}" unless id
|
|
81
|
+
|
|
82
|
+
@runner.cache_store.set_meta(workspace.name, 'self_user_id', id)
|
|
83
|
+
id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def thread_conversation(workspace, seed, root, hits)
|
|
87
|
+
raw = thread_messages(seed.channel_id, root)
|
|
88
|
+
build(workspace, seed, 'thread', root, hits, raw)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def dm_conversation(workspace, seed, hits)
|
|
92
|
+
start_at = @start_date.to_time.to_i
|
|
93
|
+
# Slack's latest is exclusive; +1 includes messages in this second.
|
|
94
|
+
end_at = [(@end_date + 1).to_time.to_i, Time.now.to_i + 1].min
|
|
95
|
+
raw = page_through(:history, channel: seed.channel_id, oldest: start_at.to_s, latest: end_at.to_s)
|
|
96
|
+
build(workspace, seed, seed.channel_type, nil, hits, raw, expand_threads: true)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def channel_conversations(workspace, seed, hits)
|
|
100
|
+
merge_windows(hits).map { |window| channel_window(workspace, seed, window) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def channel_window(workspace, seed, window)
|
|
104
|
+
preceding = preceding_messages(seed.channel_id, window.first.ts)
|
|
105
|
+
following = following_messages(seed.channel_id, window)
|
|
106
|
+
build(workspace, seed, 'channel', nil, window, preceding + following, expand_threads: true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def preceding_messages(channel_id, timestamp)
|
|
110
|
+
return [] if @before.zero?
|
|
111
|
+
|
|
112
|
+
@api.history(channel: channel_id, latest: timestamp, limit: @before).fetch('messages', [])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def following_messages(channel_id, window)
|
|
116
|
+
return [] if @after_seconds.zero?
|
|
117
|
+
|
|
118
|
+
end_at = (BigDecimal(window.last.ts) + @after_seconds).to_s('F')
|
|
119
|
+
page_through(:history, channel: channel_id, oldest: window.first.ts, latest: end_at, inclusive: true)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def merge_windows(hits)
|
|
123
|
+
hits.sort_by { |hit| hit.ts.to_f }.each_with_object([]) do |hit, windows|
|
|
124
|
+
if windows.any? && hit.ts.to_f <= windows.last.last.ts.to_f + @after_seconds
|
|
125
|
+
windows.last << hit
|
|
126
|
+
else
|
|
127
|
+
windows << [hit]
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def thread_messages(channel_id, root)
|
|
133
|
+
page_through(:replies, channel: channel_id, timestamp: root)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Cursor follow-up is necessary even when the first page is full.
|
|
137
|
+
# rubocop:disable Metrics/MethodLength
|
|
138
|
+
def page_through(endpoint, **params)
|
|
139
|
+
messages = []
|
|
140
|
+
cursor = nil
|
|
141
|
+
loop do
|
|
142
|
+
response = @api.public_send(endpoint, **params, limit: 200, cursor: cursor)
|
|
143
|
+
messages.concat(response.fetch('messages', []))
|
|
144
|
+
break unless response['has_more']
|
|
145
|
+
|
|
146
|
+
next_cursor = response.dig('response_metadata', 'next_cursor').to_s
|
|
147
|
+
if next_cursor.empty? || next_cursor == cursor
|
|
148
|
+
raise ApiError, "conversations.#{endpoint} has_more without a new cursor"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
cursor = next_cursor
|
|
152
|
+
end
|
|
153
|
+
messages
|
|
154
|
+
end
|
|
155
|
+
# rubocop:enable Metrics/MethodLength
|
|
156
|
+
|
|
157
|
+
# Keep the search hits as a fallback at Slack's exclusive history bounds.
|
|
158
|
+
# rubocop:disable Metrics/ParameterLists
|
|
159
|
+
def build(workspace, seed, type, root, hits, raw, expand_threads: false)
|
|
160
|
+
# Search's own hits ensure an indexed sent message cannot disappear just
|
|
161
|
+
# because history omits thread replies or returns a window boundary.
|
|
162
|
+
raw += hits.map { |hit| search_message(hit) }
|
|
163
|
+
raw = expand_own_threads(raw, seed, hits) if expand_threads
|
|
164
|
+
messages = normalize(raw, seed.channel_id)
|
|
165
|
+
last_is_me = messages.last&.user_id == @self_id
|
|
166
|
+
Conversation.new(workspace: workspace, channel_id: seed.channel_id, channel_name: seed.channel_name,
|
|
167
|
+
channel_type: seed.channel_type, type: type, thread_ts: root, first_sent_ts: hits.first.ts,
|
|
168
|
+
messages: messages, last_speaker_is_me: last_is_me, dropped_messages: 0,
|
|
169
|
+
self_user_id: @self_id, self_username: hits.first.username)
|
|
170
|
+
end
|
|
171
|
+
# rubocop:enable Metrics/ParameterLists
|
|
172
|
+
|
|
173
|
+
def search_message(hit)
|
|
174
|
+
{ 'ts' => hit.ts, 'user' => @self_id, 'username' => hit.username, 'text' => hit.text,
|
|
175
|
+
'thread_ts' => hit.thread_ts, 'reply_count' => hit.reply_count }
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def expand_own_threads(raw, seed, hits)
|
|
179
|
+
own_roots(raw, seed, hits).each { |root| raw.concat(thread_messages(seed.channel_id, root)) }
|
|
180
|
+
raw
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def own_roots(raw, seed, hits)
|
|
184
|
+
# Five preceding channel messages may include an old post of ours;
|
|
185
|
+
# don't expand its unrelated thread into today's conversation.
|
|
186
|
+
earliest = seed.dm? ? @start_date.to_time.to_i : hits.first.ts.to_f
|
|
187
|
+
raw.filter_map { |message| message['ts'] if own_thread_root?(message, earliest) }.uniq
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def own_thread_root?(message, earliest)
|
|
191
|
+
message['user'] == @self_id && message['reply_count'].to_i.positive? &&
|
|
192
|
+
message['ts'].to_f >= earliest
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def normalize(raw, channel_id)
|
|
196
|
+
raw.select { |message| readable?(message) }
|
|
197
|
+
.uniq { |message| message['ts'] }
|
|
198
|
+
.sort_by { |message| message['ts'].to_f }
|
|
199
|
+
.map { |message| Models::Message.from_api(message, channel_id: channel_id) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def readable?(message)
|
|
203
|
+
message['ts'] && (message['user'] || message['bot_id'] || message['username'])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def deduplicate(conversations)
|
|
207
|
+
seen = {}
|
|
208
|
+
conversations.filter_map { |conversation| unique_conversation(conversation, seen) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Global dedupe happens before --max so a duplicate cannot consume a slot.
|
|
212
|
+
# rubocop:disable Metrics/AbcSize
|
|
213
|
+
def unique_conversation(conversation, seen)
|
|
214
|
+
key = [conversation.workspace.name, conversation.channel_id]
|
|
215
|
+
unique = conversation.messages.reject { |message| seen[[*key, message.ts]] }
|
|
216
|
+
return if unique.empty?
|
|
217
|
+
|
|
218
|
+
unique.each { |message| seen[[*key, message.ts]] = true }
|
|
219
|
+
kept = @max.zero? ? unique : unique.last(@max)
|
|
220
|
+
conversation.with(messages: kept, dropped_messages: unique.size - kept.size,
|
|
221
|
+
last_speaker_is_me: unique.last.user_id == @self_ids[conversation.workspace.name])
|
|
222
|
+
end
|
|
223
|
+
# rubocop:enable Metrics/AbcSize
|
|
224
|
+
end
|
|
225
|
+
# rubocop:enable Metrics/ClassLength
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
require 'date'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Slk
|
|
8
|
+
module Support
|
|
9
|
+
# A stateless, exact timestamp for sent-conversation change detection.
|
|
10
|
+
class CheckInTime
|
|
11
|
+
RELATIVE = /\A(\d+)([mhd])\z/i
|
|
12
|
+
CLOCK = /\A(\d{1,2}):(\d{2})\z/
|
|
13
|
+
EPOCH = /\A\d{9,12}(?:\.\d{1,6})?\z/
|
|
14
|
+
ISO = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:Z|[+-]\d{2}:\d{2})?\z/
|
|
15
|
+
|
|
16
|
+
def self.parse(value, now: Time.now)
|
|
17
|
+
time = case value
|
|
18
|
+
when RELATIVE then relative(Regexp.last_match, now)
|
|
19
|
+
when CLOCK then clock(Regexp.last_match, now)
|
|
20
|
+
when EPOCH then epoch_time(value)
|
|
21
|
+
when ISO then iso_time(value)
|
|
22
|
+
else raise UsageError, 'Invalid --changed-since time. Use H:MM or HH:MM, ISO, epoch, 90m, 2h, or 1d.'
|
|
23
|
+
end
|
|
24
|
+
raise UsageError, '--changed-since must not be in the future.' if time > now
|
|
25
|
+
|
|
26
|
+
time
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.timestamp(time)
|
|
30
|
+
format('%<seconds>d.%<micros>06d', seconds: time.to_i, micros: time.usec)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.epoch_time(value)
|
|
34
|
+
Time.at(BigDecimal(value).to_r)
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
raise UsageError, "Invalid --changed-since time: #{value.inspect}."
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.iso_time(value)
|
|
40
|
+
Date.iso8601(value[0, 10])
|
|
41
|
+
with_seconds = value.sub(/(T\d{2}:\d{2})(?=Z|[+-]\d{2}:\d{2}|\z)/, '\\1:00')
|
|
42
|
+
Time.iso8601(with_seconds)
|
|
43
|
+
rescue ArgumentError
|
|
44
|
+
raise UsageError, "Invalid --changed-since time: #{value.inspect}."
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.relative(match, now)
|
|
48
|
+
amount = match[1].to_i
|
|
49
|
+
raise UsageError, '--changed-since duration must be positive.' if amount.zero?
|
|
50
|
+
|
|
51
|
+
seconds = { 'm' => 60, 'h' => 3600, 'd' => 86_400 }.fetch(match[2].downcase)
|
|
52
|
+
now - (amount * seconds)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.clock(match, now)
|
|
56
|
+
hour, minute = match.captures.map(&:to_i)
|
|
57
|
+
raise UsageError, 'Invalid --changed-since clock time.' unless hour < 24 && minute < 60
|
|
58
|
+
|
|
59
|
+
day = now.to_date
|
|
60
|
+
time = local_clock(day, hour, minute)
|
|
61
|
+
return time if time <= now
|
|
62
|
+
|
|
63
|
+
local_clock(day - 1, hour, minute)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.local_clock(day, hour, minute)
|
|
67
|
+
Time.local(day.year, day.month, day.day, hour, minute)
|
|
68
|
+
rescue ArgumentError
|
|
69
|
+
raise UsageError, 'Invalid --changed-since clock time.'
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/slk/version.rb
CHANGED
data/lib/slk.rb
CHANGED
|
@@ -113,6 +113,10 @@ module Slk
|
|
|
113
113
|
autoload :DeactivationScanner, 'slk/services/deactivation_scanner'
|
|
114
114
|
autoload :StartDateLookup, 'slk/services/start_date_lookup'
|
|
115
115
|
autoload :StartDateField, 'slk/services/start_date_field'
|
|
116
|
+
autoload :SearchPages, 'slk/services/search_pages'
|
|
117
|
+
autoload :SentConversations, 'slk/services/sent_conversations'
|
|
118
|
+
autoload :SentChanges, 'slk/services/sent_changes'
|
|
119
|
+
autoload :SentChannelLabel, 'slk/services/sent_channel_label'
|
|
116
120
|
end
|
|
117
121
|
|
|
118
122
|
# Output formatters for messages, durations, and emoji
|
|
@@ -130,6 +134,7 @@ module Slk
|
|
|
130
134
|
autoload :AttachmentFormatter, 'slk/formatters/attachment_formatter'
|
|
131
135
|
autoload :BlockFormatter, 'slk/formatters/block_formatter'
|
|
132
136
|
autoload :SearchFormatter, 'slk/formatters/search_formatter'
|
|
137
|
+
autoload :SentFormatter, 'slk/formatters/sent_formatter'
|
|
133
138
|
autoload :SavedItemFormatter, 'slk/formatters/saved_item_formatter'
|
|
134
139
|
autoload :TextProcessor, 'slk/formatters/text_processor'
|
|
135
140
|
autoload :ProfileFormatter, 'slk/formatters/profile_formatter'
|
|
@@ -152,6 +157,7 @@ module Slk
|
|
|
152
157
|
autoload :Catchup, 'slk/commands/catchup'
|
|
153
158
|
autoload :Activity, 'slk/commands/activity'
|
|
154
159
|
autoload :Search, 'slk/commands/search'
|
|
160
|
+
autoload :Sent, 'slk/commands/sent'
|
|
155
161
|
autoload :Preset, 'slk/commands/preset'
|
|
156
162
|
autoload :Workspaces, 'slk/commands/workspaces'
|
|
157
163
|
autoload :Cache, 'slk/commands/cache'
|
|
@@ -193,6 +199,7 @@ module Slk
|
|
|
193
199
|
autoload :TextWrapper, 'slk/support/text_wrapper'
|
|
194
200
|
autoload :InteractivePrompt, 'slk/support/interactive_prompt'
|
|
195
201
|
autoload :DateParser, 'slk/support/date_parser'
|
|
202
|
+
autoload :CheckInTime, 'slk/support/check_in_time'
|
|
196
203
|
autoload :TimeParser, 'slk/support/time_parser'
|
|
197
204
|
autoload :TimeRangeParser, 'slk/support/time_range_parser'
|
|
198
205
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eric Boehs
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/slk/commands/presence.rb
|
|
57
57
|
- lib/slk/commands/preset.rb
|
|
58
58
|
- lib/slk/commands/search.rb
|
|
59
|
+
- lib/slk/commands/sent.rb
|
|
59
60
|
- lib/slk/commands/ssh_key_manager.rb
|
|
60
61
|
- lib/slk/commands/status.rb
|
|
61
62
|
- lib/slk/commands/thread.rb
|
|
@@ -82,6 +83,7 @@ files:
|
|
|
82
83
|
- lib/slk/formatters/reaction_formatter.rb
|
|
83
84
|
- lib/slk/formatters/saved_item_formatter.rb
|
|
84
85
|
- lib/slk/formatters/search_formatter.rb
|
|
86
|
+
- lib/slk/formatters/sent_formatter.rb
|
|
85
87
|
- lib/slk/formatters/text_processor.rb
|
|
86
88
|
- lib/slk/models/channel.rb
|
|
87
89
|
- lib/slk/models/deactivation.rb
|
|
@@ -117,6 +119,10 @@ files:
|
|
|
117
119
|
- lib/slk/services/profile_builder.rb
|
|
118
120
|
- lib/slk/services/profile_resolver.rb
|
|
119
121
|
- lib/slk/services/reaction_enricher.rb
|
|
122
|
+
- lib/slk/services/search_pages.rb
|
|
123
|
+
- lib/slk/services/sent_changes.rb
|
|
124
|
+
- lib/slk/services/sent_channel_label.rb
|
|
125
|
+
- lib/slk/services/sent_conversations.rb
|
|
120
126
|
- lib/slk/services/setup_wizard.rb
|
|
121
127
|
- lib/slk/services/start_date_field.rb
|
|
122
128
|
- lib/slk/services/start_date_lookup.rb
|
|
@@ -129,6 +135,7 @@ files:
|
|
|
129
135
|
- lib/slk/services/user_matcher.rb
|
|
130
136
|
- lib/slk/services/user_picker.rb
|
|
131
137
|
- lib/slk/services/who_target_resolver.rb
|
|
138
|
+
- lib/slk/support/check_in_time.rb
|
|
132
139
|
- lib/slk/support/date_parser.rb
|
|
133
140
|
- lib/slk/support/error_logger.rb
|
|
134
141
|
- lib/slk/support/help_formatter.rb
|
|
@@ -164,7 +171,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
164
171
|
- !ruby/object:Gem::Version
|
|
165
172
|
version: '0'
|
|
166
173
|
requirements: []
|
|
167
|
-
rubygems_version: 4.0.
|
|
174
|
+
rubygems_version: 4.0.20
|
|
168
175
|
specification_version: 4
|
|
169
176
|
summary: A command-line interface for Slack
|
|
170
177
|
test_files: []
|