Skip to content

Commit 9dc65fd

Browse files
committed
fix(telegram): wrap destinations in <code> for approvals and mutations
Extends the /policy show fix to the other Telegram surfaces that render destinations or URLs: approval prompts and policy mutation confirmations (allow/deny). Without this, Telegram auto-links the destination in strings like 'HTTPS api.github.com:443' or 'Added allow rule: example.com' as a clickable blue URL. - bot.go FormatApprovalMessage: wrap dest:port and the rendered request URL in <code>. MCP tool name also in <code>. - commands.go policyAllow / policyDeny: wrap dest in <code> on both store and in-memory paths. - Consolidate the duplicated htmlEscape helpers: commands.go now uses the existing htmlEscape from bot.go, local wrapper removed. - Update bot_test.go expectations to match the <code>-wrapped output.
1 parent 6b3946f commit 9dc65fd

3 files changed

Lines changed: 20 additions & 29 deletions

File tree

internal/telegram/bot.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ var protoDisplayName = map[string]string{
5757
// Callers must set ParseMode to HTML when sending the message.
5858
func FormatApprovalMessage(req channel.ApprovalRequest) string {
5959
if req.Protocol == "mcp" {
60-
msg := "OpenClaw wants to call tool:\n\n" + htmlEscape(req.Destination)
60+
msg := "OpenClaw wants to call tool:\n\n" + htmlCode(req.Destination)
6161
if req.ToolArgs != "" {
6262
pretty := prettyJSONOrRaw(req.ToolArgs)
6363
msg += "\n\nArguments:\n<pre><code class=\"language-json\">" + htmlEscape(pretty) + "</code></pre>"
@@ -69,20 +69,21 @@ func FormatApprovalMessage(req channel.ApprovalRequest) string {
6969
if display == "" {
7070
display = req.Protocol
7171
}
72+
destPort := fmt.Sprintf("%s:%d", req.Destination, req.Port)
7273
if req.Method != "" {
7374
ver := ""
7475
if req.HTTPVersion != "" {
7576
ver = " (" + htmlEscape(req.HTTPVersion) + ")"
7677
}
7778
return fmt.Sprintf(
78-
"OpenClaw wants to connect to:\n\n%s %s:%d\n%s %s%s\n\nAllow this request?",
79-
htmlEscape(display), htmlEscape(req.Destination), req.Port,
80-
htmlEscape(req.Method), htmlEscape(buildRequestURL(req)), ver,
79+
"OpenClaw wants to connect to:\n\n%s %s\n%s %s%s\n\nAllow this request?",
80+
htmlEscape(display), htmlCode(destPort),
81+
htmlEscape(req.Method), htmlCode(buildRequestURL(req)), ver,
8182
)
8283
}
8384
return fmt.Sprintf(
84-
"OpenClaw wants to connect to:\n\n%s %s:%d\n\nAllow this connection?",
85-
htmlEscape(display), htmlEscape(req.Destination), req.Port,
85+
"OpenClaw wants to connect to:\n\n%s %s\n\nAllow this connection?",
86+
htmlEscape(display), htmlCode(destPort),
8687
)
8788
}
8889

internal/telegram/bot_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ func TestFormatApprovalMessage(t *testing.T) {
7777
Protocol: "http",
7878
}
7979
msg := FormatApprovalMessage(req)
80-
if !strings.Contains(msg, "HTTP example.com:8080") {
81-
t.Errorf("expected 'HTTP example.com:8080' in message, got: %s", msg)
80+
if !strings.Contains(msg, "HTTP <code>example.com:8080</code>") {
81+
t.Errorf("expected 'HTTP <code>example.com:8080</code>' in message, got: %s", msg)
8282
}
8383
})
8484

@@ -167,10 +167,10 @@ func TestFormatApprovalMessage(t *testing.T) {
167167
Path: "/users/me",
168168
}
169169
msg := FormatApprovalMessage(req)
170-
if !strings.Contains(msg, "HTTPS api.example.com:443") {
170+
if !strings.Contains(msg, "HTTPS <code>api.example.com:443</code>") {
171171
t.Errorf("expected destination line in message, got: %s", msg)
172172
}
173-
if !strings.Contains(msg, "GET https://api.example.com/users/me") {
173+
if !strings.Contains(msg, "GET <code>https://api.example.com/users/me</code>") {
174174
t.Errorf("expected request line in message, got: %s", msg)
175175
}
176176
if !strings.Contains(msg, "Allow this request?") {
@@ -187,7 +187,7 @@ func TestFormatApprovalMessage(t *testing.T) {
187187
Path: "/api/submit",
188188
}
189189
msg := FormatApprovalMessage(req)
190-
if !strings.Contains(msg, "POST http://localhost:8080/api/submit") {
190+
if !strings.Contains(msg, "POST <code>http://localhost:8080/api/submit</code>") {
191191
t.Errorf("expected URL with explicit port, got: %s", msg)
192192
}
193193
})
@@ -201,7 +201,7 @@ func TestFormatApprovalMessage(t *testing.T) {
201201
Path: "",
202202
}
203203
msg := FormatApprovalMessage(req)
204-
if !strings.Contains(msg, "HEAD https://example.com/") {
204+
if !strings.Contains(msg, "HEAD <code>https://example.com/</code>") {
205205
t.Errorf("expected URL with default path '/', got: %s", msg)
206206
}
207207
})

internal/telegram/commands.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -253,21 +253,11 @@ func (h *CommandHandler) policyShow() string {
253253
return b.String()
254254
}
255255

256-
// htmlEscapeText escapes the three characters that Telegram's HTML parse mode
257-
// requires in text nodes: &, <, >. Quotes and apostrophes are left alone
258-
// since we don't use them in tag attributes.
259-
func htmlEscapeText(s string) string {
260-
s = strings.ReplaceAll(s, "&", "&amp;")
261-
s = strings.ReplaceAll(s, "<", "&lt;")
262-
s = strings.ReplaceAll(s, ">", "&gt;")
263-
return s
264-
}
265-
266256
// htmlCode wraps s in <code>...</code> after HTML-escaping it. Inside <code>
267257
// Telegram will not auto-detect URLs, so destinations like api.github.com
268258
// render as monospace text rather than clickable blue links.
269259
func htmlCode(s string) string {
270-
return "<code>" + htmlEscapeText(s) + "</code>"
260+
return "<code>" + htmlEscape(s) + "</code>"
271261
}
272262

273263
func (h *CommandHandler) policyShowFromStore() string {
@@ -329,10 +319,10 @@ func (h *CommandHandler) policyShowFromStore() string {
329319
fmt.Fprintf(&b, " -> %s", htmlCode(r.Replacement))
330320
}
331321
if r.Name != "" {
332-
fmt.Fprintf(&b, " (%s)", htmlEscapeText(r.Name))
322+
fmt.Fprintf(&b, " (%s)", htmlEscape(r.Name))
333323
}
334324
if r.Source != "" {
335-
fmt.Fprintf(&b, " [%s]", htmlEscapeText(r.Source))
325+
fmt.Fprintf(&b, " [%s]", htmlEscape(r.Source))
336326
}
337327
b.WriteString("\n")
338328
}
@@ -364,14 +354,14 @@ func (h *CommandHandler) policyAllow(dest string) string {
364354
if err := h.recompileAndSwap(); err != nil {
365355
return fmt.Sprintf("Added allow rule but failed to recompile: %v", err)
366356
}
367-
return fmt.Sprintf("Added allow rule: %s", dest)
357+
return "Added allow rule: " + htmlCode(dest)
368358
}
369359

370360
// Fallback to in-memory mutation when store is not configured.
371361
if err := h.engine.Load().AddAllowRule(dest); err != nil { //nolint:staticcheck // backward compat fallback when no store
372362
return fmt.Sprintf("Failed to add allow rule: %v", err)
373363
}
374-
return fmt.Sprintf("Added allow rule: %s%s", dest, inMemoryWarning)
364+
return "Added allow rule: " + htmlCode(dest) + inMemoryWarning
375365
}
376366

377367
func (h *CommandHandler) policyDeny(dest string) string {
@@ -389,13 +379,13 @@ func (h *CommandHandler) policyDeny(dest string) string {
389379
if err := h.recompileAndSwap(); err != nil {
390380
return fmt.Sprintf("Added deny rule but failed to recompile: %v", err)
391381
}
392-
return fmt.Sprintf("Added deny rule: %s", dest)
382+
return "Added deny rule: " + htmlCode(dest)
393383
}
394384

395385
if err := h.engine.Load().AddDenyRule(dest); err != nil { //nolint:staticcheck // backward compat fallback when no store
396386
return fmt.Sprintf("Failed to add deny rule: %v", err)
397387
}
398-
return fmt.Sprintf("Added deny rule: %s%s", dest, inMemoryWarning)
388+
return "Added deny rule: " + htmlCode(dest) + inMemoryWarning
399389
}
400390

401391
func (h *CommandHandler) policyRemove(idStr string) string {

0 commit comments

Comments
 (0)