All checks were successful
check / check (push) Successful in 1m14s
handleTopic did not verify that the requesting user was a member of the channel before allowing them to set a topic. Any authenticated user could set the topic on any channel they hadn't joined. Add an IsChannelMember check after resolving the channel and before calling executeTopic, mirroring the existing pattern in handleChannelMsg. Non-members now receive ERR_NOTONCHANNEL (442). Add TestTopicNonMember to verify the fix.
2631 lines
52 KiB
Go
2631 lines
52 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.respondJSON(writer, request, map[string]any{
|
|
"error": "not registered",
|
|
"numeric": irc.ErrNotRegistered,
|
|
}, 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.CmdAway:
|
|
hdlr.handleAway(
|
|
writer, request,
|
|
sessionID, clientID, nick, bodyLines,
|
|
)
|
|
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.ErrNoRecipient, nick, []string{command},
|
|
"No recipient given",
|
|
)
|
|
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.ErrNoTextToSend, nick, []string{command},
|
|
"No text to send",
|
|
)
|
|
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.ErrCannotSendToChan, nick, []string{target},
|
|
"Cannot send to 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
|
|
}
|
|
|
|
// If the target is away, send RPL_AWAY to the sender.
|
|
awayMsg, awayErr := hdlr.params.Database.GetAway(
|
|
request.Context(), targetSID,
|
|
)
|
|
if awayErr == nil && awayMsg != "" {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.RplAway, nick,
|
|
[]string{target}, awayMsg,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
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()
|
|
|
|
hdlr.deliverTopicNumerics(
|
|
ctx, clientID, sessionID, nick, channel, chID,
|
|
)
|
|
|
|
hdlr.deliverNamesNumerics(
|
|
ctx, clientID, nick, channel, chID,
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
// deliverTopicNumerics sends RPL_TOPIC or RPL_NOTOPIC,
|
|
// plus RPL_TOPICWHOTIME when topic metadata is available.
|
|
func (hdlr *Handlers) deliverTopicNumerics(
|
|
ctx context.Context,
|
|
clientID, sessionID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
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,
|
|
)
|
|
|
|
topicMeta, tmErr := hdlr.params.Database.
|
|
GetTopicMeta(ctx, chID)
|
|
if tmErr == nil && topicMeta != nil {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID,
|
|
irc.RplTopicWhoTime, nick,
|
|
[]string{
|
|
channel,
|
|
topicMeta.SetBy,
|
|
strconv.FormatInt(
|
|
topicMeta.SetAt.Unix(), 10,
|
|
),
|
|
},
|
|
"",
|
|
)
|
|
}
|
|
} else {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNoTopic, nick,
|
|
[]string{channel}, "No topic is set",
|
|
)
|
|
}
|
|
}
|
|
|
|
// deliverNamesNumerics sends RPL_NAMREPLY and
|
|
// RPL_ENDOFNAMES for a channel.
|
|
func (hdlr *Handlers) deliverNamesNumerics(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
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",
|
|
)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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{channel},
|
|
"You're not on that 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.SetTopicMeta(
|
|
request.Context(), channel, topic, nick,
|
|
)
|
|
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,
|
|
)
|
|
|
|
// 333 RPL_TOPICWHOTIME
|
|
topicMeta, tmErr := hdlr.params.Database.
|
|
GetTopicMeta(request.Context(), chID)
|
|
if tmErr == nil && topicMeta != nil {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.RplTopicWhoTime, nick,
|
|
[]string{
|
|
channel,
|
|
topicMeta.SetBy,
|
|
strconv.FormatInt(
|
|
topicMeta.SetAt.Unix(), 10,
|
|
),
|
|
},
|
|
"",
|
|
)
|
|
}
|
|
|
|
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",
|
|
)
|
|
|
|
// 317 RPL_WHOISIDLE
|
|
hdlr.deliverWhoisIdle(
|
|
ctx, clientID, nick, queryNick, targetSID,
|
|
)
|
|
|
|
// 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,
|
|
)
|
|
}
|
|
}
|
|
|
|
// handleAway handles the AWAY command. An empty body
|
|
// clears the away status; a non-empty body sets it.
|
|
func (hdlr *Handlers) handleAway(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick string,
|
|
bodyLines func() []string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
lines := bodyLines()
|
|
|
|
awayMsg := ""
|
|
if len(lines) > 0 {
|
|
awayMsg = strings.Join(lines, " ")
|
|
}
|
|
|
|
err := hdlr.params.Database.SetAway(
|
|
ctx, sessionID, awayMsg,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error("set away failed", "error", err)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if awayMsg == "" {
|
|
// 305 RPL_UNAWAY
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplUnaway, nick, nil,
|
|
"You are no longer marked as being away",
|
|
)
|
|
} else {
|
|
// 306 RPL_NOWAWAY
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNowAway, nick, nil,
|
|
"You have been marked as being away",
|
|
)
|
|
}
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// deliverWhoisIdle sends RPL_WHOISIDLE (317) with idle
|
|
// time and signon time.
|
|
func (hdlr *Handlers) deliverWhoisIdle(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
targetSID int64,
|
|
) {
|
|
lastSeen, lsErr := hdlr.params.Database.
|
|
GetSessionLastSeen(ctx, targetSID)
|
|
if lsErr != nil {
|
|
return
|
|
}
|
|
|
|
createdAt, caErr := hdlr.params.Database.
|
|
GetSessionCreatedAt(ctx, targetSID)
|
|
if caErr != nil {
|
|
return
|
|
}
|
|
|
|
idleSeconds := int64(time.Since(lastSeen).Seconds())
|
|
if idleSeconds < 0 {
|
|
idleSeconds = 0
|
|
}
|
|
|
|
signonUnix := strconv.FormatInt(
|
|
createdAt.Unix(), 10,
|
|
)
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisIdle, nick,
|
|
[]string{
|
|
queryNick,
|
|
strconv.FormatInt(idleSeconds, 10),
|
|
signonUnix,
|
|
},
|
|
"seconds idle, signon time",
|
|
)
|
|
}
|