owpengram-server/internal/rpc/convert_rich_message.go

260 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"strconv"
"github.com/iamxvbaba/td/bin"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
// 本文件集中 Layer 228 富文本消息richMessage的 tg.* ↔ domain 转换。
// inputRichMessage 的 blocks、HTML 与 Markdown 三种输入均在 RPC 边界归一为 PageBlock
// blocks 以 TL 向量序列化为不透明字节存 domain详见 domain.MessageRichMessage
// encodeRichBlocks 把 []tg.PageBlockClass 序列化为 TL 向量字节(含 vector 头)。
func encodeRichBlocks(blocks []tg.PageBlockClass) ([]byte, error) {
var b bin.Buffer
b.PutVectorHeader(len(blocks))
for _, blk := range blocks {
if blk == nil {
return nil, mediaInvalidErr()
}
if err := blk.Encode(&b); err != nil {
return nil, err
}
}
return b.Buf, nil
}
// decodeRichBlocks 把 encodeRichBlocks 产生的字节还原为 []tg.PageBlockClass。
func decodeRichBlocks(data []byte) ([]tg.PageBlockClass, error) {
if len(data) == 0 {
return nil, nil
}
b := &bin.Buffer{Buf: append([]byte(nil), data...)}
n, err := b.VectorHeader()
if err != nil {
return nil, err
}
out := make([]tg.PageBlockClass, 0, n)
for i := 0; i < n; i++ {
blk, err := tg.DecodePageBlock(b)
if err != nil {
return nil, err
}
out = append(out, blk)
}
return out, nil
}
func normalizeRichBlocksForClients(blocks []tg.PageBlockClass) {
for _, block := range blocks {
normalizeRichBlockForClients(block)
}
}
func normalizeRichBlockForClients(block tg.PageBlockClass) {
switch b := block.(type) {
case *tg.PageBlockList:
for _, item := range b.Items {
if item, ok := item.(*tg.PageListItemBlocks); ok {
normalizeRichBlocksForClients(item.Blocks)
}
}
case *tg.PageBlockCover:
normalizeRichBlockForClients(b.Cover)
case *tg.PageBlockEmbedPost:
normalizeRichBlocksForClients(b.Blocks)
case *tg.PageBlockCollage:
normalizeRichBlocksForClients(b.Items)
case *tg.PageBlockSlideshow:
normalizeRichBlocksForClients(b.Items)
case *tg.PageBlockOrderedList:
normalizeOrderedListForClients(b)
case *tg.PageBlockDetails:
normalizeRichBlocksForClients(b.Blocks)
case *tg.PageBlockBlockquoteBlocks:
normalizeRichBlocksForClients(b.Blocks)
}
}
func normalizeOrderedListForClients(list *tg.PageBlockOrderedList) {
if list == nil {
return
}
reversed := list.Reversed || list.Flags.Has(2)
current := 1
if list.Flags.Has(0) || list.Start != 0 {
current = list.Start
} else if reversed {
current = len(list.Items)
}
step := 1
if reversed {
step = -1
}
for _, item := range list.Items {
value := current
switch i := item.(type) {
case *tg.PageListOrderedItemText:
if v, ok := i.GetValue(); ok || i.Value != 0 {
value = v
if !ok {
value = i.Value
}
}
if num, ok := i.GetNum(); !ok || num == "" {
i.SetNum(strconv.Itoa(value))
}
case *tg.PageListOrderedItemBlocks:
if v, ok := i.GetValue(); ok || i.Value != 0 {
value = v
if !ok {
value = i.Value
}
}
if num, ok := i.GetNum(); !ok || num == "" {
i.SetNum(strconv.Itoa(value))
}
normalizeRichBlocksForClients(i.Blocks)
}
current = value + step
}
}
// domainRichMessageFromInput 把入站 tg.InputRichMessageClass 解析为 domain 快照:
// HTML/Markdown 先在服务端解析为 PageBlock再与 blocks 形态共用限额校验、
// 序列化和 Bot API 输出投影;内嵌 photos/documents 复用 sendMedia 同款媒体解析。
// 返回 nil 表示无富文本载荷。
func (r *Router) domainRichMessageFromInput(ctx context.Context, input tg.InputRichMessageClass) (*domain.MessageRichMessage, error) {
if input == nil {
return nil, nil
}
var (
in *tg.InputRichMessage
sourceParsed bool
)
switch value := input.(type) {
case *tg.InputRichMessage:
in = value
case *tg.InputRichMessageHTML:
if value == nil || value.HTML == "" || len(value.Files) != 0 {
return nil, richMessageInvalidErr()
}
blocks, err := parseBotAPIRichHTML(value.HTML)
if err != nil {
return nil, err
}
in = &tg.InputRichMessage{Rtl: value.Rtl, Noautolink: value.Noautolink, Blocks: blocks}
sourceParsed = true
case *tg.InputRichMessageMarkdown:
if value == nil || value.Markdown == "" || len(value.Files) != 0 {
return nil, richMessageInvalidErr()
}
blocks, err := parseBotAPIRichMarkdown(value.Markdown)
if err != nil {
return nil, err
}
in = &tg.InputRichMessage{Rtl: value.Rtl, Noautolink: value.Noautolink, Blocks: blocks}
sourceParsed = true
default:
return nil, richMessageInvalidErr()
}
if len(in.Blocks) == 0 {
if len(in.Photos) == 0 && len(in.Documents) == 0 {
return nil, nil
}
return nil, richMessageInvalidErr()
}
if err := validateRichMessageBlocks(in.Blocks); err != nil {
return nil, err
}
if (len(in.Photos) > 0 || len(in.Documents) > 0) && r.deps.Files == nil {
return nil, notImplementedErr()
}
normalizeRichBlocksForClients(in.Blocks)
blocks, err := encodeRichBlocks(in.Blocks)
if err != nil {
return nil, err
}
rich := &domain.MessageRichMessage{
Rtl: in.Rtl,
Blocks: blocks,
}
projection, projectionErr := botAPIRichMessageProjection(in.Blocks, in.Rtl)
if projectionErr != nil && sourceParsed {
return nil, richMessageInvalidErr()
}
if projectionErr == nil {
rich.BotAPIProjection = projection
}
for _, p := range in.Photos {
id, ok := inputPhotoID(p)
if !ok {
return nil, photoInvalidErr()
}
photo, found, err := r.deps.Files.GetPhoto(ctx, id)
if err != nil {
return nil, internalErr()
}
if !found {
return nil, photoInvalidErr()
}
rich.Photos = append(rich.Photos, photo)
}
for _, d := range in.Documents {
id, ok := inputDocumentID(d)
if !ok {
return nil, mediaInvalidErr()
}
doc, found, err := r.deps.Files.GetDocument(ctx, id)
if err != nil {
return nil, internalErr()
}
if !found {
return nil, mediaInvalidErr()
}
rich.Documents = append(rich.Documents, doc)
}
if rich.IsZero() {
return nil, nil
}
return rich, nil
}
// tgRichMessage 把 domain 富文本快照投影为 tg.RichMessage反序列化 blocks + 复用
// tgPhoto/tgDocument 投影内嵌媒体)。空载荷或 blocks 解码失败返回 (nil, err)。
func tgRichMessage(m *domain.MessageRichMessage) (*tg.RichMessage, error) {
if m.IsZero() {
return nil, nil
}
blocks, err := decodeRichBlocks(m.Blocks)
if err != nil {
return nil, err
}
out := &tg.RichMessage{
Rtl: m.Rtl,
Part: m.Part,
Blocks: blocks,
Photos: make([]tg.PhotoClass, 0, len(m.Photos)),
Documents: make([]tg.DocumentClass, 0, len(m.Documents)),
}
for _, p := range m.Photos {
out.Photos = append(out.Photos, tgPhoto(p))
}
for _, d := range m.Documents {
out.Documents = append(out.Documents, tgDocument(d))
}
return out, nil
}
func mustTGRichMessage(m *domain.MessageRichMessage) *tg.RichMessage {
out, err := tgRichMessage(m)
if err != nil {
panic("invalid stored rich_message: " + err.Error())
}
return out
}