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
@@ -1,292 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../support/help_formatter'
4
-
5
- module SlackCli
6
- module Commands
7
- class Activity < Base
8
- def execute
9
- result = validate_options
10
- return result if result
11
-
12
- workspace = target_workspaces.first
13
- api = runner.activity_api(workspace.name)
14
-
15
- response = api.feed(limit: @options[:limit], types: activity_types)
16
-
17
- unless response['ok']
18
- error("Failed to fetch activity: #{response['error']}")
19
- return 1
20
- end
21
-
22
- items = response['items'] || []
23
-
24
- if @options[:json]
25
- output_json(items)
26
- else
27
- display_activity(items, workspace)
28
- end
29
-
30
- 0
31
- rescue ApiError => e
32
- error("Failed to fetch activity: #{e.message}")
33
- 1
34
- end
35
-
36
- protected
37
-
38
- def default_options
39
- super.merge(
40
- limit: 20,
41
- filter: :all,
42
- show_messages: false
43
- )
44
- end
45
-
46
- def handle_option(arg, args, remaining)
47
- case arg
48
- when '-n', '--limit'
49
- @options[:limit] = args.shift.to_i
50
- when '--reactions'
51
- @options[:filter] = :reactions
52
- when '--mentions'
53
- @options[:filter] = :mentions
54
- when '--threads'
55
- @options[:filter] = :threads
56
- when '--show-messages', '-m'
57
- @options[:show_messages] = true
58
- else
59
- super
60
- end
61
- end
62
-
63
- def help_text
64
- help = Support::HelpFormatter.new('slk activity [options]')
65
- help.description('Show recent activity from the activity feed.')
66
-
67
- help.section('OPTIONS') do |s|
68
- s.option('-n, --limit N', 'Number of items (default: 20, max: 50)')
69
- s.option('--reactions', 'Show only reaction activity')
70
- s.option('--mentions', 'Show only mentions')
71
- s.option('--threads', 'Show only thread replies')
72
- s.option('-m, --show-messages', 'Show the message content for each activity')
73
- s.option('--json', 'Output as JSON')
74
- s.option('-w, --workspace', 'Specify workspace')
75
- s.option('-v, --verbose', 'Show debug information')
76
- s.option('-q, --quiet', 'Suppress output')
77
- end
78
-
79
- help.render
80
- end
81
-
82
- private
83
-
84
- def activity_types
85
- case @options[:filter]
86
- when :reactions
87
- 'message_reaction'
88
- when :mentions
89
- 'at_user,at_user_group,at_channel,at_everyone'
90
- when :threads
91
- 'thread_v2'
92
- else
93
- # All activity types that the Slack web UI uses
94
- 'thread_v2,message_reaction,bot_dm_bundle,at_user,at_user_group,at_channel,at_everyone'
95
- end
96
- end
97
-
98
- def display_activity(items, workspace)
99
- return puts 'No activity found.' if items.empty?
100
-
101
- items.each do |item|
102
- display_activity_item(item, workspace)
103
- end
104
- end
105
-
106
- def display_activity_item(item, workspace)
107
- type = item.dig('item', 'type')
108
- timestamp = format_activity_time(item['feed_ts'])
109
-
110
- case type
111
- when 'message_reaction'
112
- display_reaction_activity(item, workspace, timestamp)
113
- when 'at_user', 'at_user_group', 'at_channel', 'at_everyone'
114
- display_mention_activity(item, workspace, timestamp)
115
- when 'thread_v2'
116
- display_thread_v2_activity(item, workspace, timestamp)
117
- when 'bot_dm_bundle'
118
- display_bot_dm_activity(item, workspace, timestamp)
119
- else
120
- # Unknown activity type - skip silently
121
- end
122
- end
123
-
124
- def display_reaction_activity(item, workspace, timestamp)
125
- reaction_data = item.dig('item', 'reaction')
126
- message_data = item.dig('item', 'message')
127
- return unless reaction_data && message_data
128
-
129
- user_id = reaction_data['user']
130
- username = resolve_user(workspace, user_id)
131
- emoji_name = reaction_data['name']
132
- emoji = runner.emoji_replacer.lookup_emoji(emoji_name) || ":#{emoji_name}:"
133
- channel_id = message_data['channel']
134
- channel = resolve_channel(workspace, channel_id)
135
-
136
- puts "#{output.blue(timestamp)} #{output.bold(username)} reacted #{emoji} in #{channel}"
137
-
138
- if @options[:show_messages]
139
- message = fetch_message(workspace, channel_id, message_data['ts'])
140
- display_message_preview(message, workspace)
141
- end
142
- end
143
-
144
- def display_mention_activity(item, workspace, timestamp)
145
- message_data = item.dig('item', 'message')
146
- return unless message_data
147
-
148
- user_id = message_data['author_user_id'] || message_data['user']
149
- username = resolve_user(workspace, user_id)
150
- channel_id = message_data['channel']
151
- channel = resolve_channel(workspace, channel_id)
152
-
153
- puts "#{output.blue(timestamp)} #{output.bold(username)} mentioned you in #{channel}"
154
-
155
- if @options[:show_messages]
156
- message = fetch_message(workspace, channel_id, message_data['ts'])
157
- display_message_preview(message, workspace)
158
- end
159
- end
160
-
161
- def display_thread_v2_activity(item, workspace, timestamp)
162
- thread_entry = item.dig('item', 'bundle_info', 'payload', 'thread_entry')
163
- return unless thread_entry
164
-
165
- channel_id = thread_entry['channel_id']
166
- channel = resolve_channel(workspace, channel_id)
167
-
168
- puts "#{output.blue(timestamp)} Thread activity in #{channel}"
169
-
170
- if @options[:show_messages] && thread_entry['thread_ts']
171
- # Fetch the thread parent message
172
- message = fetch_message(workspace, channel_id, thread_entry['thread_ts'])
173
- display_message_preview(message, workspace)
174
- end
175
- end
176
-
177
- def display_bot_dm_activity(item, workspace, timestamp)
178
- message_data = item.dig('item', 'bundle_info', 'payload', 'message')
179
- return unless message_data
180
-
181
- channel_id = message_data['channel']
182
- message_ts = message_data['ts']
183
- channel = resolve_channel(workspace, channel_id)
184
-
185
- puts "#{output.blue(timestamp)} Bot message in #{channel}"
186
-
187
- # Always try to fetch and show the message content (or when --show-messages is enabled)
188
- if @options[:show_messages]
189
- message = fetch_message(workspace, channel_id, message_ts)
190
- display_message_preview(message, workspace) if message
191
- end
192
- end
193
-
194
- def resolve_user(workspace, user_id)
195
- # Try cache first
196
- cached = cache_store.get_user(workspace.name, user_id)
197
- return cached if cached
198
-
199
- # Fall back to user ID
200
- user_id
201
- end
202
-
203
- def resolve_channel(workspace, channel_id)
204
- # DMs and Group DMs - don't try to resolve
205
- return 'DM' if channel_id.start_with?('D')
206
- return 'Group DM' if channel_id.start_with?('G')
207
-
208
- # Try cache first
209
- cached = cache_store.get_channel_name(workspace.name, channel_id)
210
- return "##{cached}" if cached
211
-
212
- # Try to fetch from API
213
- begin
214
- api = runner.conversations_api(workspace.name)
215
- response = api.info(channel: channel_id)
216
- if response['ok'] && response['channel']
217
- name = response['channel']['name']
218
- cache_store.set_channel(workspace.name, name, channel_id)
219
- return "##{name}"
220
- end
221
- rescue ApiError
222
- # Fall back to channel ID if API call fails
223
- end
224
-
225
- # Fall back to channel ID
226
- channel_id
227
- end
228
-
229
- def fetch_message(workspace, channel_id, message_ts)
230
- api = runner.conversations_api(workspace.name)
231
- # Fetch a window of messages around the target timestamp
232
- # Use oldest (exclusive) and latest (inclusive) to create a window
233
- oldest_ts = (message_ts.to_f - 1).to_s # 1 second before
234
- latest_ts = (message_ts.to_f + 1).to_s # 1 second after
235
-
236
- response = api.history(
237
- channel: channel_id,
238
- limit: 10,
239
- oldest: oldest_ts,
240
- latest: latest_ts
241
- )
242
-
243
- return nil unless response['ok'] && response['messages']&.any?
244
-
245
- # Find the exact message by timestamp
246
- response['messages'].find { |msg| msg['ts'] == message_ts }
247
- rescue ApiError
248
- nil
249
- end
250
-
251
- def display_message_preview(message, workspace)
252
- return unless message
253
-
254
- # Get username
255
- username = if message['user']
256
- resolve_user(workspace, message['user'])
257
- elsif message['bot_id']
258
- 'Bot'
259
- else
260
- 'Unknown'
261
- end
262
-
263
- # Get text and replace mentions
264
- text = message['text'] || ''
265
- text = '[No text]' if text.empty?
266
- text = runner.mention_replacer.replace(text, workspace) unless text == '[No text]'
267
-
268
- # Format as indented preview
269
- lines = text.lines
270
- first_line = lines.first&.strip || text
271
- first_line = "#{first_line[0..100]}..." if first_line.length > 100
272
-
273
- puts " └─ #{username}: #{first_line}"
274
-
275
- # Show additional lines if any
276
- if lines.length > 1
277
- remaining = lines[1..2].map(&:strip).reject(&:empty?)
278
- remaining.each do |line|
279
- line = "#{line[0..100]}..." if line.length > 100
280
- puts " #{line}"
281
- end
282
- puts " [#{lines.length - 3} more lines...]" if lines.length > 3
283
- end
284
- end
285
-
286
- def format_activity_time(slack_timestamp)
287
- time = Time.at(slack_timestamp.to_f)
288
- time.strftime('%b %d %-I:%M %p') # e.g., "Jan 13 2:45 PM"
289
- end
290
- end
291
- end
292
- end
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../support/help_formatter"
4
-
5
- module SlackCli
6
- module Commands
7
- class Cache < Base
8
- def execute
9
- result = validate_options
10
- return result if result
11
-
12
- case positional_args
13
- in ["status" | "info"] | []
14
- show_status
15
- in ["clear", *rest]
16
- clear_cache(rest.first)
17
- in ["populate" | "refresh", *rest]
18
- populate_cache(rest.first)
19
- else
20
- error("Unknown action: #{positional_args.first}")
21
- 1
22
- end
23
- rescue ApiError => e
24
- error("Failed: #{e.message}")
25
- 1
26
- end
27
-
28
- protected
29
-
30
- def help_text
31
- help = Support::HelpFormatter.new("slk cache <action> [workspace]")
32
- help.description("Manage user and channel cache.")
33
-
34
- help.section("ACTIONS") do |s|
35
- s.action("status", "Show cache status")
36
- s.action("clear [ws]", "Clear cache (all or specific workspace)")
37
- s.action("populate [ws]", "Populate user cache from API")
38
- end
39
-
40
- help.section("OPTIONS") do |s|
41
- s.option("-w, --workspace", "Specify workspace")
42
- s.option("-q, --quiet", "Suppress output")
43
- end
44
-
45
- help.render
46
- end
47
-
48
- private
49
-
50
- def show_status
51
- target_workspaces.each do |workspace|
52
- if target_workspaces.size > 1
53
- puts output.bold(workspace.name)
54
- end
55
-
56
- user_count = cache_store.user_cache_size(workspace.name)
57
- channel_count = cache_store.channel_cache_size(workspace.name)
58
-
59
- puts " Users cached: #{user_count}"
60
- puts " Channels cached: #{channel_count}"
61
-
62
- if cache_store.user_cache_file_exists?(workspace.name)
63
- puts " User cache: #{output.green("present")}"
64
- else
65
- puts " User cache: #{output.yellow("not populated")}"
66
- end
67
- end
68
-
69
- 0
70
- end
71
-
72
- def clear_cache(workspace_name)
73
- if workspace_name
74
- cache_store.clear_user_cache(workspace_name)
75
- cache_store.clear_channel_cache(workspace_name)
76
- success("Cleared cache for #{workspace_name}")
77
- else
78
- cache_store.clear_user_cache
79
- cache_store.clear_channel_cache
80
- success("Cleared all caches")
81
- end
82
-
83
- 0
84
- end
85
-
86
- def populate_cache(workspace_name)
87
- workspaces = workspace_name ? [runner.workspace(workspace_name)] : target_workspaces
88
-
89
- workspaces.each do |workspace|
90
- puts "Populating user cache for #{workspace.name}..."
91
-
92
- api = runner.users_api(workspace.name)
93
- all_users = []
94
- cursor = nil
95
-
96
- loop do
97
- response = api.list(cursor: cursor)
98
- members = response["members"] || []
99
- all_users.concat(members.map { |m| Models::User.from_api(m) })
100
-
101
- cursor = response.dig("response_metadata", "next_cursor")
102
- break if cursor.nil? || cursor.empty?
103
-
104
- print "."
105
- end
106
-
107
- count = cache_store.populate_user_cache(workspace.name, all_users)
108
- puts
109
- success("Cached #{count} users for #{workspace.name}")
110
- end
111
-
112
- 0
113
- end
114
- end
115
- end
116
- end