benzcloud-plugin-chat/main.go

180 lines
4.5 KiB
Go

package main
import (
"embed"
"encoding/json"
"flag"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/benzjeremy/benzcloud-plugin-chat/internal/chat"
)
//go:embed web/*
var webFS embed.FS
func main() {
port := flag.Int("port", 8093, "HTTP & WebSocket server port")
dataDir := flag.String("data", "./data", "Directory for chat history")
baseDomain := flag.String("domain", "benzcloud.local", "Base domain for chat")
flag.Parse()
log.Printf("[BenzCloud Chat] Initializing Chat Plugin v1.0 on %s (Port %d)\n", *baseDomain, *port)
store, err := chat.NewStore(*dataDir)
if err != nil {
log.Fatalf("Failed to initialize chat store: %v", err)
}
// Seed welcome message if #general is empty
genMsgs := store.GetMessages("general", 1)
if len(genMsgs) == 0 {
_ = store.SaveMessage(&chat.Message{
Channel: "general",
Sender: "BenzCloud Bot",
Content: "Welcome to your private, decentralized Mesh Chat! Real-time messaging with end-to-end local sovereignty.",
Type: chat.TypeMessage,
Timestamp: time.Now().UTC(),
})
}
hub := chat.NewHub(store)
go hub.Run()
mux := http.NewServeMux()
// Health endpoint for BenzCloud Supervisor / Router
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"status": "ok",
"plugin": "chat",
"version": "v1.0",
"subdomain": "chat",
"port": *port,
})
})
// WebSocket handler
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
user := r.URL.Query().Get("user")
if user == "" {
user = "anonymous"
}
hub.ServeWS(w, r, user)
})
// API: Channels
mux.HandleFunc("/api/chat/channels", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
channels := store.GetChannels()
_ = json.NewEncoder(w).Encode(channels)
return
}
if r.Method == http.MethodPost {
var body struct {
Name string `json:"name"`
Topic string `json:"topic"`
CreatedBy string `json:"created_by"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ch, err := store.CreateChannel(body.Name, body.Topic, body.CreatedBy)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
_ = json.NewEncoder(w).Encode(ch)
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
})
// API: Messages
mux.HandleFunc("/api/chat/messages", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet {
ch := r.URL.Query().Get("channel")
if ch == "" {
ch = "general"
}
limit := 50
if lStr := r.URL.Query().Get("limit"); lStr != "" {
if l, err := strconv.Atoi(lStr); err == nil && l > 0 {
limit = l
}
}
msgs := store.GetMessages(ch, limit)
_ = json.NewEncoder(w).Encode(msgs)
return
}
if r.Method == http.MethodPost {
var msg chat.Message
if err := json.NewDecoder(r.Body).Decode(&msg); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if msg.Channel == "" {
msg.Channel = "general"
}
if msg.Sender == "" {
msg.Sender = "anonymous"
}
if msg.Timestamp.IsZero() {
msg.Timestamp = time.Now().UTC()
}
hub.Broadcast(&msg)
_ = json.NewEncoder(w).Encode(msg)
return
}
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
})
// Static Web Chat Files
subWeb, err := fs.Sub(webFS, "web")
if err != nil {
log.Fatalf("Failed to create sub filesystem: %v", err)
}
fileServer := http.FileServer(http.FS(subWeb))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/api/") && r.URL.Path != "/ws" && r.URL.Path != "/health" {
fileServer.ServeHTTP(w, r)
return
}
})
server := &http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", *port),
Handler: mux,
}
go func() {
log.Printf("[BenzCloud Chat] Web Chat & WebSocket listening on http://0.0.0.0:%d\n", *port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Chat HTTP server error: %v", err)
}
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("[BenzCloud Chat] Shutting down gracefully...")
_ = server.Close()
}