// Package notice defines the announcement payload that is published as the // signed static mirror of the API's /v1/notices endpoint (doc/05 §5 应急广播). // // Notices are read by the client's announcement slot and MUST be verified with // the embedded Ed25519 public key before display, exactly like endpoint // documents — they share the sign.Envelope. package notice import ( "fmt" "strings" ) // Valid severity levels. const ( LevelInfo = "info" LevelWarning = "warning" LevelCritical = "critical" ) // Notice is a single bilingual announcement. type Notice struct { ID string `json:"id"` Level string `json:"level"` TitleZH string `json:"title_zh"` TitleEn string `json:"title_en"` BodyZH string `json:"body_zh"` BodyEn string `json:"body_en"` URL string `json:"url,omitempty"` PublishedAt string `json:"published_at"` // RFC3339 UTC } // List is the payload signed into a notices document. type List struct { Notices []Notice `json:"notices"` } func validLevel(l string) bool { switch l { case LevelInfo, LevelWarning, LevelCritical: return true default: return false } } // Validate checks a single notice for completeness and a known severity. func (n Notice) Validate() error { if strings.TrimSpace(n.ID) == "" { return fmt.Errorf("notice: id is required") } if !validLevel(n.Level) { return fmt.Errorf("notice %q: level must be one of info|warning|critical, got %q", n.ID, n.Level) } if strings.TrimSpace(n.TitleZH) == "" || strings.TrimSpace(n.TitleEn) == "" { return fmt.Errorf("notice %q: both title_zh and title_en are required", n.ID) } if strings.TrimSpace(n.PublishedAt) == "" { return fmt.Errorf("notice %q: published_at is required", n.ID) } return nil } // Validate checks the whole list and rejects duplicate IDs. func (l List) Validate() error { seen := map[string]bool{} for _, n := range l.Notices { if err := n.Validate(); err != nil { return err } if seen[n.ID] { return fmt.Errorf("notice: duplicate id %q", n.ID) } seen[n.ID] = true } return nil }