c5958f283f
- Astro 5.x static site with content collections for extensions - Pages: index, [slug] (per-extension), coffee (Ko-fi), feedback form - Go binary gs-feedback: SQLite storage + Discord webhook notification - Nginx config: static serving + POST proxy to Go backend - Deploy script: build + rsync to VPS - ThockBoard landing page with features and sound pack listing
221 lines
5.2 KiB
Go
221 lines
5.2 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type FeedbackRequest struct {
|
|
Ext string `json:"ext"`
|
|
Reason string `json:"reason"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
type FeedbackResponse struct {
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
func main() {
|
|
dbPath := os.Getenv("GS_DB_PATH")
|
|
if dbPath == "" {
|
|
dbPath = "/var/lib/gs-feedback/feedback.db"
|
|
}
|
|
|
|
discordWebhook := os.Getenv("GS_DISCORD_WEBHOOK")
|
|
port := os.Getenv("GS_PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
// Ensure directory exists
|
|
if err := os.MkdirAll(strings.TrimSuffix(dbPath, "/feedback.db"), 0755); err != nil {
|
|
log.Printf("warn: could not create db directory: %v", err)
|
|
}
|
|
|
|
db, err := sql.Open("sqlite", dbPath)
|
|
if err != nil {
|
|
log.Fatalf("failed to open database: %v", err)
|
|
}
|
|
defer db.Close()
|
|
|
|
// Create table if not exists
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS feedback (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ext TEXT NOT NULL,
|
|
reason TEXT DEFAULT '',
|
|
text TEXT DEFAULT '',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);`
|
|
if _, err := db.Exec(schema); err != nil {
|
|
log.Fatalf("failed to create table: %v", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// POST /feedback — receive feedback
|
|
mux.HandleFunc("/feedback", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == "OPTIONS" {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
if r.Method != "POST" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// CORS
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
var req FeedbackRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Validate
|
|
req.Ext = strings.TrimSpace(req.Ext)
|
|
req.Reason = strings.TrimSpace(req.Reason)
|
|
req.Text = strings.TrimSpace(req.Text)
|
|
|
|
if len(req.Text) > 500 {
|
|
http.Error(w, "Text too long", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Insert into SQLite
|
|
_, err := db.Exec(
|
|
"INSERT INTO feedback (ext, reason, text) VALUES (?, ?, ?)",
|
|
req.Ext, req.Reason, req.Text,
|
|
)
|
|
if err != nil {
|
|
log.Printf("error inserting feedback: %v", err)
|
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Printf("feedback received: ext=%s reason=%s", req.Ext, req.Reason)
|
|
|
|
// Discord webhook (best-effort, non-blocking)
|
|
if discordWebhook != "" {
|
|
go sendDiscordNotification(discordWebhook, req)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(FeedbackResponse{OK: true})
|
|
})
|
|
|
|
// GET /feedback/stats — simple stats endpoint (optional, for future dashboard)
|
|
mux.HandleFunc("/feedback/stats", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
rows, err := db.Query(`
|
|
SELECT ext, reason, COUNT(*) as count
|
|
FROM feedback
|
|
GROUP BY ext, reason
|
|
ORDER BY count DESC
|
|
`)
|
|
if err != nil {
|
|
http.Error(w, "Internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
type Stat struct {
|
|
Ext string `json:"ext"`
|
|
Reason string `json:"reason"`
|
|
Count int `json:"count"`
|
|
}
|
|
var stats []Stat
|
|
for rows.Next() {
|
|
var s Stat
|
|
if err := rows.Scan(&s.Ext, &s.Reason, &s.Count); err != nil {
|
|
continue
|
|
}
|
|
stats = append(stats, s)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(stats)
|
|
})
|
|
|
|
// Health check
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprint(w, "ok")
|
|
})
|
|
|
|
log.Printf("gs-feedback starting on :%s (db=%s)", port, dbPath)
|
|
srv := &http.Server{
|
|
Addr: ":" + port,
|
|
Handler: mux,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
}
|
|
if err := srv.ListenAndServe(); err != nil {
|
|
log.Fatalf("server error: %v", err)
|
|
}
|
|
}
|
|
|
|
func sendDiscordNotification(webhookURL string, req FeedbackRequest) {
|
|
reasonLabels := map[string]string{
|
|
"not-needed": "Didn't need it",
|
|
"too-many": "Too many extensions",
|
|
"bugs": "Bugs or issues",
|
|
"alternative": "Prefer an alternative",
|
|
"other": "Other",
|
|
}
|
|
reasonLabel := reasonLabels[req.Reason]
|
|
if reasonLabel == "" {
|
|
reasonLabel = req.Reason
|
|
}
|
|
|
|
extLabel := req.Ext
|
|
if extLabel == "" {
|
|
extLabel = "unknown"
|
|
}
|
|
|
|
textPreview := req.Text
|
|
if len(textPreview) > 200 {
|
|
textPreview = textPreview[:200] + "..."
|
|
}
|
|
|
|
description := fmt.Sprintf("**Extension:** %s\n**Reason:** %s", extLabel, reasonLabel)
|
|
if textPreview != "" {
|
|
description += fmt.Sprintf("\n**Comment:** %s", textPreview)
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"embeds": []map[string]interface{}{
|
|
{
|
|
"title": "📋 Uninstall Feedback",
|
|
"description": description,
|
|
"color": 0xf97316,
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(payload)
|
|
resp, err := http.Post(webhookURL, "application/json", strings.NewReader(string(body)))
|
|
if err != nil {
|
|
log.Printf("discord webhook error: %v", err)
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
} |