2438 lines
48 KiB
Go
2438 lines
48 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/db"
|
|
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
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
|
|
)
|
|
|
|
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, nil, 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"`
|
|
Hashcash string `json:"pow_token,omitempty"` //nolint:tagliatelle
|
|
}
|
|
|
|
var payload createRequest
|
|
|
|
err := json.NewDecoder(request.Body).Decode(&payload)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid request body",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// Validate hashcash proof-of-work if configured.
|
|
if hdlr.params.Config.HashcashBits > 0 {
|
|
if payload.Hashcash == "" {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"hashcash proof-of-work required",
|
|
http.StatusPaymentRequired,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err = hdlr.hashcashVal.Validate(
|
|
payload.Hashcash, hdlr.params.Config.HashcashBits,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid hashcash stamp: "+err.Error(),
|
|
http.StatusPaymentRequired,
|
|
)
|
|
|
|
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, payload.Nick)
|
|
|
|
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 db.IsUniqueConstraintError(err) {
|
|
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,
|
|
)
|
|
}
|
|
|
|
// deliverWelcome sends connection registration numerics
|
|
// (001-005) to a new client.
|
|
func (hdlr *Handlers) deliverWelcome(
|
|
request *http.Request,
|
|
clientID int64,
|
|
nick string,
|
|
) {
|
|
ctx := request.Context()
|
|
srvName := hdlr.serverName()
|
|
version := hdlr.serverVersion()
|
|
|
|
// 001 RPL_WELCOME
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWelcome, nick, nil,
|
|
"Welcome to the network, "+nick,
|
|
)
|
|
|
|
// 002 RPL_YOURHOST
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplYourHost, nick, nil,
|
|
"Your host is "+srvName+
|
|
", running version "+version,
|
|
)
|
|
|
|
// 003 RPL_CREATED
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplCreated, nick, nil,
|
|
"This server was created "+
|
|
hdlr.params.Globals.StartTime.
|
|
Format("2006-01-02"),
|
|
)
|
|
|
|
// 004 RPL_MYINFO
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplMyInfo, nick,
|
|
[]string{srvName, version, "", "imnst"},
|
|
"",
|
|
)
|
|
|
|
// 005 RPL_ISUPPORT
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplIsupport, nick,
|
|
[]string{
|
|
"CHANTYPES=#",
|
|
"NICKLEN=32",
|
|
"CHANMODES=,,," + "imnst",
|
|
"NETWORK=neoirc",
|
|
"CASEMAPPING=ascii",
|
|
},
|
|
"are supported by this server",
|
|
)
|
|
|
|
// LUSERS
|
|
hdlr.deliverLusers(ctx, clientID, nick)
|
|
}
|
|
|
|
// deliverLusers sends RPL_LUSERCLIENT (251),
|
|
// RPL_LUSEROP (252), RPL_LUSERCHANNELS (254), and
|
|
// RPL_LUSERME (255) to the client.
|
|
func (hdlr *Handlers) deliverLusers(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick string,
|
|
) {
|
|
userCount, err := hdlr.params.Database.GetUserCount(ctx)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"lusers user count", "error", err,
|
|
)
|
|
|
|
userCount = 0
|
|
}
|
|
|
|
chanCount, err := hdlr.params.Database.GetChannelCount(
|
|
ctx,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"lusers channel count", "error", err,
|
|
)
|
|
|
|
chanCount = 0
|
|
}
|
|
|
|
// 251 RPL_LUSERCLIENT
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplLuserClient, nick, nil,
|
|
fmt.Sprintf(
|
|
"There are %d users and 0 invisible on 1 servers",
|
|
userCount,
|
|
),
|
|
)
|
|
|
|
// 252 RPL_LUSEROP
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplLuserOp, nick,
|
|
[]string{"0"},
|
|
"operator(s) online",
|
|
)
|
|
|
|
// 254 RPL_LUSERCHANNELS
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplLuserChannels, nick,
|
|
[]string{strconv.FormatInt(chanCount, 10)},
|
|
"channels formed",
|
|
)
|
|
|
|
// 255 RPL_LUSERME
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplLuserMe, nick, nil,
|
|
fmt.Sprintf(
|
|
"I have %d clients and 1 servers",
|
|
userCount,
|
|
),
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) serverVersion() string {
|
|
ver := hdlr.params.Globals.Version
|
|
if ver == "" {
|
|
return "dev"
|
|
}
|
|
|
|
return ver
|
|
}
|
|
|
|
// deliverMOTD sends the MOTD as IRC numeric messages to a
|
|
// new client.
|
|
func (hdlr *Handlers) deliverMOTD(
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
nick string,
|
|
) {
|
|
motd := hdlr.params.Config.MOTD
|
|
srvName := hdlr.serverName()
|
|
|
|
ctx := request.Context()
|
|
|
|
hdlr.deliverWelcome(request, clientID, nick)
|
|
|
|
if motd == "" {
|
|
hdlr.broker.Notify(sessionID)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplMotdStart, nick, nil,
|
|
"- "+srvName+" Message of the Day -",
|
|
)
|
|
|
|
for line := range strings.SplitSeq(motd, "\n") {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplMotd, nick, nil,
|
|
"- "+line,
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfMotd, nick, nil,
|
|
"End of /MOTD command.",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
func (hdlr *Handlers) serverName() string {
|
|
name := hdlr.params.Config.ServerName
|
|
if name == "" {
|
|
return "neoirc"
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
func (hdlr *Handlers) enqueueNumeric(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
code irc.IRCMessageType,
|
|
nick string,
|
|
params []string,
|
|
text string,
|
|
) {
|
|
command := code.Code()
|
|
|
|
body, err := json.Marshal([]string{text})
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"marshal numeric body", "error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
var paramsJSON json.RawMessage
|
|
|
|
if len(params) > 0 {
|
|
paramsJSON, err = json.Marshal(params)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"marshal numeric params", "error", err,
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
dbID, _, insertErr := hdlr.params.Database.InsertMessage(
|
|
ctx, command, hdlr.serverName(), nick,
|
|
paramsJSON, 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. When called with ?initChannelState=1, it also
|
|
// enqueues synthetic JOIN + TOPIC + NAMES messages for
|
|
// every channel the session belongs to so that a
|
|
// reconnecting client can rebuild its channel tabs from
|
|
// the message stream.
|
|
func (hdlr *Handlers) HandleState() http.HandlerFunc {
|
|
return func(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
sessionID, clientID, 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
|
|
}
|
|
|
|
if request.URL.Query().Get("initChannelState") == "1" {
|
|
hdlr.initChannelState(
|
|
request, clientID, sessionID, nick,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request, map[string]any{
|
|
"id": sessionID,
|
|
"nick": nick,
|
|
"channels": channels,
|
|
}, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// initChannelState enqueues synthetic JOIN messages and
|
|
// join-numerics (TOPIC, NAMES) for every channel the
|
|
// session belongs to. Messages are enqueued only to the
|
|
// specified client so other clients/sessions are not
|
|
// affected.
|
|
func (hdlr *Handlers) initChannelState(
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
nick string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
channels, err := hdlr.params.Database.
|
|
GetSessionChannels(ctx, sessionID)
|
|
if err != nil || len(channels) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, chanInfo := range channels {
|
|
// Enqueue a synthetic JOIN (only to this client).
|
|
dbID, _, insErr := hdlr.params.Database.
|
|
InsertMessage(
|
|
ctx, "JOIN", nick, chanInfo.Name,
|
|
nil, nil, nil,
|
|
)
|
|
if insErr != nil {
|
|
hdlr.log.Error(
|
|
"initChannelState: insert JOIN",
|
|
"error", insErr,
|
|
)
|
|
|
|
continue
|
|
}
|
|
|
|
_ = hdlr.params.Database.EnqueueToClient(
|
|
ctx, clientID, dbID,
|
|
)
|
|
|
|
// Enqueue TOPIC + NAMES numerics.
|
|
hdlr.deliverJoinNumerics(
|
|
request, clientID, sessionID,
|
|
nick, chanInfo.Name, chanInfo.ID,
|
|
)
|
|
}
|
|
}
|
|
|
|
// 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, clientID, 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, clientID, nick,
|
|
payload.Command, payload.To,
|
|
payload.Body, bodyLines,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) dispatchCommand(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
bodyLines func() []string,
|
|
) {
|
|
switch command {
|
|
case irc.CmdPrivmsg, irc.CmdNotice:
|
|
hdlr.handlePrivmsg(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, body, bodyLines,
|
|
)
|
|
case irc.CmdJoin:
|
|
hdlr.handleJoin(
|
|
writer, request,
|
|
sessionID, clientID, nick, target,
|
|
)
|
|
case irc.CmdPart:
|
|
hdlr.handlePart(
|
|
writer, request,
|
|
sessionID, clientID, nick, target, body,
|
|
)
|
|
case irc.CmdNick:
|
|
hdlr.handleNick(
|
|
writer, request,
|
|
sessionID, clientID, nick, bodyLines,
|
|
)
|
|
case irc.CmdTopic:
|
|
hdlr.handleTopic(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
target, body, bodyLines,
|
|
)
|
|
case irc.CmdQuit:
|
|
hdlr.handleQuit(
|
|
writer, request, sessionID, nick, body,
|
|
)
|
|
case irc.CmdMotd, irc.CmdPing:
|
|
hdlr.dispatchInfoCommand(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, bodyLines,
|
|
)
|
|
default:
|
|
hdlr.dispatchQueryCommand(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, bodyLines,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) dispatchQueryCommand(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
bodyLines func() []string,
|
|
) {
|
|
switch command {
|
|
case irc.CmdMode:
|
|
hdlr.handleMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
target, bodyLines,
|
|
)
|
|
case irc.CmdNames:
|
|
hdlr.handleNames(
|
|
writer, request,
|
|
sessionID, clientID, nick, target,
|
|
)
|
|
case irc.CmdList:
|
|
hdlr.handleList(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
)
|
|
case irc.CmdWhois:
|
|
hdlr.handleWhois(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
target, bodyLines,
|
|
)
|
|
case irc.CmdWho:
|
|
hdlr.handleWho(
|
|
writer, request,
|
|
sessionID, clientID, nick, target,
|
|
)
|
|
case irc.CmdLusers:
|
|
hdlr.handleLusers(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
)
|
|
default:
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrUnknownCommand, nick, []string{command},
|
|
"Unknown command",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) handlePrivmsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
bodyLines func() []string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrNeedMoreParams, nick, []string{command},
|
|
"Not enough parameters",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
lines := bodyLines()
|
|
if len(lines) == 0 {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrNeedMoreParams, nick, []string{command},
|
|
"Not enough parameters",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
if strings.HasPrefix(target, "#") {
|
|
hdlr.handleChannelMsg(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, body,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.handleDirectMsg(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, body,
|
|
)
|
|
}
|
|
|
|
// respondIRCError enqueues a numeric error reply, notifies
|
|
// the broker, and sends HTTP 200 with {"status":"error"}.
|
|
func (hdlr *Handlers) respondIRCError(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
code irc.IRCMessageType,
|
|
nick string,
|
|
params []string,
|
|
text string,
|
|
) {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
code, nick, params, text,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleChannelMsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
) {
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
request.Context(), target,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchChannel, nick, []string{target},
|
|
"No such channel",
|
|
)
|
|
|
|
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.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNotOnChannel, nick, []string{target},
|
|
"You're not on that channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.sendChannelMsg(
|
|
writer, request, command, nick, target, body, chID,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) sendChannelMsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
command, nick, target string,
|
|
body json.RawMessage,
|
|
chID int64,
|
|
) {
|
|
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.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleDirectMsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
) {
|
|
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
|
request.Context(), target,
|
|
)
|
|
if err != nil {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrNoSuchNick, nick, []string{target},
|
|
"No such nick/channel",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
|
|
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.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleJoin(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdJoin},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
if !validChannelRe.MatchString(channel) {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchChannel, nick, []string{channel},
|
|
"No such channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeJoin(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeJoin(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
) {
|
|
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, irc.CmdJoin, nick, channel, nil, memberIDs,
|
|
)
|
|
|
|
hdlr.deliverJoinNumerics(
|
|
request, clientID, sessionID, nick, channel, chID,
|
|
)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"status": "joined",
|
|
"channel": channel,
|
|
},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// deliverJoinNumerics sends RPL_TOPIC/RPL_NOTOPIC,
|
|
// RPL_NAMREPLY, and RPL_ENDOFNAMES to the joining client.
|
|
func (hdlr *Handlers) deliverJoinNumerics(
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
chInfo, err := hdlr.params.Database.GetChannelByName(
|
|
ctx, channel,
|
|
)
|
|
if err == nil {
|
|
_ = chInfo // chInfo is the ID; topic comes from DB.
|
|
}
|
|
|
|
// Get topic from channel info.
|
|
channels, listErr := hdlr.params.Database.ListChannels(
|
|
ctx, sessionID,
|
|
)
|
|
|
|
topic := ""
|
|
|
|
if listErr == nil {
|
|
for _, ch := range channels {
|
|
if ch.Name == channel {
|
|
topic = ch.Topic
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if topic != "" {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplTopic, nick,
|
|
[]string{channel}, topic,
|
|
)
|
|
} else {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNoTopic, nick,
|
|
[]string{channel}, "No topic is set",
|
|
)
|
|
}
|
|
|
|
// Get member list for NAMES reply.
|
|
members, memErr := hdlr.params.Database.ChannelMembers(
|
|
ctx, chID,
|
|
)
|
|
|
|
if memErr == nil && len(members) > 0 {
|
|
nicks := make([]string, 0, len(members))
|
|
|
|
for _, mem := range members {
|
|
nicks = append(nicks, mem.Nick)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNamReply, nick,
|
|
[]string{"=", channel},
|
|
strings.Join(nicks, " "),
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfNames, nick,
|
|
[]string{channel}, "End of /NAMES list",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
func (hdlr *Handlers) handlePart(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
body json.RawMessage,
|
|
) {
|
|
if target == "" {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdPart},
|
|
"Not enough parameters",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
request.Context(), channel,
|
|
)
|
|
if err != nil {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrNoSuchChannel, nick, []string{channel},
|
|
"No such channel",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
memberIDs, _ := hdlr.params.Database.GetChannelMemberIDs(
|
|
request.Context(), chID,
|
|
)
|
|
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdPart, 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, clientID int64,
|
|
nick string,
|
|
bodyLines func() []string,
|
|
) {
|
|
lines := bodyLines()
|
|
if len(lines) == 0 {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdNick},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
newNick := strings.TrimSpace(lines[0])
|
|
|
|
if !validNickRe.MatchString(newNick) {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrErroneusNickname, nick, []string{newNick},
|
|
"Erroneous nickname",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if newNick == nick {
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"status": "ok", "nick": newNick,
|
|
},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeNickChange(
|
|
writer, request,
|
|
sessionID, clientID, nick, newNick,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeNickChange(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, newNick string,
|
|
) {
|
|
err := hdlr.params.Database.ChangeNick(
|
|
request.Context(), sessionID, newNick,
|
|
)
|
|
if err != nil {
|
|
if db.IsUniqueConstraintError(err) {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNicknameInUse, nick, []string{newNick},
|
|
"Nickname is already in use",
|
|
)
|
|
|
|
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(), irc.CmdNick, oldNick, "",
|
|
nil, 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,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
body json.RawMessage,
|
|
bodyLines func() []string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdTopic},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
lines := bodyLines()
|
|
if len(lines) == 0 {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdTopic},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
request.Context(), channel,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchChannel, nick, []string{channel},
|
|
"No such channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeTopic(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, strings.Join(lines, " "),
|
|
body, chID,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeTopic(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel, topic string,
|
|
body json.RawMessage,
|
|
chID int64,
|
|
) {
|
|
setErr := hdlr.params.Database.SetTopic(
|
|
request.Context(), channel, topic,
|
|
)
|
|
if setErr != nil {
|
|
hdlr.log.Error(
|
|
"set topic failed", "error", setErr,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
memberIDs, _ := hdlr.params.Database.GetChannelMemberIDs(
|
|
request.Context(), chID,
|
|
)
|
|
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdTopic, nick, channel, body, memberIDs,
|
|
)
|
|
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.RplTopic, nick, []string{channel}, topic,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"status": "ok", "topic": topic,
|
|
},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// dispatchInfoCommand handles informational IRC commands
|
|
// that produce server-side numerics (MOTD, PING).
|
|
func (hdlr *Handlers) dispatchInfoCommand(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
bodyLines func() []string,
|
|
) {
|
|
_ = target
|
|
_ = bodyLines
|
|
|
|
okResp := map[string]string{"status": "ok"}
|
|
|
|
switch command {
|
|
case irc.CmdMotd:
|
|
hdlr.deliverMOTD(
|
|
request, clientID, sessionID, nick,
|
|
)
|
|
case irc.CmdPing:
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"command": irc.CmdPong,
|
|
"from": hdlr.serverName(),
|
|
},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.respondJSON(
|
|
writer, request, okResp, 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(), irc.CmdQuit, nick, "",
|
|
nil, 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)
|
|
}
|
|
|
|
// handleMode handles the MODE command for channels and
|
|
// users. Currently supports query-only (no mode changes).
|
|
func (hdlr *Handlers) handleMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
bodyLines func() []string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdMode},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
// User mode query — return empty modes.
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.RplUmodeIs, nick, nil, "+",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
_ = bodyLines
|
|
|
|
hdlr.handleChannelMode(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleChannelMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
ctx, channel,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchChannel, nick, []string{channel},
|
|
"No such channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// 324 RPL_CHANNELMODEIS
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplChannelModeIs, nick,
|
|
[]string{channel, "+n"}, "",
|
|
)
|
|
|
|
// 329 RPL_CREATIONTIME
|
|
createdAt, timeErr := hdlr.params.Database.
|
|
GetChannelCreatedAt(ctx, chID)
|
|
if timeErr == nil {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplCreationTime, nick,
|
|
[]string{
|
|
channel,
|
|
strconv.FormatInt(
|
|
createdAt.Unix(), 10,
|
|
),
|
|
},
|
|
"",
|
|
)
|
|
}
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleNames sends NAMES reply for a channel.
|
|
func (hdlr *Handlers) handleNames(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdNames},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
ctx := request.Context()
|
|
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
ctx, channel,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchChannel, nick, []string{channel},
|
|
"No such channel",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
members, memErr := hdlr.params.Database.ChannelMembers(
|
|
ctx, chID,
|
|
)
|
|
if memErr == nil && len(members) > 0 {
|
|
nicks := make([]string, 0, len(members))
|
|
|
|
for _, mem := range members {
|
|
nicks = append(nicks, mem.Nick)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNamReply, nick,
|
|
[]string{"=", channel},
|
|
strings.Join(nicks, " "),
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfNames, nick,
|
|
[]string{channel}, "End of /NAMES list",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleList sends the LIST response with 322/323
|
|
// numerics.
|
|
func (hdlr *Handlers) handleList(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
channels, err := hdlr.params.Database.
|
|
ListAllChannelsWithCounts(ctx)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"list channels failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
for _, chanInfo := range channels {
|
|
// 322 RPL_LIST
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplList, nick,
|
|
[]string{
|
|
chanInfo.Name,
|
|
strconv.FormatInt(
|
|
chanInfo.MemberCount, 10,
|
|
),
|
|
},
|
|
chanInfo.Topic,
|
|
)
|
|
}
|
|
|
|
// 323 — end of channel list.
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplListEnd, nick, nil,
|
|
"End of /LIST",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleWhois handles the WHOIS command.
|
|
func (hdlr *Handlers) handleWhois(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
bodyLines func() []string,
|
|
) {
|
|
queryNick := target
|
|
|
|
// If target is empty, check body for the nick.
|
|
if queryNick == "" {
|
|
lines := bodyLines()
|
|
if len(lines) > 0 {
|
|
queryNick = strings.TrimSpace(lines[0])
|
|
}
|
|
}
|
|
|
|
if queryNick == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdWhois},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeWhois(
|
|
writer, request,
|
|
sessionID, clientID, nick, queryNick,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeWhois(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, queryNick string,
|
|
) {
|
|
ctx := request.Context()
|
|
srvName := hdlr.serverName()
|
|
|
|
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
|
ctx, queryNick,
|
|
)
|
|
if err != nil {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.ErrNoSuchNick, nick,
|
|
[]string{queryNick},
|
|
"No such nick/channel",
|
|
)
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfWhois, nick,
|
|
[]string{queryNick},
|
|
"End of /WHOIS list",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
// 311 RPL_WHOISUSER
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisUser, nick,
|
|
[]string{queryNick, queryNick, srvName, "*"},
|
|
queryNick,
|
|
)
|
|
|
|
// 312 RPL_WHOISSERVER
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisServer, nick,
|
|
[]string{queryNick, srvName},
|
|
"neoirc server",
|
|
)
|
|
|
|
// 319 RPL_WHOISCHANNELS
|
|
hdlr.deliverWhoisChannels(
|
|
ctx, clientID, nick, queryNick, targetSID,
|
|
)
|
|
|
|
// 318 RPL_ENDOFWHOIS
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfWhois, nick,
|
|
[]string{queryNick},
|
|
"End of /WHOIS list",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) deliverWhoisChannels(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
targetSID int64,
|
|
) {
|
|
channels, chanErr := hdlr.params.Database.
|
|
GetSessionChannels(ctx, targetSID)
|
|
if chanErr != nil || len(channels) == 0 {
|
|
return
|
|
}
|
|
|
|
chanNames := make([]string, 0, len(channels))
|
|
|
|
for _, chanInfo := range channels {
|
|
chanNames = append(chanNames, chanInfo.Name)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisChannels, nick,
|
|
[]string{queryNick},
|
|
strings.Join(chanNames, " "),
|
|
)
|
|
}
|
|
|
|
// handleWho handles the WHO command.
|
|
func (hdlr *Handlers) handleWho(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
) {
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdWho},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
ctx := request.Context()
|
|
srvName := hdlr.serverName()
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
chID, err := hdlr.params.Database.GetChannelByName(
|
|
ctx, channel,
|
|
)
|
|
if err != nil {
|
|
// 315 RPL_ENDOFWHO (empty result)
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfWho, nick,
|
|
[]string{target},
|
|
"End of /WHO list",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
|
|
return
|
|
}
|
|
|
|
members, memErr := hdlr.params.Database.ChannelMembers(
|
|
ctx, chID,
|
|
)
|
|
if memErr == nil {
|
|
for _, mem := range members {
|
|
// 352 RPL_WHOREPLY
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoReply, nick,
|
|
[]string{
|
|
channel, mem.Nick, srvName,
|
|
srvName, mem.Nick, "H",
|
|
},
|
|
"0 "+mem.Nick,
|
|
)
|
|
}
|
|
}
|
|
|
|
// 315 RPL_ENDOFWHO
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfWho, nick,
|
|
[]string{channel},
|
|
"End of /WHO list",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleLusers handles the LUSERS command.
|
|
func (hdlr *Handlers) handleLusers(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick string,
|
|
) {
|
|
hdlr.deliverLusers(
|
|
request.Context(), clientID, nick,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
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, irc.CmdQuit, nick, "",
|
|
nil, 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
|
|
}
|
|
|
|
resp := map[string]any{
|
|
"name": hdlr.params.Config.ServerName,
|
|
"version": hdlr.params.Globals.Version,
|
|
"motd": hdlr.params.Config.MOTD,
|
|
"users": users,
|
|
}
|
|
|
|
if hdlr.params.Config.HashcashBits > 0 {
|
|
resp["hashcash_bits"] = hdlr.params.Config.HashcashBits
|
|
}
|
|
|
|
hdlr.respondJSON(
|
|
writer, request, resp, http.StatusOK,
|
|
)
|
|
}
|
|
}
|