slk 0.1.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 +7 -0
- data/CHANGELOG.md +46 -0
- data/LICENSE +21 -0
- data/README.md +190 -0
- data/bin/slk +7 -0
- data/lib/slack_cli/api/activity.rb +28 -0
- data/lib/slack_cli/api/bots.rb +32 -0
- data/lib/slack_cli/api/client.rb +49 -0
- data/lib/slack_cli/api/conversations.rb +52 -0
- data/lib/slack_cli/api/dnd.rb +40 -0
- data/lib/slack_cli/api/emoji.rb +21 -0
- data/lib/slack_cli/api/threads.rb +44 -0
- data/lib/slack_cli/api/usergroups.rb +25 -0
- data/lib/slack_cli/api/users.rb +101 -0
- data/lib/slack_cli/cli.rb +118 -0
- data/lib/slack_cli/commands/activity.rb +292 -0
- data/lib/slack_cli/commands/base.rb +175 -0
- data/lib/slack_cli/commands/cache.rb +116 -0
- data/lib/slack_cli/commands/catchup.rb +484 -0
- data/lib/slack_cli/commands/config.rb +159 -0
- data/lib/slack_cli/commands/dnd.rb +143 -0
- data/lib/slack_cli/commands/emoji.rb +412 -0
- data/lib/slack_cli/commands/help.rb +76 -0
- data/lib/slack_cli/commands/messages.rb +317 -0
- data/lib/slack_cli/commands/presence.rb +107 -0
- data/lib/slack_cli/commands/preset.rb +239 -0
- data/lib/slack_cli/commands/status.rb +194 -0
- data/lib/slack_cli/commands/thread.rb +62 -0
- data/lib/slack_cli/commands/unread.rb +312 -0
- data/lib/slack_cli/commands/workspaces.rb +151 -0
- data/lib/slack_cli/formatters/duration_formatter.rb +28 -0
- data/lib/slack_cli/formatters/emoji_replacer.rb +143 -0
- data/lib/slack_cli/formatters/mention_replacer.rb +154 -0
- data/lib/slack_cli/formatters/message_formatter.rb +429 -0
- data/lib/slack_cli/formatters/output.rb +89 -0
- data/lib/slack_cli/models/channel.rb +52 -0
- data/lib/slack_cli/models/duration.rb +85 -0
- data/lib/slack_cli/models/message.rb +217 -0
- data/lib/slack_cli/models/preset.rb +73 -0
- data/lib/slack_cli/models/reaction.rb +54 -0
- data/lib/slack_cli/models/status.rb +57 -0
- data/lib/slack_cli/models/user.rb +56 -0
- data/lib/slack_cli/models/workspace.rb +52 -0
- data/lib/slack_cli/runner.rb +123 -0
- data/lib/slack_cli/services/api_client.rb +149 -0
- data/lib/slack_cli/services/cache_store.rb +198 -0
- data/lib/slack_cli/services/configuration.rb +74 -0
- data/lib/slack_cli/services/encryption.rb +51 -0
- data/lib/slack_cli/services/preset_store.rb +112 -0
- data/lib/slack_cli/services/reaction_enricher.rb +87 -0
- data/lib/slack_cli/services/token_store.rb +117 -0
- data/lib/slack_cli/support/error_logger.rb +28 -0
- data/lib/slack_cli/support/help_formatter.rb +139 -0
- data/lib/slack_cli/support/inline_images.rb +62 -0
- data/lib/slack_cli/support/slack_url_parser.rb +78 -0
- data/lib/slack_cli/support/user_resolver.rb +114 -0
- data/lib/slack_cli/support/xdg_paths.rb +37 -0
- data/lib/slack_cli/version.rb +5 -0
- data/lib/slack_cli.rb +91 -0
- metadata +103 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../support/help_formatter"
|
|
4
|
+
|
|
5
|
+
module SlackCli
|
|
6
|
+
module Commands
|
|
7
|
+
class Workspaces < Base
|
|
8
|
+
def execute
|
|
9
|
+
result = validate_options
|
|
10
|
+
return result if result
|
|
11
|
+
|
|
12
|
+
case positional_args
|
|
13
|
+
in ["list"] | []
|
|
14
|
+
list_workspaces
|
|
15
|
+
in ["add"]
|
|
16
|
+
add_workspace
|
|
17
|
+
in ["remove", name]
|
|
18
|
+
remove_workspace(name)
|
|
19
|
+
in ["primary"]
|
|
20
|
+
show_primary
|
|
21
|
+
in ["primary", name]
|
|
22
|
+
set_primary(name)
|
|
23
|
+
else
|
|
24
|
+
error("Unknown action: #{positional_args.first}")
|
|
25
|
+
1
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
protected
|
|
30
|
+
|
|
31
|
+
def help_text
|
|
32
|
+
help = Support::HelpFormatter.new("slk workspaces <action> [name]")
|
|
33
|
+
help.description("Manage Slack workspaces.")
|
|
34
|
+
|
|
35
|
+
help.section("ACTIONS") do |s|
|
|
36
|
+
s.action("list", "List configured workspaces")
|
|
37
|
+
s.action("add", "Add a new workspace (interactive)")
|
|
38
|
+
s.action("remove <name>", "Remove a workspace")
|
|
39
|
+
s.action("primary", "Show primary workspace")
|
|
40
|
+
s.action("primary <name>", "Set primary workspace")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
help.section("OPTIONS") do |s|
|
|
44
|
+
s.option("-q, --quiet", "Suppress output")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
help.render
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def list_workspaces
|
|
53
|
+
names = runner.workspace_names
|
|
54
|
+
primary = config.primary_workspace
|
|
55
|
+
|
|
56
|
+
if names.empty?
|
|
57
|
+
puts "No workspaces configured."
|
|
58
|
+
puts "Run 'slack workspaces add' to add one."
|
|
59
|
+
return 0
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
puts "Workspaces:"
|
|
63
|
+
names.each do |name|
|
|
64
|
+
marker = name == primary ? output.green("*") : " "
|
|
65
|
+
puts " #{marker} #{name}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
0
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_workspace
|
|
72
|
+
print "Workspace name: "
|
|
73
|
+
name = $stdin.gets&.chomp
|
|
74
|
+
return error("Name is required") if name.nil? || name.empty?
|
|
75
|
+
|
|
76
|
+
if token_store.exists?(name)
|
|
77
|
+
return error("Workspace '#{name}' already exists")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
print "Token (xoxb-... or xoxc-...): "
|
|
81
|
+
token = $stdin.gets&.chomp
|
|
82
|
+
return error("Token is required") if token.nil? || token.empty?
|
|
83
|
+
|
|
84
|
+
cookie = nil
|
|
85
|
+
if token.start_with?("xoxc-")
|
|
86
|
+
print "Cookie (d=...): "
|
|
87
|
+
cookie = $stdin.gets&.chomp
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
token_store.add(name, token, cookie)
|
|
91
|
+
|
|
92
|
+
# Set as primary if first workspace
|
|
93
|
+
if runner.workspace_names.size == 1
|
|
94
|
+
config.primary_workspace = name
|
|
95
|
+
success("Added workspace '#{name}' (set as primary)")
|
|
96
|
+
else
|
|
97
|
+
success("Added workspace '#{name}'")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def remove_workspace(name)
|
|
104
|
+
unless token_store.exists?(name)
|
|
105
|
+
return error("Workspace '#{name}' not found")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
token_store.remove(name)
|
|
109
|
+
|
|
110
|
+
# Clear primary if removing it
|
|
111
|
+
if config.primary_workspace == name
|
|
112
|
+
remaining = runner.workspace_names
|
|
113
|
+
if remaining.any?
|
|
114
|
+
config.primary_workspace = remaining.first
|
|
115
|
+
success("Removed workspace '#{name}'. Primary changed to '#{remaining.first}'")
|
|
116
|
+
else
|
|
117
|
+
config.primary_workspace = nil
|
|
118
|
+
success("Removed workspace '#{name}'")
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
success("Removed workspace '#{name}'")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
0
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def show_primary
|
|
128
|
+
primary = config.primary_workspace
|
|
129
|
+
|
|
130
|
+
if primary
|
|
131
|
+
puts "Primary workspace: #{primary}"
|
|
132
|
+
else
|
|
133
|
+
puts "No primary workspace set."
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def set_primary(name)
|
|
140
|
+
unless token_store.exists?(name)
|
|
141
|
+
return error("Workspace '#{name}' not found")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
config.primary_workspace = name
|
|
145
|
+
success("Primary workspace set to '#{name}'")
|
|
146
|
+
|
|
147
|
+
0
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
module Formatters
|
|
5
|
+
class DurationFormatter
|
|
6
|
+
def format(duration)
|
|
7
|
+
return "" if duration.nil? || duration.zero?
|
|
8
|
+
|
|
9
|
+
duration.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def format_remaining(seconds)
|
|
13
|
+
return "" if seconds.nil? || seconds <= 0
|
|
14
|
+
|
|
15
|
+
Models::Duration.new(seconds: seconds).to_s
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def format_until(timestamp)
|
|
19
|
+
return "" if timestamp.nil? || timestamp <= 0
|
|
20
|
+
|
|
21
|
+
remaining = timestamp - Time.now.to_i
|
|
22
|
+
return "expired" if remaining <= 0
|
|
23
|
+
|
|
24
|
+
format_remaining(remaining)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
module Formatters
|
|
5
|
+
class EmojiReplacer
|
|
6
|
+
EMOJI_REGEX = /:([a-zA-Z0-9_+-]+):/
|
|
7
|
+
SKIN_TONE_REGEX = /::skin-tone-(\d)/
|
|
8
|
+
|
|
9
|
+
# Common emoji mappings (subset - full list would be much larger)
|
|
10
|
+
EMOJI_MAP = {
|
|
11
|
+
# Faces
|
|
12
|
+
"smile" => "\u{1F604}", "grinning" => "\u{1F600}", "joy" => "\u{1F602}",
|
|
13
|
+
"rofl" => "\u{1F923}", "smiley" => "\u{1F603}", "sweat_smile" => "\u{1F605}",
|
|
14
|
+
"laughing" => "\u{1F606}", "wink" => "\u{1F609}", "blush" => "\u{1F60A}",
|
|
15
|
+
"yum" => "\u{1F60B}", "sunglasses" => "\u{1F60E}", "heart_eyes" => "\u{1F60D}",
|
|
16
|
+
"kissing_heart" => "\u{1F618}", "thinking" => "\u{1F914}", "thinking_face" => "\u{1F914}",
|
|
17
|
+
"raised_eyebrow" => "\u{1F928}", "neutral_face" => "\u{1F610}", "expressionless" => "\u{1F611}",
|
|
18
|
+
"unamused" => "\u{1F612}", "rolling_eyes" => "\u{1F644}", "grimacing" => "\u{1F62C}",
|
|
19
|
+
"relieved" => "\u{1F60C}", "pensive" => "\u{1F614}", "sleepy" => "\u{1F62A}",
|
|
20
|
+
"sleeping" => "\u{1F634}", "sob" => "\u{1F62D}", "cry" => "\u{1F622}",
|
|
21
|
+
"scream" => "\u{1F631}", "angry" => "\u{1F620}", "rage" => "\u{1F621}",
|
|
22
|
+
|
|
23
|
+
# Gestures
|
|
24
|
+
"wave" => "\u{1F44B}", "+1" => "\u{1F44D}", "-1" => "\u{1F44E}",
|
|
25
|
+
"thumbsup" => "\u{1F44D}", "thumbsdown" => "\u{1F44E}",
|
|
26
|
+
"clap" => "\u{1F44F}", "raised_hands" => "\u{1F64C}", "pray" => "\u{1F64F}",
|
|
27
|
+
"point_up" => "\u{261D}", "point_down" => "\u{1F447}", "point_left" => "\u{1F448}",
|
|
28
|
+
"point_right" => "\u{1F449}", "ok_hand" => "\u{1F44C}", "v" => "\u{270C}",
|
|
29
|
+
"muscle" => "\u{1F4AA}", "fist" => "\u{270A}",
|
|
30
|
+
|
|
31
|
+
# Hearts
|
|
32
|
+
"heart" => "\u{2764}", "hearts" => "\u{2665}", "yellow_heart" => "\u{1F49B}",
|
|
33
|
+
"green_heart" => "\u{1F49A}", "blue_heart" => "\u{1F499}", "purple_heart" => "\u{1F49C}",
|
|
34
|
+
"black_heart" => "\u{1F5A4}", "broken_heart" => "\u{1F494}", "sparkling_heart" => "\u{1F496}",
|
|
35
|
+
|
|
36
|
+
# Objects
|
|
37
|
+
"fire" => "\u{1F525}", "star" => "\u{2B50}", "sparkles" => "\u{2728}",
|
|
38
|
+
"boom" => "\u{1F4A5}", "zap" => "\u{26A1}", "sunny" => "\u{2600}",
|
|
39
|
+
"cloud" => "\u{2601}", "umbrella" => "\u{2614}", "snowflake" => "\u{2744}",
|
|
40
|
+
"rocket" => "\u{1F680}", "airplane" => "\u{2708}", "car" => "\u{1F697}",
|
|
41
|
+
"gift" => "\u{1F381}", "trophy" => "\u{1F3C6}", "medal" => "\u{1F3C5}",
|
|
42
|
+
"bell" => "\u{1F514}", "key" => "\u{1F511}", "lock" => "\u{1F512}",
|
|
43
|
+
"bulb" => "\u{1F4A1}", "book" => "\u{1F4D6}", "pencil" => "\u{270F}",
|
|
44
|
+
"memo" => "\u{1F4DD}", "computer" => "\u{1F4BB}", "phone" => "\u{1F4F1}",
|
|
45
|
+
"camera" => "\u{1F4F7}", "headphones" => "\u{1F3A7}", "microphone" => "\u{1F3A4}",
|
|
46
|
+
|
|
47
|
+
# Food
|
|
48
|
+
"coffee" => "\u{2615}", "tea" => "\u{1F375}", "beer" => "\u{1F37A}",
|
|
49
|
+
"wine_glass" => "\u{1F377}", "pizza" => "\u{1F355}", "hamburger" => "\u{1F354}",
|
|
50
|
+
"cake" => "\u{1F370}", "cookie" => "\u{1F36A}", "apple" => "\u{1F34E}",
|
|
51
|
+
"banana" => "\u{1F34C}", "taco" => "\u{1F32E}", "burrito" => "\u{1F32F}",
|
|
52
|
+
"knife_fork_plate" => "\u{1F37D}",
|
|
53
|
+
|
|
54
|
+
# Nature
|
|
55
|
+
"dog" => "\u{1F436}", "cat" => "\u{1F431}", "mouse" => "\u{1F42D}",
|
|
56
|
+
"rabbit" => "\u{1F430}", "bear" => "\u{1F43B}", "panda_face" => "\u{1F43C}",
|
|
57
|
+
"chicken" => "\u{1F414}", "penguin" => "\u{1F427}", "bird" => "\u{1F426}",
|
|
58
|
+
"fish" => "\u{1F41F}", "bug" => "\u{1F41B}", "bee" => "\u{1F41D}",
|
|
59
|
+
"rose" => "\u{1F339}", "sunflower" => "\u{1F33B}", "tree" => "\u{1F333}",
|
|
60
|
+
"cactus" => "\u{1F335}", "palm_tree" => "\u{1F334}",
|
|
61
|
+
|
|
62
|
+
# Symbols
|
|
63
|
+
"white_check_mark" => "\u{2705}", "heavy_check_mark" => "\u{2714}",
|
|
64
|
+
"x" => "\u{274C}", "heavy_multiplication_x" => "\u{2716}",
|
|
65
|
+
"warning" => "\u{26A0}", "no_entry" => "\u{26D4}", "sos" => "\u{1F198}",
|
|
66
|
+
"question" => "\u{2753}", "exclamation" => "\u{2757}", "bangbang" => "\u{203C}",
|
|
67
|
+
"100" => "\u{1F4AF}", "1234" => "\u{1F522}",
|
|
68
|
+
|
|
69
|
+
# Status-related
|
|
70
|
+
"house" => "\u{1F3E0}", "office" => "\u{1F3E2}", "hospital" => "\u{1F3E5}",
|
|
71
|
+
"calendar" => "\u{1F4C5}", "date" => "\u{1F4C5}", "spiral_calendar" => "\u{1F5D3}",
|
|
72
|
+
"clock1" => "\u{1F550}", "hourglass" => "\u{231B}", "stopwatch" => "\u{23F1}",
|
|
73
|
+
"zzz" => "\u{1F4A4}", "speech_balloon" => "\u{1F4AC}", "thought_balloon" => "\u{1F4AD}",
|
|
74
|
+
|
|
75
|
+
# Common Slack custom
|
|
76
|
+
"party-blob" => "\u{1F389}", "blob-wave" => "\u{1F44B}",
|
|
77
|
+
"tada" => "\u{1F389}", "confetti_ball" => "\u{1F38A}",
|
|
78
|
+
"balloon" => "\u{1F388}", "party_popper" => "\u{1F389}",
|
|
79
|
+
"eyes" => "\u{1F440}", "eye" => "\u{1F441}",
|
|
80
|
+
"ear" => "\u{1F442}", "nose" => "\u{1F443}",
|
|
81
|
+
"brb" => "\u{1F6B6}", "away" => "\u{1F6B6}",
|
|
82
|
+
"test_tube" => "\u{1F9EA}"
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
def initialize(custom_emoji: {}, on_debug: nil)
|
|
86
|
+
@custom_emoji = custom_emoji
|
|
87
|
+
@on_debug = on_debug
|
|
88
|
+
@gemoji_cache = load_gemoji_cache
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def replace(text, workspace = nil)
|
|
92
|
+
result = text.dup
|
|
93
|
+
|
|
94
|
+
# Remove skin tone modifiers (we don't render them in terminal)
|
|
95
|
+
result.gsub!(SKIN_TONE_REGEX, "")
|
|
96
|
+
|
|
97
|
+
# Replace emoji codes
|
|
98
|
+
result.gsub!(EMOJI_REGEX) do
|
|
99
|
+
name = ::Regexp.last_match(1)
|
|
100
|
+
lookup_emoji(name) || ":#{name}:"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def lookup_emoji(name)
|
|
107
|
+
# Check custom emoji first
|
|
108
|
+
return nil if @custom_emoji[name] # Custom emoji are URLs, skip for now
|
|
109
|
+
|
|
110
|
+
# Check gemoji cache first (from sync-standard)
|
|
111
|
+
if @gemoji_cache&.key?(name)
|
|
112
|
+
return @gemoji_cache[name]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Fall back to built-in map
|
|
116
|
+
EMOJI_MAP[name]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def with_custom_emoji(emoji_hash)
|
|
120
|
+
self.class.new(custom_emoji: emoji_hash, on_debug: @on_debug)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def load_gemoji_cache
|
|
126
|
+
cache_path = File.join(
|
|
127
|
+
ENV.fetch("XDG_CACHE_HOME", File.expand_path("~/.cache")),
|
|
128
|
+
"slack-cli",
|
|
129
|
+
"gemoji.json"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return nil unless File.exist?(cache_path)
|
|
133
|
+
|
|
134
|
+
JSON.parse(File.read(cache_path))
|
|
135
|
+
rescue JSON::ParserError => e
|
|
136
|
+
@on_debug&.call("Failed to load gemoji cache: #{e.message}")
|
|
137
|
+
nil
|
|
138
|
+
rescue Errno::ENOENT
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SlackCli
|
|
4
|
+
module Formatters
|
|
5
|
+
class MentionReplacer
|
|
6
|
+
USER_MENTION_REGEX = /<@([UW][A-Z0-9]+)(?:\|([^>]+))?>/
|
|
7
|
+
CHANNEL_MENTION_REGEX = /<#([A-Z0-9]+)(?:\|([^>]*))?>/
|
|
8
|
+
SUBTEAM_MENTION_REGEX = /<!subteam\^([A-Z0-9]+)(?:\|@?([^>]+))?>/
|
|
9
|
+
LINK_REGEX = /<(https?:\/\/[^|>]+)(?:\|([^>]+))?>/
|
|
10
|
+
SPECIAL_MENTIONS = {
|
|
11
|
+
"<!here>" => "@here",
|
|
12
|
+
"<!channel>" => "@channel",
|
|
13
|
+
"<!everyone>" => "@everyone"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(cache_store:, api_client: nil, on_debug: nil)
|
|
17
|
+
@cache = cache_store
|
|
18
|
+
@api = api_client
|
|
19
|
+
@on_debug = on_debug
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def replace(text, workspace)
|
|
23
|
+
result = text.dup
|
|
24
|
+
|
|
25
|
+
# Replace user mentions
|
|
26
|
+
result.gsub!(USER_MENTION_REGEX) do
|
|
27
|
+
user_id = ::Regexp.last_match(1)
|
|
28
|
+
display_name = ::Regexp.last_match(2)
|
|
29
|
+
|
|
30
|
+
if display_name && !display_name.empty?
|
|
31
|
+
"@#{display_name}"
|
|
32
|
+
else
|
|
33
|
+
name = lookup_user_name(workspace, user_id)
|
|
34
|
+
"@#{name || user_id}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Replace channel mentions
|
|
39
|
+
result.gsub!(CHANNEL_MENTION_REGEX) do
|
|
40
|
+
channel_id = ::Regexp.last_match(1)
|
|
41
|
+
channel_name = ::Regexp.last_match(2)
|
|
42
|
+
|
|
43
|
+
if channel_name && !channel_name.empty?
|
|
44
|
+
"##{channel_name}"
|
|
45
|
+
else
|
|
46
|
+
name = lookup_channel_name(workspace, channel_id)
|
|
47
|
+
name ? "##{name}" : "##{channel_id}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Replace subteam (user group) mentions
|
|
52
|
+
result.gsub!(SUBTEAM_MENTION_REGEX) do
|
|
53
|
+
subteam_id = ::Regexp.last_match(1)
|
|
54
|
+
handle = ::Regexp.last_match(2)
|
|
55
|
+
|
|
56
|
+
if handle && !handle.empty?
|
|
57
|
+
"@#{handle}"
|
|
58
|
+
else
|
|
59
|
+
name = lookup_subteam_handle(workspace, subteam_id)
|
|
60
|
+
"@#{name || subteam_id}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Replace links
|
|
65
|
+
result.gsub!(LINK_REGEX) do
|
|
66
|
+
url = ::Regexp.last_match(1)
|
|
67
|
+
label = ::Regexp.last_match(2)
|
|
68
|
+
label || url
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Replace special mentions
|
|
72
|
+
SPECIAL_MENTIONS.each do |pattern, replacement|
|
|
73
|
+
result.gsub!(pattern, replacement)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def lookup_user_name(workspace, user_id)
|
|
82
|
+
# Try cache first
|
|
83
|
+
cached = @cache.get_user(workspace.name, user_id)
|
|
84
|
+
return cached if cached
|
|
85
|
+
|
|
86
|
+
# Try API lookup
|
|
87
|
+
return nil unless @api
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
users_api = Api::Users.new(@api, workspace)
|
|
91
|
+
response = users_api.info(user_id)
|
|
92
|
+
if response["ok"] && response["user"]
|
|
93
|
+
profile = response["user"]["profile"] || {}
|
|
94
|
+
name = profile["display_name"]
|
|
95
|
+
name = profile["real_name"] if name.to_s.empty?
|
|
96
|
+
name = response["user"]["name"] if name.to_s.empty?
|
|
97
|
+
# Cache for future lookups
|
|
98
|
+
@cache.set_user(workspace.name, user_id, name, persist: true) if name && !name.empty?
|
|
99
|
+
return name unless name.to_s.empty?
|
|
100
|
+
end
|
|
101
|
+
rescue ApiError => e
|
|
102
|
+
@on_debug&.call("User lookup failed for #{user_id}: #{e.message}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def lookup_channel_name(workspace, channel_id)
|
|
109
|
+
# Try cache first
|
|
110
|
+
cached = @cache.get_channel_name(workspace.name, channel_id)
|
|
111
|
+
return cached if cached
|
|
112
|
+
|
|
113
|
+
# Try API lookup
|
|
114
|
+
return nil unless @api
|
|
115
|
+
|
|
116
|
+
begin
|
|
117
|
+
conversations_api = Api::Conversations.new(@api, workspace)
|
|
118
|
+
response = conversations_api.info(channel: channel_id)
|
|
119
|
+
if response["ok"] && response["channel"]
|
|
120
|
+
name = response["channel"]["name"]
|
|
121
|
+
# Cache for future lookups
|
|
122
|
+
@cache.set_channel(workspace.name, name, channel_id) if name
|
|
123
|
+
return name
|
|
124
|
+
end
|
|
125
|
+
rescue ApiError => e
|
|
126
|
+
@on_debug&.call("Channel lookup failed for #{channel_id}: #{e.message}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def lookup_subteam_handle(workspace, subteam_id)
|
|
133
|
+
# Try cache first
|
|
134
|
+
cached = @cache.get_subteam(workspace.name, subteam_id)
|
|
135
|
+
return cached if cached
|
|
136
|
+
|
|
137
|
+
# Try API lookup
|
|
138
|
+
return nil unless @api
|
|
139
|
+
|
|
140
|
+
begin
|
|
141
|
+
usergroups_api = Api::Usergroups.new(@api, workspace)
|
|
142
|
+
handle = usergroups_api.get_handle(subteam_id)
|
|
143
|
+
# Cache for future lookups
|
|
144
|
+
@cache.set_subteam(workspace.name, subteam_id, handle) if handle
|
|
145
|
+
return handle
|
|
146
|
+
rescue ApiError => e
|
|
147
|
+
@on_debug&.call("Subteam lookup failed for #{subteam_id}: #{e.message}")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|