Files
dumpsterChat/internal/channel/threads.go
T

194 lines
6.3 KiB
Go

package channel
import (
"database/sql"
"encoding/json"
"net/http"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
"github.com/go-chi/chi/v5"
)
type createThreadRequest struct {
Name string `json:"name"`
MessageID *string `json:"message_id,omitempty"`
}
type threadResponse struct {
ID string `json:"id"`
ServerID string `json:"server_id"`
ParentChannelID string `json:"parent_channel_id"`
Name string `json:"name"`
Type string `json:"type"`
Category *string `json:"category"`
Position int `json:"position"`
ArchivedAt *string `json:"archived_at"`
AutoArchiveDuration int `json:"auto_archive_duration"`
MessageCount int `json:"message_count"`
LastMessageAt *string `json:"last_message_at"`
CreatedAt string `json:"created_at"`
}
func (h *Handler) registerThreadRoutes(r chi.Router) {
r.Post("/{channelID}/threads", h.CreateThread)
r.Get("/{channelID}/threads", h.ListThreads)
r.Patch("/threads/{threadID}", h.UpdateThread)
}
func scanThread(row interface{ Scan(dest ...any) error }) (threadResponse, error) {
var t threadResponse
var archivedAt, lastMsgAt, category sql.NullString
err := row.Scan(&t.ID, &t.ServerID, &t.ParentChannelID, &t.Name, &t.Type, &category, &t.Position, &archivedAt, &t.AutoArchiveDuration, &t.MessageCount, &lastMsgAt, &t.CreatedAt)
if err != nil {
return t, err
}
if archivedAt.Valid {
t.ArchivedAt = &archivedAt.String
}
if lastMsgAt.Valid {
t.LastMessageAt = &lastMsgAt.String
}
if category.Valid {
t.Category = &category.String
}
return t, nil
}
func (h *Handler) CreateThread(w http.ResponseWriter, r *http.Request) {
parentID := chi.URLParam(r, "channelID")
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
serverID, err := h.serverIDForChannel(r.Context(), parentID)
if err != nil {
http.Error(w, `{"error":"channel not found"}`, http.StatusNotFound)
return
}
member, err := h.isMember(r.Context(), userID, serverID)
if err != nil || !member {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
allowed, err := h.checker.CheckPermission(r.Context(), serverID, userID, permissions.SEND_MESSAGES)
if err != nil || !allowed {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
var req createThreadRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
// ponytail: reuse channels table with parent_channel_id set. Messages in the thread channel are normal messages.
thread, err := scanThread(h.db.QueryRowContext(r.Context(), `
INSERT INTO channels (server_id, name, type, category, position, parent_channel_id, auto_archive_duration)
VALUES ($1, $2, 'thread', 'threads', 0, $3, 1440)
RETURNING id, server_id, parent_channel_id, name, type, category, position, archived_at, auto_archive_duration, message_count, last_message_at, created_at
`, serverID, req.Name, parentID))
if err != nil {
http.Error(w, `{"error":"failed to create thread"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(thread)
}
func (h *Handler) ListThreads(w http.ResponseWriter, r *http.Request) {
parentID := chi.URLParam(r, "channelID")
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
serverID, err := h.serverIDForChannel(r.Context(), parentID)
if err != nil {
http.Error(w, `{"error":"channel not found"}`, http.StatusNotFound)
return
}
member, err := h.isMember(r.Context(), userID, serverID)
if err != nil || !member {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
rows, err := h.db.QueryContext(r.Context(), `
SELECT id, server_id, parent_channel_id, name, type, category, position, archived_at, auto_archive_duration, message_count, last_message_at, created_at
FROM channels
WHERE parent_channel_id = $1 AND server_id = $2 AND type = 'thread' AND archived_at IS NULL
ORDER BY last_message_at DESC NULLS LAST, created_at DESC
`, parentID, serverID)
if err != nil {
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
return
}
defer rows.Close()
threads := make([]threadResponse, 0)
for rows.Next() {
t, err := scanThread(rows)
if err != nil {
continue
}
threads = append(threads, t)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(threads)
}
type updateThreadRequest struct {
Archived *bool `json:"archived"`
}
func (h *Handler) UpdateThread(w http.ResponseWriter, r *http.Request) {
threadID := chi.URLParam(r, "threadID")
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
var serverID string
err := h.db.QueryRowContext(r.Context(), `SELECT server_id FROM channels WHERE id = $1 AND type = 'thread'`, threadID).Scan(&serverID)
if err != nil {
http.Error(w, `{"error":"thread not found"}`, http.StatusNotFound)
return
}
var req updateThreadRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
return
}
allowed, err := h.checker.CheckPermission(r.Context(), serverID, userID, permissions.MANAGE_CHANNELS)
if err != nil || !allowed {
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
return
}
var archivedAt interface{}
if req.Archived != nil && *req.Archived {
archivedAt = "NOW()"
} else {
archivedAt = nil
}
thread, err := scanThread(h.db.QueryRowContext(r.Context(), `
UPDATE channels SET archived_at = COALESCE($1, archived_at) WHERE id = $2
RETURNING id, server_id, parent_channel_id, name, type, category, position, archived_at, auto_archive_duration, message_count, last_message_at, created_at
`, archivedAt, threadID))
if err != nil {
http.Error(w, `{"error":"failed to update thread"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(thread)
}