package main import ( "fmt" "image" "image/gif" "image/jpeg" _ "image/png" "math/rand/v2" "mime/multipart" "net/http" "os" "strconv" "strings" "time" "github.com/go-chi/chi/v5" xdraw "golang.org/x/image/draw" ) type Entry struct { PID int64 `json:"pid"` UID int64 `json:"uid"` CreatedAt int64 `json:"created_at"` Content string `json:"content"` Filepath string `json:"filepath"` Username string `json:"username"` } func handleFeed(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(chi.URLParam(r, "page")) if page < 0 { page = 0 } if page > 100 { page = 100 } const count = 20 offset := page * count rows, err := db.Query(` SELECT e.pid, e.uid, e.created_at, e.content, e.filepath, u.username FROM entry e JOIN user u ON u.uid = e.uid ORDER BY e.created_at DESC LIMIT ? OFFSET ?`, count, offset) if err != nil { writeError(w, http.StatusInternalServerError, "Datenbankfehler") return } defer rows.Close() entries := []Entry{} for rows.Next() { var e Entry if err := rows.Scan(&e.PID, &e.UID, &e.CreatedAt, &e.Content, &e.Filepath, &e.Username); err != nil { continue } entries = append(entries, e) } writeJSON(w, http.StatusOK, entries) } func handleCreateEntry(w http.ResponseWriter, r *http.Request) { uid := uidFromContext(r.Context()) if err := r.ParseMultipartForm(16 << 20); err != nil && err != http.ErrNotMultipart { writeError(w, http.StatusBadRequest, "Ungültige Anfrage") return } content := strings.TrimSpace(r.FormValue("content")) if len(content) > 1000 { content = content[:1000] } filepath := "none" if r.MultipartForm != nil { if files := r.MultipartForm.File["file"]; len(files) > 0 { if stored, err := storeImage(files[0]); err == nil && stored != "" { filepath = stored } } } if filepath == "none" && content == "" { writeError(w, http.StatusBadRequest, "Leerer Beitrag!") return } pid := int64(rand.IntN(999999999999)) now := time.Now().Unix() if _, err := db.Exec( `INSERT INTO entry (pid, uid, created_at, content, filepath) VALUES (?, ?, ?, ?, ?)`, pid, uid, now, content, filepath, ); err != nil { writeError(w, http.StatusInternalServerError, "Beitrag konnte nicht gespeichert werden") return } writeJSON(w, http.StatusCreated, map[string]any{ "pid": pid, "content": content, "filepath": filepath, }) } func handleInteractions(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusNotImplemented, "Voting ist noch nicht implementiert") } // storeImage skaliert ein hochgeladenes Bild auf max. 1024px und speichert es unter static/media. // Hinweis: animierte GIFs werden derzeit auf das erste Frame reduziert. func storeImage(fh *multipart.FileHeader) (string, error) { const maxSize = 1024 src, err := fh.Open() if err != nil { return "", err } defer src.Close() img, format, err := image.Decode(src) if err != nil { return "", err } b := img.Bounds() width, height := b.Dx(), b.Dy() nw, nh := width, height if width > maxSize || height > maxSize { if width > height { nw, nh = maxSize, height*maxSize/width } else { nw, nh = width*maxSize/height, maxSize } } ext := "jpg" if format == "gif" { ext = "gif" } if err := os.MkdirAll("static/media", 0o755); err != nil { return "", err } name := fmt.Sprintf("static/media/%d-%d.%s", time.Now().Unix(), rand.IntN(999999), ext) dst := image.NewRGBA(image.Rect(0, 0, nw, nh)) xdraw.CatmullRom.Scale(dst, dst.Bounds(), img, b, xdraw.Over, nil) out, err := os.Create(name) if err != nil { return "", err } defer out.Close() if ext == "gif" { err = gif.Encode(out, dst, nil) } else { err = jpeg.Encode(out, dst, &jpeg.Options{Quality: 85}) } if err != nil { os.Remove(name) return "", err } return name, nil }