package handlers import ( "context" "encoding/json" "fmt" "net/http" "regexp" "strconv" "strings" "time" "github.com/go-chi/chi" ) var validNickRe = regexp.MustCompile( `^[a-zA-Z_][a-zA-Z0-9_\-\[\]\\^{}|` + "`" + `]{0,31}$`, ) var validChannelRe = regexp.MustCompile( `^#[a-zA-Z0-9_\-]{1,63}$`, ) const ( maxLongPollTimeout = 30 pollMessageLimit = 100 defaultMaxBodySize = 4096 defaultHistLimit = 50 maxHistLimit = 500 cmdPrivmsg = "PRIVMSG" ) func (hdlr *Handlers) maxBodySize() int64 { if hdlr.params.Config.MaxMessageSize > 0 { return int64(hdlr.params.Config.MaxMessageSize) } return defaultMaxBodySize } // authSession extracts the session from the client token. func (hdlr *Handlers) authSession( request *http.Request, ) (int64, int64, string, error) { auth := request.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { return 0, 0, "", errUnauthorized } token := strings.TrimPrefix(auth, "Bearer ") if token == "" { return 0, 0, "", errUnauthorized } sessionID, clientID, nick, err := hdlr.params.Database.GetSessionByToken( request.Context(), token, ) if err != nil { return 0, 0, "", fmt.Errorf("auth: %w", err) } return sessionID, clientID, nick, nil } func (hdlr *Handlers) requireAuth( writer http.ResponseWriter, request *http.Request, ) (int64, int64, string, bool) { sessionID, clientID, nick, err := hdlr.authSession(request) if err != nil { hdlr.respondError( writer, request, "unauthorized", http.StatusUnauthorized, ) return 0, 0, "", false } return sessionID, clientID, nick, true } // fanOut stores a message and enqueues it to all specified // session IDs, then notifies them. func (hdlr *Handlers) fanOut( request *http.Request, command, from, target string, body json.RawMessage, sessionIDs []int64, ) (string, error) { dbID, msgUUID, err := hdlr.params.Database.InsertMessage( request.Context(), command, from, target, body, nil, ) if err != nil { return "", fmt.Errorf("insert message: %w", err) } for _, sid := range sessionIDs { enqErr := hdlr.params.Database.EnqueueToSession( request.Context(), sid, dbID, ) if enqErr != nil { hdlr.log.Error("enqueue failed", "error", enqErr, "session_id", sid) } hdlr.broker.Notify(sid) } return msgUUID, nil } // fanOutSilent calls fanOut and discards the UUID. func (hdlr *Handlers) fanOutSilent( request *http.Request, command, from, target string, body json.RawMessage, sessionIDs []int64, ) error { _, err := hdlr.fanOut( request, command, from, target, body, sessionIDs, ) return err } // HandleCreateSession creates a new user session. func (hdlr *Handlers) HandleCreateSession() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { request.Body = http.MaxBytesReader( writer, request.Body, hdlr.maxBodySize(), ) hdlr.handleCreateSession(writer, request) } } func (hdlr *Handlers) handleCreateSession( writer http.ResponseWriter, request *http.Request, ) { type createRequest struct { Nick string `json:"nick"` } var payload createRequest err := json.NewDecoder(request.Body).Decode(&payload) if err != nil { hdlr.respondError( writer, request, "invalid request body", http.StatusBadRequest, ) return } payload.Nick = strings.TrimSpace(payload.Nick) if !validNickRe.MatchString(payload.Nick) { hdlr.respondError( writer, request, "invalid nick format", http.StatusBadRequest, ) return } sessionID, clientID, token, err := hdlr.params.Database.CreateSession( request.Context(), payload.Nick, ) if err != nil { hdlr.handleCreateSessionError( writer, request, err, ) return } hdlr.deliverMOTD(request, clientID, sessionID) hdlr.respondJSON(writer, request, map[string]any{ "id": sessionID, "nick": payload.Nick, "token": token, }, http.StatusCreated) } func (hdlr *Handlers) handleCreateSessionError( writer http.ResponseWriter, request *http.Request, err error, ) { if strings.Contains(err.Error(), "UNIQUE") { hdlr.respondError( writer, request, "nick already taken", http.StatusConflict, ) return } hdlr.log.Error( "create session failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) } // deliverMOTD sends the MOTD as IRC numeric messages to a // new client. func (hdlr *Handlers) deliverMOTD( request *http.Request, clientID, sessionID int64, ) { motd := hdlr.params.Config.MOTD serverName := hdlr.params.Config.ServerName if serverName == "" { serverName = "chat" } if motd == "" { return } ctx := request.Context() hdlr.enqueueNumeric( ctx, clientID, "375", serverName, "- "+serverName+" Message of the Day -", ) for line := range strings.SplitSeq(motd, "\n") { hdlr.enqueueNumeric( ctx, clientID, "372", serverName, "- "+line, ) } hdlr.enqueueNumeric( ctx, clientID, "376", serverName, "End of /MOTD command.", ) hdlr.broker.Notify(sessionID) } func (hdlr *Handlers) enqueueNumeric( ctx context.Context, clientID int64, command, serverName, text string, ) { body, err := json.Marshal([]string{text}) if err != nil { hdlr.log.Error( "marshal numeric body", "error", err, ) return } dbID, _, insertErr := hdlr.params.Database.InsertMessage( ctx, command, serverName, "", json.RawMessage(body), nil, ) if insertErr != nil { hdlr.log.Error( "insert numeric message", "error", insertErr, ) return } _ = hdlr.params.Database.EnqueueToClient( ctx, clientID, dbID, ) } // HandleState returns the current session's info and // channels. func (hdlr *Handlers) HandleState() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { sessionID, _, nick, ok := hdlr.requireAuth(writer, request) if !ok { return } channels, err := hdlr.params.Database.ListChannels( request.Context(), sessionID, ) if err != nil { hdlr.log.Error( "list channels failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON(writer, request, map[string]any{ "id": sessionID, "nick": nick, "channels": channels, }, http.StatusOK) } } // HandleListAllChannels returns all channels on the server. func (hdlr *Handlers) HandleListAllChannels() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { _, _, _, ok := hdlr.requireAuth(writer, request) if !ok { return } channels, err := hdlr.params.Database.ListAllChannels( request.Context(), ) if err != nil { hdlr.log.Error( "list all channels failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON( writer, request, channels, http.StatusOK, ) } } // HandleChannelMembers returns members of a channel. func (hdlr *Handlers) HandleChannelMembers() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { _, _, _, ok := hdlr.requireAuth(writer, request) if !ok { return } name := "#" + chi.URLParam(request, "channel") chID, err := hdlr.params.Database.GetChannelByName( request.Context(), name, ) if err != nil { hdlr.respondError( writer, request, "channel not found", http.StatusNotFound, ) return } members, err := hdlr.params.Database.ChannelMembers( request.Context(), chID, ) if err != nil { hdlr.log.Error( "channel members failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON( writer, request, members, http.StatusOK, ) } } // HandleGetMessages returns messages via long-polling. func (hdlr *Handlers) HandleGetMessages() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { sessionID, clientID, _, ok := hdlr.requireAuth(writer, request) if !ok { return } afterID, _ := strconv.ParseInt( request.URL.Query().Get("after"), 10, 64, ) timeout, _ := strconv.Atoi( request.URL.Query().Get("timeout"), ) if timeout < 0 { timeout = 0 } if timeout > maxLongPollTimeout { timeout = maxLongPollTimeout } msgs, lastQID, err := hdlr.params.Database.PollMessages( request.Context(), clientID, afterID, pollMessageLimit, ) if err != nil { hdlr.log.Error( "poll messages failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } if len(msgs) > 0 || timeout == 0 { hdlr.respondJSON(writer, request, map[string]any{ "messages": msgs, "last_id": lastQID, }, http.StatusOK) return } hdlr.longPoll( writer, request, sessionID, clientID, afterID, timeout, ) } } func (hdlr *Handlers) longPoll( writer http.ResponseWriter, request *http.Request, sessionID, clientID, afterID int64, timeout int, ) { waitCh := hdlr.broker.Wait(sessionID) timer := time.NewTimer( time.Duration(timeout) * time.Second, ) defer timer.Stop() select { case <-waitCh: case <-timer.C: case <-request.Context().Done(): hdlr.broker.Remove(sessionID, waitCh) return } hdlr.broker.Remove(sessionID, waitCh) msgs, lastQID, err := hdlr.params.Database.PollMessages( request.Context(), clientID, afterID, pollMessageLimit, ) if err != nil { hdlr.log.Error( "poll messages failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON(writer, request, map[string]any{ "messages": msgs, "last_id": lastQID, }, http.StatusOK) } // HandleSendCommand handles all C2S commands. func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc { type commandRequest struct { Command string `json:"command"` To string `json:"to"` Body json.RawMessage `json:"body,omitempty"` Meta json.RawMessage `json:"meta,omitempty"` } return func( writer http.ResponseWriter, request *http.Request, ) { request.Body = http.MaxBytesReader( writer, request.Body, hdlr.maxBodySize(), ) sessionID, _, nick, ok := hdlr.requireAuth(writer, request) if !ok { return } var payload commandRequest err := json.NewDecoder(request.Body).Decode(&payload) if err != nil { hdlr.respondError( writer, request, "invalid request body", http.StatusBadRequest, ) return } payload.Command = strings.ToUpper( strings.TrimSpace(payload.Command), ) payload.To = strings.TrimSpace(payload.To) if payload.Command == "" { hdlr.respondError( writer, request, "command required", http.StatusBadRequest, ) return } bodyLines := func() []string { if payload.Body == nil { return nil } var lines []string decErr := json.Unmarshal(payload.Body, &lines) if decErr != nil { return nil } return lines } hdlr.dispatchCommand( writer, request, sessionID, nick, payload.Command, payload.To, payload.Body, bodyLines, ) } } func (hdlr *Handlers) dispatchCommand( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, command, target string, body json.RawMessage, bodyLines func() []string, ) { switch command { case cmdPrivmsg, "NOTICE": hdlr.handlePrivmsg( writer, request, sessionID, nick, command, target, body, bodyLines, ) case "JOIN": hdlr.handleJoin( writer, request, sessionID, nick, target, ) case "PART": hdlr.handlePart( writer, request, sessionID, nick, target, body, ) case "NICK": hdlr.handleNick( writer, request, sessionID, nick, bodyLines, ) case "TOPIC": hdlr.handleTopic( writer, request, nick, target, body, bodyLines, ) case "QUIT": hdlr.handleQuit( writer, request, sessionID, nick, body, ) case "PING": hdlr.respondJSON(writer, request, map[string]string{ "command": "PONG", "from": hdlr.params.Config.ServerName, }, http.StatusOK) default: hdlr.respondError( writer, request, "unknown command: "+command, http.StatusBadRequest, ) } } func (hdlr *Handlers) handlePrivmsg( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, command, target string, body json.RawMessage, bodyLines func() []string, ) { if target == "" { hdlr.respondError( writer, request, "to field required", http.StatusBadRequest, ) return } lines := bodyLines() if len(lines) == 0 { hdlr.respondError( writer, request, "body required", http.StatusBadRequest, ) return } if strings.HasPrefix(target, "#") { hdlr.handleChannelMsg( writer, request, sessionID, nick, command, target, body, ) return } hdlr.handleDirectMsg( writer, request, sessionID, nick, command, target, body, ) } func (hdlr *Handlers) handleChannelMsg( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, command, target string, body json.RawMessage, ) { chID, err := hdlr.params.Database.GetChannelByName( request.Context(), target, ) if err != nil { hdlr.respondError( writer, request, "channel not found", http.StatusNotFound, ) return } isMember, err := hdlr.params.Database.IsChannelMember( request.Context(), chID, sessionID, ) if err != nil { hdlr.log.Error( "check membership failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } if !isMember { hdlr.respondError( writer, request, "not a member of this channel", http.StatusForbidden, ) return } memberIDs, err := hdlr.params.Database.GetChannelMemberIDs( request.Context(), chID, ) if err != nil { hdlr.log.Error( "get channel members failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } msgUUID, err := hdlr.fanOut( request, command, nick, target, body, memberIDs, ) if err != nil { hdlr.log.Error("send message failed", "error", err) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON(writer, request, map[string]string{"id": msgUUID, "status": "sent"}, http.StatusCreated) } func (hdlr *Handlers) handleDirectMsg( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, command, target string, body json.RawMessage, ) { targetSID, err := hdlr.params.Database.GetSessionByNick( request.Context(), target, ) if err != nil { hdlr.respondError( writer, request, "user not found", http.StatusNotFound, ) return } recipients := []int64{targetSID} if targetSID != sessionID { recipients = append(recipients, sessionID) } msgUUID, err := hdlr.fanOut( request, command, nick, target, body, recipients, ) if err != nil { hdlr.log.Error("send dm failed", "error", err) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON(writer, request, map[string]string{"id": msgUUID, "status": "sent"}, http.StatusCreated) } func (hdlr *Handlers) handleJoin( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, target string, ) { if target == "" { hdlr.respondError( writer, request, "to field required", http.StatusBadRequest, ) return } channel := target if !strings.HasPrefix(channel, "#") { channel = "#" + channel } if !validChannelRe.MatchString(channel) { hdlr.respondError( writer, request, "invalid channel name", http.StatusBadRequest, ) return } chID, err := hdlr.params.Database.GetOrCreateChannel( request.Context(), channel, ) if err != nil { hdlr.log.Error( "get/create channel failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } err = hdlr.params.Database.JoinChannel( request.Context(), chID, sessionID, ) if err != nil { hdlr.log.Error( "join channel failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } memberIDs, _ := hdlr.params.Database.GetChannelMemberIDs( request.Context(), chID, ) _ = hdlr.fanOutSilent( request, "JOIN", nick, channel, nil, memberIDs, ) hdlr.respondJSON(writer, request, map[string]string{ "status": "joined", "channel": channel, }, http.StatusOK) } func (hdlr *Handlers) handlePart( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, target string, body json.RawMessage, ) { if target == "" { hdlr.respondError( writer, request, "to field required", http.StatusBadRequest, ) return } channel := target if !strings.HasPrefix(channel, "#") { channel = "#" + channel } chID, err := hdlr.params.Database.GetChannelByName( request.Context(), channel, ) if err != nil { hdlr.respondError( writer, request, "channel not found", http.StatusNotFound, ) return } memberIDs, _ := hdlr.params.Database.GetChannelMemberIDs( request.Context(), chID, ) _ = hdlr.fanOutSilent( request, "PART", nick, channel, body, memberIDs, ) err = hdlr.params.Database.PartChannel( request.Context(), chID, sessionID, ) if err != nil { hdlr.log.Error( "part channel failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } _ = hdlr.params.Database.DeleteChannelIfEmpty( request.Context(), chID, ) hdlr.respondJSON(writer, request, map[string]string{ "status": "parted", "channel": channel, }, http.StatusOK) } func (hdlr *Handlers) handleNick( writer http.ResponseWriter, request *http.Request, sessionID int64, nick string, bodyLines func() []string, ) { lines := bodyLines() if len(lines) == 0 { hdlr.respondError( writer, request, "body required (new nick)", http.StatusBadRequest, ) return } newNick := strings.TrimSpace(lines[0]) if !validNickRe.MatchString(newNick) { hdlr.respondError( writer, request, "invalid nick", http.StatusBadRequest, ) return } if newNick == nick { hdlr.respondJSON(writer, request, map[string]string{ "status": "ok", "nick": newNick, }, http.StatusOK) return } err := hdlr.params.Database.ChangeNick( request.Context(), sessionID, newNick, ) if err != nil { if strings.Contains(err.Error(), "UNIQUE") { hdlr.respondError( writer, request, "nick already in use", http.StatusConflict, ) return } hdlr.log.Error( "change nick failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.broadcastNick(request, sessionID, nick, newNick) hdlr.respondJSON(writer, request, map[string]string{ "status": "ok", "nick": newNick, }, http.StatusOK) } func (hdlr *Handlers) broadcastNick( request *http.Request, sessionID int64, oldNick, newNick string, ) { channels, _ := hdlr.params.Database. GetSessionChannels( request.Context(), sessionID, ) notified := map[int64]bool{sessionID: true} nickBody, err := json.Marshal([]string{newNick}) if err != nil { hdlr.log.Error( "marshal nick body", "error", err, ) return } dbID, _, _ := hdlr.params.Database.InsertMessage( request.Context(), "NICK", oldNick, "", json.RawMessage(nickBody), nil, ) _ = hdlr.params.Database.EnqueueToSession( request.Context(), sessionID, dbID, ) hdlr.broker.Notify(sessionID) for _, chanInfo := range channels { memberIDs, _ := hdlr.params.Database. GetChannelMemberIDs( request.Context(), chanInfo.ID, ) for _, mid := range memberIDs { if !notified[mid] { notified[mid] = true _ = hdlr.params.Database.EnqueueToSession( request.Context(), mid, dbID, ) hdlr.broker.Notify(mid) } } } } func (hdlr *Handlers) handleTopic( writer http.ResponseWriter, request *http.Request, nick, target string, body json.RawMessage, bodyLines func() []string, ) { if target == "" { hdlr.respondError( writer, request, "to field required", http.StatusBadRequest, ) return } lines := bodyLines() if len(lines) == 0 { hdlr.respondError( writer, request, "body required (topic text)", http.StatusBadRequest, ) return } topic := strings.Join(lines, " ") channel := target if !strings.HasPrefix(channel, "#") { channel = "#" + channel } err := hdlr.params.Database.SetTopic( request.Context(), channel, topic, ) if err != nil { hdlr.log.Error( "set topic failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } chID, err := hdlr.params.Database.GetChannelByName( request.Context(), channel, ) if err != nil { hdlr.respondError( writer, request, "channel not found", http.StatusNotFound, ) return } memberIDs, _ := hdlr.params.Database.GetChannelMemberIDs( request.Context(), chID, ) _ = hdlr.fanOutSilent( request, "TOPIC", nick, channel, body, memberIDs, ) hdlr.respondJSON(writer, request, map[string]string{ "status": "ok", "topic": topic, }, http.StatusOK) } func (hdlr *Handlers) handleQuit( writer http.ResponseWriter, request *http.Request, sessionID int64, nick string, body json.RawMessage, ) { channels, _ := hdlr.params.Database. GetSessionChannels( request.Context(), sessionID, ) notified := map[int64]bool{} var dbID int64 if len(channels) > 0 { dbID, _, _ = hdlr.params.Database.InsertMessage( request.Context(), "QUIT", nick, "", body, nil, ) } for _, chanInfo := range channels { memberIDs, _ := hdlr.params.Database. GetChannelMemberIDs( request.Context(), chanInfo.ID, ) for _, mid := range memberIDs { if mid != sessionID && !notified[mid] { notified[mid] = true _ = hdlr.params.Database.EnqueueToSession( request.Context(), mid, dbID, ) hdlr.broker.Notify(mid) } } _ = hdlr.params.Database.PartChannel( request.Context(), chanInfo.ID, sessionID, ) _ = hdlr.params.Database.DeleteChannelIfEmpty( request.Context(), chanInfo.ID, ) } _ = hdlr.params.Database.DeleteSession( request.Context(), sessionID, ) hdlr.respondJSON(writer, request, map[string]string{"status": "quit"}, http.StatusOK) } // HandleGetHistory returns message history for a target. func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { sessionID, _, nick, ok := hdlr.requireAuth(writer, request) if !ok { return } target := request.URL.Query().Get("target") if target == "" { hdlr.respondError( writer, request, "target required", http.StatusBadRequest, ) return } if !hdlr.canAccessHistory( writer, request, sessionID, nick, target, ) { return } beforeID, _ := strconv.ParseInt( request.URL.Query().Get("before"), 10, 64, ) limit, _ := strconv.Atoi( request.URL.Query().Get("limit"), ) if limit <= 0 || limit > maxHistLimit { limit = defaultHistLimit } msgs, err := hdlr.params.Database.GetHistory( request.Context(), target, beforeID, limit, ) if err != nil { hdlr.log.Error( "get history failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON( writer, request, msgs, http.StatusOK, ) } } // canAccessHistory verifies the user can read history // for the given target (channel or DM participant). func (hdlr *Handlers) canAccessHistory( writer http.ResponseWriter, request *http.Request, sessionID int64, nick, target string, ) bool { if strings.HasPrefix(target, "#") { return hdlr.canAccessChannelHistory( writer, request, sessionID, target, ) } // DM history: only allow if the target is the // requester's own nick (messages sent to them). if target != nick { hdlr.respondError( writer, request, "forbidden", http.StatusForbidden, ) return false } return true } func (hdlr *Handlers) canAccessChannelHistory( writer http.ResponseWriter, request *http.Request, sessionID int64, target string, ) bool { chID, err := hdlr.params.Database.GetChannelByName( request.Context(), target, ) if err != nil { hdlr.respondError( writer, request, "channel not found", http.StatusNotFound, ) return false } isMember, err := hdlr.params.Database.IsChannelMember( request.Context(), chID, sessionID, ) if err != nil { hdlr.log.Error( "check membership failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return false } if !isMember { hdlr.respondError( writer, request, "not a member of this channel", http.StatusForbidden, ) return false } return true } // HandleLogout deletes the authenticated client's token // and cleans up the user (session) if no clients remain. func (hdlr *Handlers) HandleLogout() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { sessionID, clientID, nick, ok := hdlr.requireAuth(writer, request) if !ok { return } ctx := request.Context() err := hdlr.params.Database.DeleteClient( ctx, clientID, ) if err != nil { hdlr.log.Error( "delete client failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } // If no clients remain, clean up the user fully: // part all channels (notifying members) and // delete the session. remaining, err := hdlr.params.Database. ClientCountForSession(ctx, sessionID) if err != nil { hdlr.log.Error( "client count check failed", "error", err, ) } if remaining == 0 { hdlr.cleanupUser( ctx, sessionID, nick, ) } hdlr.respondJSON(writer, request, map[string]string{"status": "ok"}, http.StatusOK) } } // cleanupUser parts the user from all channels (notifying // members) and deletes the session. func (hdlr *Handlers) cleanupUser( ctx context.Context, sessionID int64, nick string, ) { channels, _ := hdlr.params.Database. GetSessionChannels(ctx, sessionID) notified := map[int64]bool{} var quitDBID int64 if len(channels) > 0 { quitDBID, _, _ = hdlr.params.Database.InsertMessage( ctx, "QUIT", nick, "", nil, nil, ) } for _, chanInfo := range channels { memberIDs, _ := hdlr.params.Database. GetChannelMemberIDs(ctx, chanInfo.ID) for _, mid := range memberIDs { if mid != sessionID && !notified[mid] { notified[mid] = true _ = hdlr.params.Database.EnqueueToSession( ctx, mid, quitDBID, ) hdlr.broker.Notify(mid) } } _ = hdlr.params.Database.PartChannel( ctx, chanInfo.ID, sessionID, ) _ = hdlr.params.Database.DeleteChannelIfEmpty( ctx, chanInfo.ID, ) } _ = hdlr.params.Database.DeleteSession(ctx, sessionID) } // HandleUsersMe returns the current user's session info. func (hdlr *Handlers) HandleUsersMe() http.HandlerFunc { return hdlr.HandleState() } // HandleServerInfo returns server metadata. func (hdlr *Handlers) HandleServerInfo() http.HandlerFunc { return func( writer http.ResponseWriter, request *http.Request, ) { users, err := hdlr.params.Database.GetUserCount( request.Context(), ) if err != nil { hdlr.log.Error( "get user count failed", "error", err, ) hdlr.respondError( writer, request, "internal error", http.StatusInternalServerError, ) return } hdlr.respondJSON(writer, request, map[string]any{ "name": hdlr.params.Config.ServerName, "motd": hdlr.params.Config.MOTD, "users": users, }, http.StatusOK) } }