package tgrich import ( "fmt" "math" "strings" "unicode/utf8" "git.scuroneko.dev/scuroneko/laniakea/tgapi" ) type richValidator struct { chars int blocks int media int allowThinking bool } func validateRichBlocks(blocks []tgapi.InputRichBlock, allowThinking bool) error { v := &richValidator{allowThinking: allowThinking} for _, block := range blocks { if err := v.block(block, 0); err != nil { return err } } return nil } func (v *richValidator) addChars(s string) error { v.chars += utf8.RuneCountInString(s) if v.chars > maxRichTextChars { return ErrRichTextTooLong } return nil } func (v *richValidator) addBlocks(n int) error { v.blocks += n if v.blocks > maxRichBlocks { return ErrRichTooManyBlocks } return nil } func (v *richValidator) addMedia(media tgapi.InputMedia, want tgapi.InputMediaType) error { if media.Type != want || media.Media == "" { return fmt.Errorf("%w: got %q, want %q", ErrRichInvalidMedia, media.Type, want) } v.media++ if v.media > maxRichMedia { return ErrRichTooManyMedia } return nil } func expectBlockType(got, want tgapi.InputRichType) error { if got != want { return fmt.Errorf("%w: got %q, want %q", ErrRichInvalidBlockType, got, want) } return nil } func validListItemType(t tgapi.RichBlockListItemType) bool { switch t { case tgapi.InputRichBlockListItemTypeLower, tgapi.InputRichBlockListItemTypeUpper, tgapi.InputRichBlockListItemTypeRomanLow, tgapi.InputRichBlockListItemTypeRomanUpper, tgapi.InputRichBlockListItemTypeDecimal: return true default: return false } } func validateTableCell(cell tgapi.RichBlockTableCell) error { if cell.ColSpan < 0 || cell.RowSpan < 0 { return ErrRichInvalidTableCell } switch cell.Align { case "", "left", "center", "right": default: return ErrRichInvalidTableCell } switch cell.VAlign { case "", "top", "middle", "bottom": default: return ErrRichInvalidTableCell } return nil } func validateMap(block tgapi.InputRichBlockMap) error { if math.IsNaN(block.Location.Latitude) || math.IsInf(block.Location.Latitude, 0) || math.IsNaN(block.Location.Longitude) || math.IsInf(block.Location.Longitude, 0) || block.Location.Latitude < -90 || block.Location.Latitude > 90 || block.Location.Longitude < -180 || block.Location.Longitude > 180 || block.Zoom > 24 || block.Width > 10000 || block.Height > 10000 || uint32(block.Width)+uint32(block.Height) > 10000 { return ErrRichInvalidMap } if block.Width != 0 && block.Height != 0 { longer := float64(block.Width) shorter := float64(block.Height) if longer < shorter { longer, shorter = shorter, longer } if longer/shorter > 20 { return ErrRichInvalidMap } } return nil } func (v *richValidator) text(text tgapi.RichText, depth int) error { switch el := text.(type) { case nil: return nil case tgapi.RichTextPlain: return v.addChars(string(el)) case tgapi.RichTextArray: for _, item := range el { if err := v.text(item, depth); err != nil { return err } } return nil case tgapi.RichTextCustomEmoji: return v.addChars(el.AlternativeText) case tgapi.RichTextMathematicalExpression: return v.addChars(el.Expression) case tgapi.RichTextAnchor: return nil } if depth >= maxRichDepth { return ErrRichNestingTooDeep } var child tgapi.RichText switch el := text.(type) { case tgapi.RichTextWrap: case tgapi.RichTextURL: case tgapi.RichTextEmailAddress: case tgapi.RichTextPhoneNumber: case tgapi.RichTextBankCardNumber: if err := validateAutomaticEntity(el.Text, el.BankCardNumber); err != nil { return err } case tgapi.RichTextMention: if err := validateAutomaticEntity(el.Text, "@"+strings.TrimPrefix(el.Username, "@")); err != nil { return err } case tgapi.RichTextHashtag: if err := validateAutomaticEntity(el.Text, "#"+strings.TrimPrefix(el.Hashtag, "#")); err != nil { return err } case tgapi.RichTextCashtag: if err := validateAutomaticEntity(el.Text, "$"+strings.TrimPrefix(el.Cashtag, "$")); err != nil { return err } case tgapi.RichTextBotCommand: if err := validateAutomaticEntity(el.Text, "/"+strings.TrimPrefix(el.BotCommand, "/")); err != nil { return err } case tgapi.RichTextAnchorLink: case tgapi.RichTextReference: case tgapi.RichTextReferenceLink: case tgapi.RichTextDateTime: case tgapi.RichTextTextMention: child = el.Text default: return ErrRichUnknownTag } return v.text(child, depth+1) } func validateAutomaticEntity(text tgapi.RichText, semantic string) error { visible, err := visibleRichText(text) if err != nil { return err } if visible != semantic { return fmt.Errorf("%w: visible %q, semantic %q", ErrRichEntityMismatch, visible, semantic) } return nil } func visibleRichText(text tgapi.RichText) (string, error) { switch el := text.(type) { case nil: return "", nil case tgapi.RichTextPlain: return string(el), nil case tgapi.RichTextArray: var result strings.Builder for _, item := range el { part, err := visibleRichText(item) if err != nil { return "", err } result.WriteString(part) } return result.String(), nil case tgapi.RichTextCustomEmoji: return el.AlternativeText, nil case tgapi.RichTextMathematicalExpression: return el.Expression, nil case tgapi.RichTextAnchor: return "", nil case tgapi.RichTextWrap: return visibleRichText(el.Text) case tgapi.RichTextURL: return visibleRichText(el.Text) case tgapi.RichTextEmailAddress: return visibleRichText(el.Text) case tgapi.RichTextPhoneNumber: return visibleRichText(el.Text) case tgapi.RichTextBankCardNumber: return visibleRichText(el.Text) case tgapi.RichTextMention: return visibleRichText(el.Text) case tgapi.RichTextHashtag: return visibleRichText(el.Text) case tgapi.RichTextCashtag: return visibleRichText(el.Text) case tgapi.RichTextBotCommand: return visibleRichText(el.Text) case tgapi.RichTextAnchorLink: return visibleRichText(el.Text) case tgapi.RichTextReference: return visibleRichText(el.Text) case tgapi.RichTextReferenceLink: return visibleRichText(el.Text) case tgapi.RichTextDateTime: return visibleRichText(el.Text) case tgapi.RichTextTextMention: return visibleRichText(el.Text) default: return "", ErrRichUnknownTag } } func (v *richValidator) caption(caption *tgapi.RichBlockCaption, depth int) error { if caption == nil { return nil } if err := v.text(caption.Text, depth); err != nil { return err } return v.text(caption.Credit, depth) } func (v *richValidator) nested(blocks []tgapi.InputRichBlock, depth int) error { for _, block := range blocks { if err := v.block(block, depth); err != nil { return err } } return nil } func (v *richValidator) block(block tgapi.InputRichBlock, depth int) error { if depth >= maxRichDepth { return ErrRichNestingTooDeep } if err := v.addBlocks(1); err != nil { return err } switch el := block.(type) { case tgapi.InputRichBlockParagraph: if err := expectBlockType(el.Type, tgapi.InputRichTypeParagraph); err != nil { return err } return v.text(el.Text, depth+1) case tgapi.InputRichBlockSectionHeading: if err := expectBlockType(el.Type, tgapi.InputRichTypeSectionHeading); err != nil { return err } if el.Size < 1 || el.Size > 6 { return ErrRichInvalidHeading } return v.text(el.Text, depth+1) case tgapi.InputRichBlockPreformatted: if err := expectBlockType(el.Type, tgapi.InputRichTypePre); err != nil { return err } return v.text(el.Text, depth+1) case tgapi.InputRichBlockFooter: if err := expectBlockType(el.Type, tgapi.InputRichTypeFooter); err != nil { return err } return v.text(el.Text, depth+1) case tgapi.InputRichBlockDivider: return expectBlockType(el.Type, tgapi.InputRichTypeDivider) case tgapi.InputRichBlockAnchor: return expectBlockType(el.Type, tgapi.InputRichTypeAnchor) case tgapi.InputRichBlockMath: if err := expectBlockType(el.Type, tgapi.InputRichTypeMathematicalExpression); err != nil { return err } return v.addChars(el.Expression) case tgapi.InputRichBlockList: if err := expectBlockType(el.Type, tgapi.InputRichTypeList); err != nil { return err } if err := v.addBlocks(len(el.Items)); err != nil { return err } ordered := false unordered := false for _, item := range el.Items { if item.IsChecked && !item.HasCheckbox { return ErrRichInvalidCheckbox } if item.Type == "" { unordered = true if item.Value != 0 { return ErrRichInvalidListItem } } else { ordered = true if !validListItemType(item.Type) { return ErrRichInvalidListItemType } } if err := v.nested(item.Blocks, depth+1); err != nil { return err } } if ordered && unordered { return ErrRichListItemMix } return nil case tgapi.InputRichBlockBlockQuotation: if err := expectBlockType(el.Type, tgapi.InputRichTypeBlockQuotation); err != nil { return err } if err := v.textValue(el.Credit, depth+1); err != nil { return err } return v.nested(el.Blocks, depth+1) case tgapi.InputRichBlockPullQuotation: if err := expectBlockType(el.Type, tgapi.InputRichTypePullQuotation); err != nil { return err } if err := v.text(el.Text, depth+1); err != nil { return err } return v.textValue(el.Credit, depth+1) case tgapi.InputRichBlockCollage: if err := expectBlockType(el.Type, tgapi.InputRichTypeCollage); err != nil { return err } if err := v.caption(el.Caption, depth+1); err != nil { return err } return v.nested(el.Blocks, depth+1) case tgapi.InputRichBlockSlideshow: if err := expectBlockType(el.Type, tgapi.InputRichTypeSlideshow); err != nil { return err } if err := v.caption(el.Caption, depth+1); err != nil { return err } return v.nested(el.Blocks, depth+1) case tgapi.InputRichBlockTable: if err := expectBlockType(el.Type, tgapi.InputRichTypeTable); err != nil { return err } if err := v.addBlocks(len(el.Cells)); err != nil { return err } if err := v.textValue(el.Caption, depth+1); err != nil { return err } for _, row := range el.Cells { columns := 0 for _, cell := range row { if err := validateTableCell(cell); err != nil { return err } span := cell.ColSpan if span < 1 { span = 1 } columns += span if err := v.text(cell.Text, depth+1); err != nil { return err } } if columns > maxTableColumns { return ErrRichTableTooWide } } return nil case tgapi.InputRichBlockDetails: if err := expectBlockType(el.Type, tgapi.InputRichTypeDetails); err != nil { return err } if err := v.text(el.Summary, depth+1); err != nil { return err } return v.nested(el.Blocks, depth+1) case tgapi.InputRichBlockMap: if err := expectBlockType(el.Type, tgapi.InputRichTypeMap); err != nil { return err } if err := validateMap(el); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockAnimation: if err := expectBlockType(el.Type, tgapi.InputRichTypeAnimation); err != nil { return err } if err := v.addMedia(el.Animation, tgapi.InputMediaTypeAnimation); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockAudio: if err := expectBlockType(el.Type, tgapi.InputRichTypeAudio); err != nil { return err } if err := v.addMedia(el.Audio, tgapi.InputMediaTypeAudio); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockPhoto: if err := expectBlockType(el.Type, tgapi.InputRichTypePhoto); err != nil { return err } if err := v.addMedia(el.Photo, tgapi.InputMediaTypePhoto); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockVideo: if err := expectBlockType(el.Type, tgapi.InputRichTypeVideo); err != nil { return err } if err := v.addMedia(el.Video, tgapi.InputMediaTypeVideo); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockVoiceNote: if err := expectBlockType(el.Type, tgapi.InputRichTypeVoiceNote); err != nil { return err } if err := v.addMedia(el.VoiceNote, tgapi.InputMediaTypeVoiceNote); err != nil { return err } return v.caption(el.Caption, depth+1) case tgapi.InputRichBlockThinking: if !v.allowThinking { return ErrRichThinkingDraftOnly } if err := expectBlockType(el.Type, tgapi.InputRichTypeThinking); err != nil { return err } return v.text(el.Text, depth+1) default: return ErrRichUnknownTag } } func (v *richValidator) textValue(text *tgapi.RichText, depth int) error { if text == nil { return nil } return v.text(*text, depth) }