232 lines
6.4 KiB
Go
232 lines
6.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
func ListPosts(w http.ResponseWriter, r *http.Request) {
|
|
listPostsQuery(w, r, false)
|
|
}
|
|
|
|
func ListPostsRecommended(w http.ResponseWriter, r *http.Request) {
|
|
pool := poolFrom(r)
|
|
limit := postLimitDef(r, 12, 50)
|
|
rows, err := pool.Query(r.Context(), postSelectBase()+`
|
|
WHERE `+publicPostWhere+` AND p.is_recommended = TRUE
|
|
ORDER BY p.sort_order ASC, p.published_at DESC NULLS LAST, p.id DESC
|
|
LIMIT $1`, limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
items, err := collectPostRows(r, rows)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"items": items})
|
|
}
|
|
|
|
func ListPostsLatest(w http.ResponseWriter, r *http.Request) {
|
|
pool := poolFrom(r)
|
|
limit := postLimitDef(r, 8, 50)
|
|
rows, err := pool.Query(r.Context(), postSelectBase()+`
|
|
WHERE `+publicPostWhere+`
|
|
ORDER BY p.published_at DESC NULLS LAST, p.id DESC
|
|
LIMIT $1`, limit)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
items, err := collectPostRows(r, rows)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"items": items})
|
|
}
|
|
|
|
func SearchPosts(w http.ResponseWriter, r *http.Request) {
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
if q == "" {
|
|
writeJSON(w, PostListResponse{Items: []PostDTO{}})
|
|
return
|
|
}
|
|
listPostsQuery(w, r, true)
|
|
}
|
|
|
|
func GetPost(w http.ResponseWriter, r *http.Request) {
|
|
pool := poolFrom(r)
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
http.Error(w, "bad id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
row := pool.QueryRow(r.Context(), postSelectBase()+`
|
|
WHERE p.id = $1 AND `+publicPostWhere, id)
|
|
dto, _, err := scanPostRow(r, row)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
atts, _ := loadAttachmentsByPostIDs(r.Context(), pool, []uuid.UUID{id})
|
|
dto.Attachments = atts[id]
|
|
if dto.Attachments == nil {
|
|
dto.Attachments = []AttachmentDTO{}
|
|
}
|
|
tags, _ := loadPostTagNames(r.Context(), pool, id, requestLangCode(r))
|
|
if tags == nil {
|
|
tags = []string{}
|
|
}
|
|
dto.Tags = tags
|
|
writeJSON(w, dto)
|
|
}
|
|
|
|
func PostAttachmentDownload(w http.ResponseWriter, r *http.Request) {
|
|
pool := poolFrom(r)
|
|
postID, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
http.Error(w, "bad id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
aid, err := uuid.Parse(chi.URLParam(r, "aid"))
|
|
if err != nil {
|
|
http.Error(w, "bad attachment id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
cmd, err := pool.Exec(r.Context(), `
|
|
UPDATE posts SET download_count = download_count + 1, updated_at = NOW()
|
|
WHERE id = $1 AND status = 'published' AND is_public = TRUE
|
|
AND (published_at IS NULL OR published_at <= NOW())`, postID)
|
|
if err != nil || cmd.RowsAffected() == 0 {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]any{"ok": true})
|
|
_ = aid
|
|
}
|
|
|
|
func listPostsQuery(w http.ResponseWriter, r *http.Request, searchMode bool) {
|
|
pool := poolFrom(r)
|
|
limit := postLimitDef(r, 20, 50)
|
|
cursor := strings.TrimSpace(r.URL.Query().Get("cursor"))
|
|
catSlug := strings.TrimSpace(r.URL.Query().Get("category"))
|
|
typ := strings.TrimSpace(r.URL.Query().Get("type"))
|
|
if typ == "" {
|
|
typ = "all"
|
|
}
|
|
langFilter := strings.TrimSpace(r.URL.Query().Get("language"))
|
|
|
|
base := postSelectBase() + ` WHERE ` + publicPostWhere
|
|
args := []any{}
|
|
cond := postTypeFilterSQL(typ, &args)
|
|
cond += postLanguageFilterSQL(langFilter, &args)
|
|
if catSlug != "" {
|
|
args = append(args, catSlug)
|
|
cond += fmt.Sprintf(` AND c.slug = $%d`, len(args))
|
|
}
|
|
if searchMode {
|
|
q := strings.TrimSpace(r.URL.Query().Get("q"))
|
|
if q != "" {
|
|
pat := "%" + q + "%"
|
|
args = append(args, pat)
|
|
n := len(args)
|
|
cond += fmt.Sprintf(` AND (
|
|
p.text_zh ILIKE $%d OR p.text_en ILIKE $%d OR p.text_ja ILIKE $%d OR p.text_ko ILIKE $%d OR
|
|
p.text_vi ILIKE $%d OR p.text_id ILIKE $%d OR p.text_ms ILIKE $%d OR
|
|
EXISTS (SELECT 1 FROM post_attachments pa WHERE pa.post_id = p.id AND pa.filename ILIKE $%d) OR
|
|
EXISTS (SELECT 1 FROM post_tags pt JOIN tags t ON t.id = pt.tag_id WHERE pt.post_id = p.id AND (
|
|
t.name ILIKE $%d OR t.name_en ILIKE $%d OR t.name_ja ILIKE $%d OR t.name_ko ILIKE $%d OR
|
|
t.name_vi ILIKE $%d OR t.name_id ILIKE $%d OR t.name_ms ILIKE $%d)))`,
|
|
n, n, n, n, n, n, n, n, n, n, n, n, n, n, n)
|
|
}
|
|
}
|
|
order := ` ORDER BY p.published_at DESC NULLS LAST, p.id DESC`
|
|
if cursor != "" {
|
|
pubT, cid, err := decodePostCursor(cursor)
|
|
if err != nil {
|
|
http.Error(w, "bad cursor", http.StatusBadRequest)
|
|
return
|
|
}
|
|
args = append(args, pubT, cid)
|
|
n := len(args)
|
|
cond += fmt.Sprintf(` AND (p.published_at, p.id) < ($%d::timestamptz, $%d::uuid)`, n-1, n)
|
|
}
|
|
args = append(args, limit+1)
|
|
limitArg := len(args)
|
|
q := base + cond + order + fmt.Sprintf(` LIMIT $%d`, limitArg)
|
|
|
|
rows, err := pool.Query(r.Context(), q, args...)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
items, err := collectPostRows(r, rows)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
var nextCursor string
|
|
if len(items) > limit {
|
|
last := items[limit]
|
|
items = items[:limit]
|
|
if t, err := time.Parse(time.RFC3339, last.PublishedAt); err == nil {
|
|
uid, _ := uuid.Parse(last.ID)
|
|
nextCursor = encodePostCursor(t, uid)
|
|
}
|
|
}
|
|
writeJSON(w, PostListResponse{Items: items, NextCursor: nextCursor})
|
|
}
|
|
|
|
func collectPostRows(r *http.Request, rows pgx.Rows) ([]PostDTO, error) {
|
|
defer rows.Close()
|
|
var ids []uuid.UUID
|
|
list := make([]PostDTO, 0)
|
|
for rows.Next() {
|
|
dto, _, err := scanPostRow(r, rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uid, _ := uuid.Parse(dto.ID)
|
|
ids = append(ids, uid)
|
|
dto.Attachments = []AttachmentDTO{}
|
|
dto.Tags = []string{}
|
|
list = append(list, dto)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
atts, err := loadAttachmentsByPostIDs(r.Context(), poolFrom(r), ids)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
lang := requestLangCode(r)
|
|
tagsByPost, err := loadPostTagNamesByPostIDs(r.Context(), poolFrom(r), ids, lang)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range list {
|
|
uid, _ := uuid.Parse(list[i].ID)
|
|
if a, ok := atts[uid]; ok {
|
|
list[i].Attachments = a
|
|
}
|
|
if t, ok := tagsByPost[uid]; ok {
|
|
list[i].Tags = t
|
|
} else {
|
|
list[i].Tags = []string{}
|
|
}
|
|
}
|
|
return list, nil
|
|
}
|