Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion condor/reports/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,41 @@
}


_CELL_LINK_RE = re.compile(
r"^\[([^\]\n]+)\]\((https?://[^)\s]+)\)(\s*\S*)?$"
)
Comment thread
greptile-apps[bot] marked this conversation as resolved.
_HTML_ANCHOR_RE = re.compile(
r'^<a href="(https?://[^"]+)"[^>]*>(.*?)</a>$',
re.DOTALL,
)


def _escape_cell(value: object) -> str:
"""Escape a table cell; a full ``[label](url)`` markdown link or an
existing ``<a href="https://…">…</a>`` anchor becomes a clickable link.

Only a cell whose whole value is a markdown link (an optional trailing
marker such as `` ⚠`` is allowed) or a well-formed http(s) anchor with
tag-free content is converted to an ``<a>``. Everything else is escaped
verbatim, so existing plain-text cells render unchanged.
"""
text = str(value)
m = _CELL_LINK_RE.match(text)
if m:
return (
f'<a href="{html.escape(m.group(2), quote=True)}" '
f'target="_blank" rel="noopener noreferrer">'
f"{html.escape(m.group(1))}</a>{html.escape(m.group(3) or '')}"
)
m = _HTML_ANCHOR_RE.match(text)
if m and "<" not in m.group(2) and ">" not in m.group(2):
return (
f'<a href="{html.escape(m.group(1), quote=True)}" '
f'target="_blank" rel="noopener noreferrer">{m.group(2)}</a>'
)
return html.escape(text)


class ReportBuilder:
"""Compose a report section by section, then ``save()`` it.

Expand Down Expand Up @@ -561,7 +596,7 @@ def _render_table(columns: list[str], rows: list[dict]) -> str:
body_rows = []
for row in rows:
cells = "".join(
f"<td>{html.escape(str(row.get(column, '')))}</td>"
f"<td>{_escape_cell(row.get(column, ''))}</td>"
for column in columns
)
body_rows.append(f"<tr>{cells}</tr>")
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/routines/ReportFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function ReportFrame({
srcDoc={html}
className={`h-full w-full border-0 ${className}`}
title={title}
sandbox="allow-scripts allow-popups allow-downloads"
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads"
/>
);
}
37 changes: 36 additions & 1 deletion frontend/src/components/routines/RoutineResultView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@ interface Props {
instance: RoutineInstance;
}

const CELL_LINK_RE = /^\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)(\s*\S*)?$/;
const HTML_ANCHOR_RE = /^<a href="(https?:\/\/[^"]+)"[^>]*>(.*?)<\/a>$/;

function formatCellValue(val: unknown) {
if (typeof val === "number") {
// Numbers keep their exact formatting — a link can be a numeric-looking
// string while only strings matching the full [label](url) pattern change.
return val.toFixed(val % 1 === 0 ? 0 : 2);
}
const s = String(val ?? "");
const m = CELL_LINK_RE.exec(s);
if (m) {
// Same [label](url) convention as report table cells (ReportBuilder).
return (
<>
<a href={m[2]} target="_blank" rel="noopener noreferrer" className="underline hover:opacity-80">
{m[1]}
</a>
{m[3] ?? ""}
</>
);
}
const m2 = HTML_ANCHOR_RE.exec(s);
if (m2 && !m2[2].includes("<") && !m2[2].includes(">")) {
// Existing HTML anchors (e.g. the pool scanner's Link cell) render as
// links too, mirroring ReportBuilder's _escape_cell.
return (
<a href={m2[1]} target="_blank" rel="noopener noreferrer" className="underline hover:opacity-80">
{m2[2]}
</a>
);
}
return s;
}

interface KpiSection {
type: "kpi";
label: string;
Expand Down Expand Up @@ -141,7 +176,7 @@ export function RoutineResultView({ instance }: Props) {
: "";
return (
<td key={col} className={`px-3 py-1.5 font-mono ${numColor}`}>
{isNum ? (val as number).toFixed(val % 1 === 0 ? 0 : 2) : String(val ?? "")}
{formatCellValue(val)}
</td>
);
})}
Expand Down
14 changes: 9 additions & 5 deletions handlers/routines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@
normalize_result,
)
from utils.auth import restricted
from utils.telegram_formatters import escape_markdown_v2, escape_markdown_v2_code
from utils.telegram_formatters import (
escape_markdown_v2,
escape_markdown_v2_code,
format_routine_result,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -547,7 +551,7 @@ async def _interval_job_callback(context: CallbackContext) -> None:
text = (
f"{icon} *{escape_markdown_v2(_display_name(routine_name))}* `{instance_id}`\n"
f"⏱️ {escape_markdown_v2(interval_str)} \\| Run \\#{run_count} \\| {escape_markdown_v2(_format_duration(duration))}\n\n"
f"```\n{escape_markdown_v2_code(result[:400])}\n```"
f"{format_routine_result(result)}"
)
try:
keyboard = None
Expand Down Expand Up @@ -614,7 +618,7 @@ async def _oneshot_job_callback(context: CallbackContext) -> None:
text = (
f"{icon} *{escape_markdown_v2(_display_name(routine_name))}*\n"
f"Duration: {escape_markdown_v2(_format_duration(duration))}\n\n"
f"```\n{escape_markdown_v2_code(result[:400])}\n```"
f"{format_routine_result(result)}"
)
try:
keyboard = None
Expand Down Expand Up @@ -688,7 +692,7 @@ async def _daily_job_callback(context: CallbackContext) -> None:
icon = "✅" if not result.startswith("Error") else "❌"
text = (
f"{icon} *Daily: {escape_markdown_v2(_display_name(routine_name))}*\n"
f"```\n{escape_markdown_v2_code(result[:400])}\n```"
f"{format_routine_result(result)}"
)
try:
keyboard = None
Expand Down Expand Up @@ -1340,7 +1344,7 @@ async def _refresh_detail_msg(
dur_str = _format_duration(duration) if duration else ""
result_section = (
f"\n\n┌─ {icon} Result ─ {escape_markdown_v2(dur_str)} ────\n"
f"```\n{escape_markdown_v2_code(result[:250])}\n```\n"
f"{format_routine_result(result, max_len=250)}\n"
f"└────────────────────────────"
)

Expand Down
32 changes: 32 additions & 0 deletions tests/test_report_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import condor.reports as reports
from condor.reports import rendering, store
from condor.reports.builder import _escape_cell
from condor.reports.footprint import (
build_estimated_footprint_figure,
candle_timestamps,
Expand Down Expand Up @@ -45,6 +46,37 @@ def test_markdown_preserves_single_newlines():
assert "<strong>Market Analysis</strong>" in rendered


def test_escape_cell_markdown_link_becomes_anchor():
out = _escape_cell("[GeckoTerminal](https://www.geckoterminal.com/solana)")
assert 'href="https://www.geckoterminal.com/solana"' in out
assert 'target="_blank"' in out
assert ">GeckoTerminal</a>" in out


def test_escape_cell_existing_html_anchor_passes_through():
# The pool scanner already emits HTML anchors in its Link cell.
out = _escape_cell(
'<a href="https://www.geckoterminal.com/solana/pools/abc" '
'target="_blank" title="Open on Meteora">&#x1F517;</a>'
)
assert 'href="https://www.geckoterminal.com/solana/pools/abc"' in out
assert 'target="_blank"' in out
assert "&#x1F517;" in out


def test_escape_cell_anchor_with_tags_is_escaped():
# An anchor whose content carries raw tags must not pass through.
out = _escape_cell('<a href="https://x.example">ok<script>alert(1)</script></a>')
assert "<script>" not in out
assert "&lt;script&gt;" in out


def test_escape_cell_plain_text_is_escaped():
out = _escape_cell("<b>bold</b> & more")
assert "<b>" not in out
assert "&lt;b&gt;bold&lt;/b&gt;" in out


def test_interactive_report_embeds_safe_runtime(reports_dir):
rows = [
{"timestamp": "2026-01-01T00:00:00Z", "pair": "BTC-USDT", "price": 100},
Expand Down
57 changes: 56 additions & 1 deletion tests/test_routine_result_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
from pydantic import BaseModel

import handlers.routines as hr
from utils.telegram_formatters import escape_markdown_v2_code
from utils.telegram_formatters import (
escape_markdown_v2_code,
format_routine_result,
)

# Backtick (closes the fence), backslash (eats the next char), and a few chars
# that are reserved outside a fence but literal inside one.
Expand Down Expand Up @@ -56,6 +59,17 @@ def render_markdown_v2(text: str) -> str:
out.append(c)
i += 1
continue
if c == "[":
end = text.find("](", i)
close = text.find(")", end + 2) if end != -1 else -1
if end == -1 or close == -1:
raise ValueError(
f"Can't parse entities: character '[' is reserved "
f"and must be escaped at byte offset {i}"
)
out.append(text[i + 1 : end].replace("\\", ""))
i = close + 1
continue
if c in "*_":
if stack and stack[-1] == c:
stack.pop()
Expand Down Expand Up @@ -179,3 +193,44 @@ def test_truncation_happens_before_escaping():
escaped = escape_markdown_v2_code("\\" * 400)
assert len(escaped) == 800
assert render_markdown_v2(f"```\n{escaped}\n```") == "\n" + "\\" * 400 + "\n"


def test_bold_only_result_renders_bold_outside_code_block():
out = format_routine_result("**Profit: 5%**")
assert "```" not in out
assert render_markdown_v2(out) == "Profit: 5%"


def test_italic_only_result_renders_italic_outside_code_block():
out = format_routine_result("*Profit: 5%*")
assert "```" not in out
assert render_markdown_v2(out) == "Profit: 5%"


def test_mixed_emphasis_and_link_renders():
out = format_routine_result(
"**Bold** and *italic* and [link](https://x.example)"
)
assert "```" not in out
assert render_markdown_v2(out) == "Bold and italic and link"


def test_link_result_renders_clickable_link():
out = format_routine_result("[GeckoTerminal](https://www.geckoterminal.com/solana)")
assert "```" not in out
assert "](https://www.geckoterminal.com/solana)" in out
assert render_markdown_v2(out) == "GeckoTerminal"


def test_link_with_backslash_in_url_is_escaped():
# A raw backslash in the destination would escape the closing ")" in
# MarkdownV2 and make Telegram reject the whole message.
out = format_routine_result("[x](https://x.example/a\\b)")
assert "\\\\" in out
assert render_markdown_v2(out) == "x"


def test_plain_result_keeps_code_block():
out = format_routine_result("just text, no markers")
assert out.startswith("```")
assert render_markdown_v2(out) == "\njust text, no markers\n"
46 changes: 46 additions & 0 deletions utils/telegram_formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,52 @@ def escape_markdown_v2_code(text: str) -> str:
return str(text).replace("\\", "\\\\").replace("`", "\\`")


def format_routine_result(result: str, max_len: int = 400) -> str:
"""Format a routine result for a Telegram MarkdownV2 message.

Results containing ``[label](url)`` links, ``**bold**`` or ``*italic*``
markers are rendered outside a code block (which would suppress them),
keeping the links clickable and the emphasis rendered, with everything
else escaped. Results with none of these keep the monospace code block,
so existing routines render exactly as before.
"""
result = str(result)[:max_len]
if not re.search(
r"\[[^\]\n]+\]\(https?://[^)\s]+\)|\*\*[^*\n]+\*\*|\*[^*\n]+\*",
result,
):
return f"```\n{escape_markdown_v2_code(result)}\n```"
return _markdown_v2_with_links(result)


def _markdown_v2_with_links(text: str) -> str:
"""Escape text for MarkdownV2 but keep [label](url) links, **bold** and
*italic* markers rendered instead of escaped."""
pattern = re.compile(
r"(\[[^\]\n]+\]\(https?://[^)\s]+\)|\*\*[^*\n]+\*\*|\*[^*\n]+\*)"
)
out = []
for part in pattern.split(text):
if not part:
continue
if part.startswith("[") and "](http" in part:
label, url = part[1:].split("](", 1)
if url.endswith(")"):
url = url[:-1]
# A backslash in the destination would escape the closing ")"
# (or the next char) in MarkdownV2 and make Telegram reject the
# whole message, so double it to render literally.
escaped_url = url.replace("\\", "\\\\")
out.append(f"[{escape_markdown_v2(label)}]({escaped_url})")
elif part.startswith("**") and part.endswith("**"):
out.append(f"*{escape_markdown_v2(part[2:-2])}*")
elif part.startswith("*") and part.endswith("*"):
out.append(f"_{escape_markdown_v2(part[1:-1])}_")
else:
out.append(escape_markdown_v2(part))
return "".join(out)


def format_number(value: float, decimals: int = 2) -> str:
"""Format a number with commas and specified decimals"""
if value >= 1000000:
Expand Down
Loading