package platform import ( "bytes" "encoding/json" "io" "net/url" "regexp" ) type Paragraph struct { ID string `json:"id"` Text string `json:"text"` Description string `json:"description,omitempty"` } type Section struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description,omitempty"` Paragraphs []Paragraph `json:"paragraphs"` } type Article struct { Sections []Section `json:"sections"` } type Image struct { URL string `json:"url"` Alt string `json:"alt,omitempty"` Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` } type CardPart struct { Kind string `json:"kind"` Text *string `json:"text,omitempty"` Article *Article `json:"article,omitempty"` Image *Image `json:"image,omitempty"` } type Content struct { SchemaVersion int `json:"schema_version"` Text *string `json:"text,omitempty"` Article *Article `json:"article,omitempty"` Image *Image `json:"image,omitempty"` Parts []CardPart `json:"parts,omitempty"` } var blockID = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`) func validArticle(article *Article) bool { if article == nil || len(article.Sections) > 200 { return false } seen := map[string]bool{} total := 0 for _, section := range article.Sections { if !blockID.MatchString(section.ID) || seen[section.ID] || len(section.Title) > 300 || len(section.Description) > 10000 { return false } seen[section.ID] = true total += len(section.Paragraphs) if total > 2000 { return false } for _, paragraph := range section.Paragraphs { if !blockID.MatchString(paragraph.ID) || seen[paragraph.ID] || len(paragraph.Text) > 100000 || len(paragraph.Description) > 10000 { return false } seen[paragraph.ID] = true } } return true } func validImage(image *Image) bool { if image == nil || len(image.URL) > 2048 || len(image.Alt) > 2000 || image.Width < 0 || image.Width > 100000 || image.Height < 0 || image.Height > 100000 { return false } u, err := url.Parse(image.URL) return err == nil && u.Scheme == "https" && u.Hostname() != "" && u.User == nil && u.Fragment == "" } func validPart(part CardPart) bool { switch part.Kind { case "text": return part.Text != nil && len(*part.Text) <= 100000 && part.Article == nil && part.Image == nil case "article": return part.Text == nil && part.Image == nil && validArticle(part.Article) case "image": return part.Text == nil && part.Article == nil && validImage(part.Image) default: return false } } func normalizeContent(kind string, raw json.RawMessage) (json.RawMessage, bool) { if raw = bytes.TrimSpace(raw); len(raw) == 0 || raw[0] != '{' { return nil, false } var content Content d := json.NewDecoder(bytes.NewReader(raw)) d.DisallowUnknownFields() if d.Decode(&content) != nil || d.Decode(new(any)) != io.EOF { return nil, false } if content.SchemaVersion == 0 { content.SchemaVersion = 1 } if content.SchemaVersion != 1 { return nil, false } switch kind { case "text", "article", "image": if len(content.Parts) > 0 || !validPart(CardPart{Kind: kind, Text: content.Text, Article: content.Article, Image: content.Image}) { return nil, false } case "board": if content.Text != nil || content.Article != nil || content.Image != nil || len(content.Parts) > 0 { return nil, false } case "card": if content.Text != nil || content.Article != nil || content.Image != nil || len(content.Parts) < 1 || len(content.Parts) > 20 { return nil, false } for _, part := range content.Parts { if !validPart(part) { return nil, false } } default: return nil, false } out, err := json.Marshal(content) return out, err == nil }