From 3a82e2a0d1ccec9b72995ac5bbdfe7da38e41028 Mon Sep 17 00:00:00 2001 From: Jocadbz Date: Fri, 22 Aug 2025 00:07:13 -0300 Subject: [PATCH] All: Migrate boards from global to board specific Massive commit. Rewrites some of the chat logic to work individually rather than a global chat. --- handlers/board.go | 5 ++ handlers/boards.go | 20 +++++- handlers/chat.go | 110 +++++++++++++++++++++++---------- main.go | 11 ++-- models/board.go | 9 +-- models/chat.go | 15 +++-- models/user.go | 11 +++- templates/pages/boards.html | 13 ++++ templates/pages/chat.html | 28 ++++++--- templates/partials/navbar.html | 1 - 10 files changed, 158 insertions(+), 65 deletions(-) diff --git a/handlers/board.go b/handlers/board.go index 374cf5d..8486949 100644 --- a/handlers/board.go +++ b/handlers/board.go @@ -33,6 +33,11 @@ func BoardHandler(app *App) http.HandlerFunc { return } + if board.Type == "chat" { + http.Redirect(w, r, app.Config.ThreadrDir+"/chat/?id="+boardIDStr, http.StatusFound) + return + } + if board.Private { if !loggedIn { http.Redirect(w, r, app.Config.ThreadrDir+"/login/", http.StatusFound) diff --git a/handlers/boards.go b/handlers/boards.go index 54141c4..99d272f 100644 --- a/handlers/boards.go +++ b/handlers/boards.go @@ -28,25 +28,39 @@ func BoardsHandler(app *App) http.HandlerFunc { if r.Method == http.MethodPost && loggedIn && isAdmin { name := r.FormValue("name") description := r.FormValue("description") + boardType := r.FormValue("type") + if name == "" { http.Error(w, "Board name is required", http.StatusBadRequest) return } + if boardType != "classic" && boardType != "chat" { + boardType = "classic" + } + board := models.Board{ Name: name, Description: description, Private: false, PublicVisible: true, + Type: boardType, } - query := "INSERT INTO boards (name, description, private, public_visible) VALUES (?, ?, ?, ?)" - result, err := app.DB.Exec(query, board.Name, board.Description, board.Private, board.PublicVisible) + query := "INSERT INTO boards (name, description, private, public_visible, type) VALUES (?, ?, ?, ?, ?)" + result, err := app.DB.Exec(query, board.Name, board.Description, board.Private, board.PublicVisible, board.Type) if err != nil { log.Printf("Error creating board: %v", err) http.Error(w, "Failed to create board", http.StatusInternalServerError) return } boardID, _ := result.LastInsertId() - http.Redirect(w, r, app.Config.ThreadrDir+"/board/?id="+strconv.FormatInt(boardID, 10), http.StatusFound) + + var redirectURL string + if boardType == "chat" { + redirectURL = app.Config.ThreadrDir + "/chat/?id=" + strconv.FormatInt(boardID, 10) + } else { + redirectURL = app.Config.ThreadrDir + "/board/?id=" + strconv.FormatInt(boardID, 10) + } + http.Redirect(w, r, redirectURL, http.StatusFound) return } diff --git a/handlers/chat.go b/handlers/chat.go index 9553dba..d9b800f 100644 --- a/handlers/chat.go +++ b/handlers/chat.go @@ -5,6 +5,7 @@ import ( "html/template" "log" "net/http" + "strconv" "sync" "threadr/models" @@ -20,21 +21,26 @@ var upgrader = websocket.Upgrader{ }, } -// ChatHub manages WebSocket connections and broadcasts messages +type Client struct { + conn *websocket.Conn + userID int + boardID int +} + type ChatHub struct { - clients map[*websocket.Conn]int // Map of connections to user IDs - broadcast chan []byte - register chan *websocket.Conn - unregister chan *websocket.Conn + clients map[*Client]bool + broadcast chan models.ChatMessage + register chan *Client + unregister chan *Client mutex sync.Mutex } func NewChatHub() *ChatHub { return &ChatHub{ - clients: make(map[*websocket.Conn]int), - broadcast: make(chan []byte), - register: make(chan *websocket.Conn), - unregister: make(chan *websocket.Conn), + clients: make(map[*Client]bool), + broadcast: make(chan models.ChatMessage), + register: make(chan *Client), + unregister: make(chan *Client), } } @@ -43,21 +49,26 @@ func (h *ChatHub) Run() { select { case client := <-h.register: h.mutex.Lock() - h.clients[client] = 0 // UserID set later + h.clients[client] = true h.mutex.Unlock() case client := <-h.unregister: h.mutex.Lock() - delete(h.clients, client) + if _, ok := h.clients[client]; ok { + delete(h.clients, client) + client.conn.Close() + } h.mutex.Unlock() - client.Close() case message := <-h.broadcast: h.mutex.Lock() for client := range h.clients { - err := client.WriteMessage(websocket.TextMessage, message) - if err != nil { - log.Printf("Error broadcasting message: %v", err) - client.Close() - delete(h.clients, client) + if client.boardID == message.BoardID { + response, _ := json.Marshal(message) + err := client.conn.WriteMessage(websocket.TextMessage, response) + if err != nil { + log.Printf("Error broadcasting message: %v", err) + client.conn.Close() + delete(h.clients, client) + } } } h.mutex.Unlock() @@ -81,20 +92,52 @@ func ChatHandler(app *App) http.HandlerFunc { } cookie, _ := r.Cookie("threadr_cookie_banner") + boardIDStr := r.URL.Query().Get("id") + boardID, err := strconv.Atoi(boardIDStr) + if err != nil { + http.Error(w, "Invalid board ID", http.StatusBadRequest) + return + } + + board, err := models.GetBoardByID(app.DB, boardID) + if err != nil { + log.Printf("Error fetching board: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if board == nil { + http.Error(w, "Chat board not found", http.StatusNotFound) + return + } + if board.Type != "chat" { + http.Error(w, "This is not a chat board", http.StatusBadRequest) + return + } + + if board.Private { + hasPerm, err := models.HasBoardPermission(app.DB, userID, boardID, models.PermViewBoard) + if err != nil { + log.Printf("Error checking permission: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + if !hasPerm { + http.Error(w, "You do not have permission to view this chat", http.StatusForbidden) + return + } + } + if r.URL.Query().Get("ws") == "true" { - // Handle WebSocket connection ws, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("Error upgrading to WebSocket: %v", err) return } - hub.register <- ws - hub.mutex.Lock() - hub.clients[ws] = userID - hub.mutex.Unlock() + client := &Client{conn: ws, userID: userID, boardID: boardID} + hub.register <- client defer func() { - hub.unregister <- ws + hub.unregister <- client }() for { @@ -115,6 +158,7 @@ func ChatHandler(app *App) http.HandlerFunc { if chatMsg.Type == "message" { msgObj := models.ChatMessage{ + BoardID: boardID, UserID: userID, Content: chatMsg.Content, ReplyTo: chatMsg.ReplyTo, @@ -123,7 +167,6 @@ func ChatHandler(app *App) http.HandlerFunc { log.Printf("Error saving chat message: %v", err) continue } - // Fetch the saved message with timestamp and user details var msgID int app.DB.QueryRow("SELECT LAST_INSERT_ID()").Scan(&msgID) savedMsg, err := models.GetChatMessageByID(app.DB, msgID) @@ -131,47 +174,46 @@ func ChatHandler(app *App) http.HandlerFunc { log.Printf("Error fetching saved message: %v", err) continue } - response, _ := json.Marshal(savedMsg) - hub.broadcast <- response + hub.broadcast <- *savedMsg } } return } - // Render chat page - messages, err := models.GetRecentChatMessages(app.DB, 50) + messages, err := models.GetRecentChatMessages(app.DB, boardID, 50) if err != nil { log.Printf("Error fetching chat messages: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - // Reverse messages to show oldest first for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 { messages[i], messages[j] = messages[j], messages[i] } - allUsernames, err := models.GetAllUsernames(app.DB) + allUsernames, err := models.GetUsernamesInBoard(app.DB, boardID) if err != nil { - log.Printf("Error fetching all usernames: %v", err) - allUsernames = []string{} // Proceed without autocomplete on error + log.Printf("Error fetching usernames for board: %v", err) + allUsernames = []string{} } allUsernamesJSON, _ := json.Marshal(allUsernames) data := struct { PageData + Board models.Board Messages []models.ChatMessage AllUsernames template.JS }{ PageData: PageData{ - Title: "ThreadR - Chat", - Navbar: "chat", + Title: "ThreadR Chat - " + board.Name, + Navbar: "boards", LoggedIn: true, ShowCookieBanner: cookie == nil || cookie.Value != "accepted", BasePath: app.Config.ThreadrDir, StaticPath: app.Config.ThreadrDir + "/static", CurrentURL: r.URL.Path, }, + Board: *board, Messages: messages, AllUsernames: template.JS(allUsernamesJSON), } diff --git a/main.go b/main.go index 0fc557c..06ac7eb 100644 --- a/main.go +++ b/main.go @@ -43,13 +43,14 @@ func createTablesIfNotExist(db *sql.DB) error { public_visible BOOLEAN DEFAULT TRUE, pinned_threads TEXT, custom_landing_page TEXT, - color_scheme VARCHAR(255) + color_scheme VARCHAR(255), + type VARCHAR(20) DEFAULT 'classic' NOT NULL )`) if err != nil { return fmt.Errorf("error creating boards table: %v", err) } - // Create threads table (without type field) + // Create threads table _, err = db.Exec(` CREATE TABLE threads ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -170,10 +171,12 @@ func createTablesIfNotExist(db *sql.DB) error { _, err = db.Exec(` CREATE TABLE chat_messages ( id INT AUTO_INCREMENT PRIMARY KEY, + board_id INT NOT NULL, user_id INT NOT NULL, content TEXT NOT NULL, reply_to INT DEFAULT -1, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE )`) if err != nil { return fmt.Errorf("error creating chat_messages table: %v", err) @@ -387,4 +390,4 @@ func main() { log.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", nil)) -} +} \ No newline at end of file diff --git a/models/board.go b/models/board.go index fedbab5..375a35d 100644 --- a/models/board.go +++ b/models/board.go @@ -14,17 +14,18 @@ type Board struct { PinnedThreads []int // Stored as JSON CustomLandingPage string ColorScheme string + Type string } func GetBoardByID(db *sql.DB, id int) (*Board, error) { - query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme FROM boards WHERE id = ?" + query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme, type FROM boards WHERE id = ?" row := db.QueryRow(query, id) board := &Board{} var pinnedThreadsJSON sql.NullString var customLandingPage sql.NullString var colorScheme sql.NullString var description sql.NullString - err := row.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme) + err := row.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme, &board.Type) if err == sql.ErrNoRows { return nil, nil } @@ -56,7 +57,7 @@ func GetBoardByID(db *sql.DB, id int) (*Board, error) { } func GetAllBoards(db *sql.DB, private bool) ([]Board, error) { - query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme FROM boards WHERE private = ? ORDER BY id ASC" + query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme, type FROM boards WHERE private = ? ORDER BY id ASC" rows, err := db.Query(query, private) if err != nil { return nil, err @@ -70,7 +71,7 @@ func GetAllBoards(db *sql.DB, private bool) ([]Board, error) { var customLandingPage sql.NullString var colorScheme sql.NullString var description sql.NullString - err := rows.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme) + err := rows.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme, &board.Type) if err != nil { return nil, err } diff --git a/models/chat.go b/models/chat.go index 3138e5a..8bcdb8f 100644 --- a/models/chat.go +++ b/models/chat.go @@ -8,6 +8,7 @@ import ( type ChatMessage struct { ID int `json:"id"` + BoardID int `json:"boardId"` UserID int `json:"userId"` Content string `json:"content"` ReplyTo int `json:"replyTo"` @@ -18,19 +19,20 @@ type ChatMessage struct { } func CreateChatMessage(db *sql.DB, msg ChatMessage) error { - query := "INSERT INTO chat_messages (user_id, content, reply_to, timestamp) VALUES (?, ?, ?, NOW())" - _, err := db.Exec(query, msg.UserID, msg.Content, msg.ReplyTo) + query := "INSERT INTO chat_messages (board_id, user_id, content, reply_to, timestamp) VALUES (?, ?, ?, ?, NOW())" + _, err := db.Exec(query, msg.BoardID, msg.UserID, msg.Content, msg.ReplyTo) return err } -func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { +func GetRecentChatMessages(db *sql.DB, boardID int, limit int) ([]ChatMessage, error) { query := ` SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id FROM chat_messages cm JOIN users u ON cm.user_id = u.id + WHERE cm.board_id = ? ORDER BY cm.timestamp DESC LIMIT ?` - rows, err := db.Query(query, limit) + rows, err := db.Query(query, boardID, limit) if err != nil { return nil, err } @@ -44,6 +46,7 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { if err != nil { return nil, err } + msg.BoardID = boardID msg.Timestamp, err = time.Parse("2006-01-02 15:04:05", timestampStr) if err != nil { msg.Timestamp = time.Time{} @@ -56,14 +59,14 @@ func GetRecentChatMessages(db *sql.DB, limit int) ([]ChatMessage, error) { func GetChatMessageByID(db *sql.DB, id int) (*ChatMessage, error) { query := ` - SELECT cm.id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id + SELECT cm.id, cm.board_id, cm.user_id, cm.content, cm.reply_to, cm.timestamp, u.username, u.pfp_file_id FROM chat_messages cm JOIN users u ON cm.user_id = u.id WHERE cm.id = ?` row := db.QueryRow(query, id) var msg ChatMessage var timestampStr string - err := row.Scan(&msg.ID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID) + err := row.Scan(&msg.ID, &msg.BoardID, &msg.UserID, &msg.Content, &msg.ReplyTo, ×tampStr, &msg.Username, &msg.PfpFileID) if err == sql.ErrNoRows { return nil, nil } diff --git a/models/user.go b/models/user.go index 8016a05..281867c 100644 --- a/models/user.go +++ b/models/user.go @@ -154,9 +154,14 @@ func HasGlobalPermission(user *User, perm int64) bool { return user.Permissions&perm != 0 } -func GetAllUsernames(db *sql.DB) ([]string, error) { - query := "SELECT username FROM users ORDER BY username ASC" - rows, err := db.Query(query) +func GetUsernamesInBoard(db *sql.DB, boardID int) ([]string, error) { + query := ` + SELECT DISTINCT u.username + FROM users u + JOIN chat_messages cm ON u.id = cm.user_id + WHERE cm.board_id = ? + ORDER BY u.username ASC` + rows, err := db.Query(query, boardID) if err != nil { return nil, err } diff --git a/templates/pages/boards.html b/templates/pages/boards.html index d4350e6..bbeaf0e 100644 --- a/templates/pages/boards.html +++ b/templates/pages/boards.html @@ -17,7 +17,11 @@