-
Notifications
You must be signed in to change notification settings - Fork 77
Expand file tree
/
Copy pathsummarizer_format_telegram.go
More file actions
249 lines (218 loc) · 6.58 KB
/
summarizer_format_telegram.go
File metadata and controls
249 lines (218 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
package taskengine
import (
"html"
"strings"
)
// formatTelegramFromStructured formats Summary into Telegram HTML using structured data
// Uses the PRD format: emoji + subject, network, time, executions list, footer
// All user-controlled content is HTML-escaped to prevent XSS attacks
func formatTelegramFromStructured(s Summary) string {
var sb strings.Builder
// Get status emoji - API returns subject WITHOUT emoji, aggregator prepends it
statusEmoji := getStatusEmoji(s)
// Subject as header with emoji prefix - only bold the workflow name
if s.Subject != "" {
sb.WriteString(statusEmoji)
if statusEmoji != "" {
sb.WriteString(" ")
}
sb.WriteString(formatSubjectWithBoldName(s.Subject))
sb.WriteString("\n")
}
// Skipped note (success runs with branch-skipped steps) — surfaces why the
// header shows ⚠️ instead of ✅.
if s.SkippedNote != "" {
sb.WriteString("<i>")
sb.WriteString(html.EscapeString(s.SkippedNote))
sb.WriteString("</i>\n")
}
// Network: use body.network field, fallback to workflow.chain or derive from chainID
network := s.Network
if network == "" && s.Workflow != nil {
network = s.Workflow.Chain
if network == "" && s.Workflow.ChainID > 0 {
network = getChainDisplayName(s.Workflow.ChainID)
}
}
// Network and Time section
if network != "" || s.TriggeredAt != "" {
sb.WriteString("\n")
if network != "" {
sb.WriteString("<b>Network:</b> ")
sb.WriteString(html.EscapeString(network))
sb.WriteString("\n")
}
if s.TriggeredAt != "" {
sb.WriteString("<b>Time:</b> ")
sb.WriteString(html.EscapeString(formatTimestampHumanReadable(s.TriggeredAt)))
sb.WriteString("\n")
}
}
// Trigger section
if s.Trigger != "" {
sb.WriteString("\n<b>Trigger:</b> ")
sb.WriteString(html.EscapeString(s.Trigger))
sb.WriteString("\n")
}
// Display executions with "Executed:" header (PRD format)
if len(s.Executions) > 0 {
sb.WriteString("\n<b>Executed:</b>\n")
for _, exec := range s.Executions {
sb.WriteString("• ")
sb.WriteString(formatBackticksForChannel(exec.Description, "telegram"))
sb.WriteString("\n")
if exec.TxHash != "" {
if explorerURL := buildTxExplorerURL(s, exec.TxHash); explorerURL != "" {
sb.WriteString(" Transaction: <a href=\"")
sb.WriteString(html.EscapeString(explorerURL))
sb.WriteString("\">")
sb.WriteString(html.EscapeString(truncateTxHash(exec.TxHash)))
sb.WriteString("</a>\n")
}
}
}
}
// "What Went Wrong" section — consistent with email
if len(s.Errors) > 0 {
sb.WriteString("\n<b>What Went Wrong:</b>\n")
for _, err := range s.Errors {
sb.WriteString("• ")
sb.WriteString(formatBackticksForChannel(err, "telegram"))
sb.WriteString("\n")
}
}
if s.Annotation != "" {
sb.WriteString("\n<i>")
sb.WriteString(html.EscapeString(s.Annotation))
sb.WriteString("</i>")
}
return strings.TrimSpace(sb.String())
}
// getStatusEmoji returns the emoji for the summary's status.
// A success run with branch-skipped steps renders ⚠️ (warn) rather than ✅,
// matching the yellow badge on email.
func getStatusEmoji(s Summary) string {
switch s.Status {
case "success":
if s.SkippedSteps > 0 {
return "⚠️"
}
return "✅"
case "failed", "error":
return "❌"
default:
return ""
}
}
// formatSubjectWithBoldName formats the subject with <code> tags around the
// prefix + workflow name (everything except the trailing status suffix).
// Subject patterns:
// - "Simulation: {name} successfully completed"
// - "Run Node: {name} succeeded"
// - "Run #N: {name} successfully completed"
// - "{name} successfully completed"
// - And similar patterns for "failed to execute" and "partially executed"
func formatSubjectWithBoldName(subject string) string {
// Suffixes to look for (ordered from most specific to least)
suffixes := []string{
" successfully completed",
" failed to execute",
" partially executed",
" succeeded",
}
// Prefixes to look for
prefixes := []string{
"Simulation: ",
"Run Node: ",
}
// Check for "Run #N: " prefix pattern
runPrefix := ""
if strings.HasPrefix(subject, "Run #") {
// Find the ": " after "Run #N"
if idx := strings.Index(subject, ": "); idx > 0 {
runPrefix = subject[:idx+2]
}
}
// Find which suffix matches
var suffix string
var nameEnd int
for _, s := range suffixes {
if strings.HasSuffix(subject, s) {
suffix = s
nameEnd = len(subject) - len(s)
break
}
}
// Check for deployed workflow format: "{name}: succeeded (...)" or "{name}: failed at ..."
// Must be checked before the generic " failed at " pattern to avoid splitting at the wrong point.
if suffix == "" {
for _, marker := range []string{": succeeded (", ": failed at "} {
if idx := strings.Index(subject, marker); idx > 0 {
suffix = subject[idx:]
nameEnd = idx
break
}
}
}
// Check for "failed at <stepName>" suffix (Run Node failure format)
if suffix == "" && strings.Contains(subject, " failed at ") {
idx := strings.LastIndex(subject, " failed at ")
if idx > 0 {
suffix = subject[idx:]
nameEnd = idx
}
}
// If no suffix found, just escape and return
if suffix == "" {
return html.EscapeString(subject)
}
// Find the prefix and extract the name
var prefix string
nameStart := 0
if runPrefix != "" {
prefix = runPrefix
nameStart = len(runPrefix)
} else {
for _, p := range prefixes {
if strings.HasPrefix(subject, p) {
prefix = p
nameStart = len(p)
break
}
}
}
// Extract the workflow name
name := subject[nameStart:nameEnd]
// Build the formatted string: <code>prefix + name</code> + suffix
// Using <code> prevents Telegram from auto-linking names that contain dots
var sb strings.Builder
sb.WriteString("<code>")
if prefix != "" {
sb.WriteString(html.EscapeString(prefix))
}
sb.WriteString(html.EscapeString(name))
sb.WriteString("</code>")
sb.WriteString(html.EscapeString(suffix))
return sb.String()
}
func formatTelegramExampleMessage(workflowName, chainName string) string {
var sb strings.Builder
// Status line with emoji and workflow name (code-wrapped to prevent auto-linking)
sb.WriteString("✅ <code>")
sb.WriteString(html.EscapeString(workflowName))
sb.WriteString("</code> completed\n\n")
// Network
sb.WriteString("<b>Network:</b> ")
sb.WriteString(html.EscapeString(chainName))
sb.WriteString("\n\n")
// Executed section with example
sb.WriteString("<b>Executed:</b>\n")
sb.WriteString("• (Simulated) ")
sb.WriteString(ExampleExecutionMessage)
sb.WriteString("\n\n")
// Example notice
sb.WriteString("<i>")
sb.WriteString(ExampleExecutionAnnotation)
sb.WriteString("</i>")
return sb.String()
}