`,
t, actorAttr, escapeXMLAttrValue(event.PinnedMessageID))
default:
return ""
}
}
// --- Helpers ---
func escapeXMLAttrValue(s string) string {
r := strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
"\"", """,
)
return r.Replace(s)
}
func escapeXMLText(s string) string {
r := strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
)
return r.Replace(s)
}
func formatSender(user *CanonicalUser, contactNames map[string]string) string {
if user == nil {
return ""
}
if contactNames != nil {
if name, ok := contactNames[user.ID]; ok {
if user.Username != "" && user.Username != name {
return name + " (@" + user.Username + ")"
}
return name
}
}
displayName := user.DisplayName
if displayName == "" {
if user.Username != "" {
return user.Username
}
return user.ID
}
if user.Username != "" && user.Username != displayName {
return displayName + " (@" + user.Username + ")"
}
return displayName
}
func resolveForwardFrom(info *ForwardInfo, contactNames map[string]string) string {
if info.Sender != nil {
return formatSender(info.Sender, contactNames)
}
if info.SenderName != "" {
return info.SenderName
}
if info.FromUserID != "" {
return "user:" + info.FromUserID
}
if info.FromChatID != "" {
return "chat:" + info.FromChatID
}
return "unknown"
}
func pad2(n int) string {
if n < 10 && n >= 0 {
return "0" + strconv.Itoa(n)
}
return strconv.Itoa(n)
}
func formatTimestamp(epochSec int64, utcOffsetMin int) string {
t := time.Unix(epochSec, 0).UTC().Add(time.Duration(utcOffsetMin) * time.Minute)
date := fmt.Sprintf("%d-%s-%s", t.Year(), pad2(int(t.Month())), pad2(t.Day()))
timeStr := fmt.Sprintf("%s:%s:%s", pad2(t.Hour()), pad2(t.Minute()), pad2(t.Second()))
sign := "+"
abs := utcOffsetMin
if utcOffsetMin < 0 {
sign = "-"
abs = -utcOffsetMin
}
offset := fmt.Sprintf("%s%s:%s", sign, pad2(abs/60), pad2(abs%60))
return date + "T" + timeStr + offset
}
func hasMention(nodes []ContentNode, userID string) bool {
for _, n := range nodes {
if n.Type == "mention" && n.UserID == userID {
return true
}
if hasMention(n.Children, userID) {
return true
}
}
return false
}
func renderContentNodes(nodes []ContentNode) string {
var sb strings.Builder
for _, n := range nodes {
renderContentNode(&sb, n)
}
return sb.String()
}
func renderContentNode(sb *strings.Builder, node ContentNode) {
switch node.Type {
case "text":
sb.WriteString(escapeXMLText(node.Text))
case "code":
sb.WriteString("")
sb.WriteString(escapeXMLText(node.Text))
sb.WriteString("")
case "pre":
if node.Language != "" {
fmt.Fprintf(sb, ``, escapeXMLAttrValue(node.Language))
} else {
sb.WriteString("")
}
sb.WriteString(escapeXMLText(node.Text))
sb.WriteString("")
case "bold":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("")
case "italic":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("")
case "underline":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("")
case "strikethrough":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("")
case "spoiler":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("")
case "blockquote":
sb.WriteString("")
renderChildren(sb, node.Children)
sb.WriteString("
")
case "link":
fmt.Fprintf(sb, ``, escapeXMLAttrValue(node.URL))
renderChildren(sb, node.Children)
sb.WriteString("")
case "mention":
if node.UserID != "" {
fmt.Fprintf(sb, ``, escapeXMLAttrValue(node.UserID))
} else {
sb.WriteString("")
}
renderChildren(sb, node.Children)
sb.WriteString("")
case "custom_emoji":
renderChildren(sb, node.Children)
}
}
func renderChildren(sb *strings.Builder, children []ContentNode) {
for _, child := range children {
renderContentNode(sb, child)
}
}
func renderAttachment(att Attachment) string {
attrs := []string{fmt.Sprintf("type=%q", att.Type)}
if att.MimeType != "" {
attrs = append(attrs, fmt.Sprintf("mime=%q", escapeXMLAttrValue(att.MimeType)))
}
if att.FileName != "" {
attrs = append(attrs, fmt.Sprintf("name=%q", escapeXMLAttrValue(att.FileName)))
}
if att.Width > 0 && att.Height > 0 {
attrs = append(attrs, fmt.Sprintf("size=%q", fmt.Sprintf("%dx%d", att.Width, att.Height)))
}
if att.Duration > 0 {
attrs = append(attrs, fmt.Sprintf("duration=%q", strconv.Itoa(att.Duration)))
}
if att.FilePath != "" {
attrs = append(attrs, fmt.Sprintf("path=%q", escapeXMLAttrValue(att.FilePath)))
}
if att.AltText != "" {
return fmt.Sprintf("%s", strings.Join(attrs, " "), escapeXMLText(att.AltText))
}
return fmt.Sprintf("", strings.Join(attrs, " "))
}