130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
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
|
|
}
|