This commit is contained in:
@@ -23,6 +23,19 @@ type postLocalePayload struct {
|
|||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// requestLangCode is the normalized locale from ?lang= or Accept-Language (zh, en, ja, …).
|
||||||
|
func requestLangCode(r *http.Request) string {
|
||||||
|
raw := strings.TrimSpace(r.URL.Query().Get("lang"))
|
||||||
|
if raw == "" {
|
||||||
|
raw = r.Header.Get("Accept-Language")
|
||||||
|
}
|
||||||
|
raw = strings.TrimSpace(strings.Split(raw, ",")[0])
|
||||||
|
if raw == "" {
|
||||||
|
return "zh"
|
||||||
|
}
|
||||||
|
return translateNormalizePostLang(raw)
|
||||||
|
}
|
||||||
|
|
||||||
func (t postTextI18n) pick(r *http.Request) string {
|
func (t postTextI18n) pick(r *http.Request) string {
|
||||||
return pickLangField(r, t.TextZh, t.TextEn, t.TextJa, t.TextKo, t.TextVi, t.TextId, t.TextMs)
|
return pickLangField(r, t.TextZh, t.TextEn, t.TextJa, t.TextKo, t.TextVi, t.TextId, t.TextMs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Valid post_type values (Browse filter chips + admin selector).
|
// Valid post_type values (Browse filter chips; inferred on admin save).
|
||||||
var validPostTypes = map[string]bool{
|
var validPostTypes = map[string]bool{
|
||||||
"image": true, "video": true, "music": true, "ppt": true, "pdf": true,
|
"image": true, "video": true, "music": true, "ppt": true, "pdf": true,
|
||||||
"link": true, "text": true, "archive": true,
|
"link": true, "text": true, "archive": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var postTextURLPattern = regexp.MustCompile(`https?://`)
|
||||||
|
|
||||||
func normalizePostType(raw string) string {
|
func normalizePostType(raw string) string {
|
||||||
s := strings.ToLower(strings.TrimSpace(raw))
|
s := strings.ToLower(strings.TrimSpace(raw))
|
||||||
switch s {
|
switch s {
|
||||||
@@ -33,3 +38,84 @@ func normalizePostType(raw string) string {
|
|||||||
}
|
}
|
||||||
return "text"
|
return "text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inferPostTypeFromContent picks post_type from attachments and text (admin no longer sends postType).
|
||||||
|
func inferPostTypeFromContent(text string, attachments []attachmentInput) string {
|
||||||
|
hasText := strings.TrimSpace(text) != ""
|
||||||
|
if len(attachments) == 0 {
|
||||||
|
if hasText && postTextURLPattern.MatchString(text) {
|
||||||
|
return "link"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
var image, video, music, ppt, pdf, archive bool
|
||||||
|
for _, a := range attachments {
|
||||||
|
img, vid, mus, p, pd, ar := attachmentPostTypeSignals(a)
|
||||||
|
image = image || img
|
||||||
|
video = video || vid
|
||||||
|
music = music || mus
|
||||||
|
ppt = ppt || p
|
||||||
|
pdf = pdf || pd
|
||||||
|
archive = archive || ar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific file types first; image beats video when both exist (e.g. multi-photo posts).
|
||||||
|
switch {
|
||||||
|
case ppt:
|
||||||
|
return "ppt"
|
||||||
|
case pdf && !image && !video:
|
||||||
|
return "pdf"
|
||||||
|
case archive && !image && !video && !music && !ppt && !pdf:
|
||||||
|
return "archive"
|
||||||
|
case music && !image && !video:
|
||||||
|
return "music"
|
||||||
|
case image:
|
||||||
|
return "image"
|
||||||
|
case video:
|
||||||
|
return "video"
|
||||||
|
case pdf:
|
||||||
|
return "pdf"
|
||||||
|
case archive:
|
||||||
|
return "archive"
|
||||||
|
case music:
|
||||||
|
return "music"
|
||||||
|
}
|
||||||
|
if hasText && postTextURLPattern.MatchString(text) {
|
||||||
|
return "link"
|
||||||
|
}
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachmentPostTypeSignals(a attachmentInput) (image, video, music, ppt, pdf, archive bool) {
|
||||||
|
kind := strings.ToLower(strings.TrimSpace(a.Kind))
|
||||||
|
mime := strings.ToLower(strings.TrimSpace(a.Mime))
|
||||||
|
fn := strings.ToLower(strings.TrimSpace(a.Filename))
|
||||||
|
if kind == "" && mime != "" {
|
||||||
|
kind = classifyAttachmentKind(mime, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == "video" || strings.HasPrefix(mime, "video/") {
|
||||||
|
video = true
|
||||||
|
}
|
||||||
|
if kind == "image" || strings.HasPrefix(mime, "image/") {
|
||||||
|
image = true
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(mime, "audio/") ||
|
||||||
|
strings.HasSuffix(fn, ".mp3") || strings.HasSuffix(fn, ".wav") ||
|
||||||
|
strings.HasSuffix(fn, ".m4a") || strings.HasSuffix(fn, ".flac") || strings.HasSuffix(fn, ".aac") {
|
||||||
|
music = true
|
||||||
|
}
|
||||||
|
if strings.Contains(mime, "pdf") || strings.HasSuffix(fn, ".pdf") {
|
||||||
|
pdf = true
|
||||||
|
}
|
||||||
|
if strings.Contains(mime, "presentation") ||
|
||||||
|
strings.HasSuffix(fn, ".ppt") || strings.HasSuffix(fn, ".pptx") {
|
||||||
|
ppt = true
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(fn, ".zip") || strings.HasSuffix(fn, ".rar") ||
|
||||||
|
strings.HasSuffix(fn, ".7z") || strings.Contains(mime, "zip") {
|
||||||
|
archive = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ type PostDTO struct {
|
|||||||
PostType string `json:"postType"`
|
PostType string `json:"postType"`
|
||||||
CategoryID int `json:"categoryId,omitempty"`
|
CategoryID int `json:"categoryId,omitempty"`
|
||||||
CategorySlug string `json:"categorySlug,omitempty"`
|
CategorySlug string `json:"categorySlug,omitempty"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"` // UI locale from ?lang= (matches text selection)
|
||||||
|
SourceLanguage string `json:"sourceLanguage,omitempty"` // DB source metadata (admin input language)
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
Localizations map[string]postLocalePayload `json:"localizations"`
|
Localizations map[string]postLocalePayload `json:"localizations"`
|
||||||
Attachments []AttachmentDTO `json:"attachments"`
|
Attachments []AttachmentDTO `json:"attachments"`
|
||||||
@@ -212,9 +213,9 @@ func translateNormalizePostLang(lang string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateAdminPost(ap *adminPost) error {
|
func validateAdminPost(ap *adminPost) error {
|
||||||
ap.PostType = normalizePostType(ap.PostType)
|
ap.PostType = inferPostTypeFromContent(ap.Text, ap.Attachments)
|
||||||
if ap.PostType == "" || !validPostTypes[ap.PostType] {
|
if !validPostTypes[ap.PostType] {
|
||||||
return fmt.Errorf("postType required (image, video, music, ppt, pdf, link, text, archive)")
|
ap.PostType = "text"
|
||||||
}
|
}
|
||||||
if len(ap.Attachments) > maxPostAttachments {
|
if len(ap.Attachments) > maxPostAttachments {
|
||||||
return fmt.Errorf("too many attachments (max %d)", maxPostAttachments)
|
return fmt.Errorf("too many attachments (max %d)", maxPostAttachments)
|
||||||
@@ -389,11 +390,11 @@ func scanPostRow(r *http.Request, row pgx.Row) (PostDTO, postTextI18n, error) {
|
|||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
var catID *int
|
var catID *int
|
||||||
var slug *string
|
var slug *string
|
||||||
var lang, postType string
|
var sourceLang, postType string
|
||||||
var pub, updated, created *time.Time
|
var pub, updated, created *time.Time
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&id, &texts.TextZh, &texts.TextEn, &texts.TextJa, &texts.TextKo, &texts.TextVi, &texts.TextId, &texts.TextMs,
|
&id, &texts.TextZh, &texts.TextEn, &texts.TextJa, &texts.TextKo, &texts.TextVi, &texts.TextId, &texts.TextMs,
|
||||||
&lang, &postType, &catID, &slug, &dto.IsRecommended, &pub, &updated, &created,
|
&sourceLang, &postType, &catID, &slug, &dto.IsRecommended, &pub, &updated, &created,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dto, texts, err
|
return dto, texts, err
|
||||||
@@ -406,7 +407,8 @@ func scanPostRow(r *http.Request, row pgx.Row) (PostDTO, postTextI18n, error) {
|
|||||||
if slug != nil {
|
if slug != nil {
|
||||||
dto.CategorySlug = *slug
|
dto.CategorySlug = *slug
|
||||||
}
|
}
|
||||||
dto.Language = lang
|
dto.SourceLanguage = sourceLang
|
||||||
|
dto.Language = requestLangCode(r)
|
||||||
dto.Text = texts.pick(r)
|
dto.Text = texts.pick(r)
|
||||||
dto.Localizations = texts.toLocalizations()
|
dto.Localizations = texts.toLocalizations()
|
||||||
if pub != nil {
|
if pub != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user