All checks were successful
check / check (push) Successful in 59s
1. ISUPPORT/applyChannelModes: extend IRC MODE handler to support +i/-i, +s/-s, +n/-n (routed through svc.SetChannelFlag), and +H/-H (hashcash bits with parameter parsing). Add 'n' (no external messages) as a proper DB-backed channel flag with is_no_external column (default: on). Update IRC ISUPPORT to CHANMODES=,,H,imnst to match actual support. 2. QueryChannelMode: rewrite to return complete mode string including all boolean flags (n, i, m, s, t) and parameterized modes (k, l, H), matching the HTTP handler's buildChannelModeString logic. Simplify buildChannelModeString to delegate to QueryChannelMode for consistency. 3. Service struct encapsulation: change exported fields (DB, Broker, Config, Log) to unexported (db, broker, config, log). Add NewTestService constructor for use by external test packages. Update ircserver export_test.go to use the new constructor. Closes #89
4038 lines
80 KiB
Go
4038 lines
80 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/neoirc/internal/db"
|
|
"git.eeqj.de/sneak/neoirc/internal/hashcash"
|
|
"git.eeqj.de/sneak/neoirc/internal/service"
|
|
"git.eeqj.de/sneak/neoirc/pkg/irc"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
var (
|
|
errHashcashRequired = errors.New("hashcash required")
|
|
errHashcashReused = errors.New("hashcash reused")
|
|
)
|
|
|
|
var validNickRe = regexp.MustCompile(
|
|
`^[a-zA-Z_][a-zA-Z0-9_\-\[\]\\^{}|` + "`" + `]{0,31}$`,
|
|
)
|
|
|
|
var validChannelRe = regexp.MustCompile(
|
|
`^#[a-zA-Z0-9_\-]{1,63}$`,
|
|
)
|
|
|
|
var validUsernameRe = regexp.MustCompile(
|
|
`^[a-zA-Z0-9_\-\[\]\\^{}|` + "`" + `]{1,32}$`,
|
|
)
|
|
|
|
const dnsLookupTimeout = 3 * time.Second
|
|
|
|
const (
|
|
maxLongPollTimeout = 30
|
|
pollMessageLimit = 100
|
|
defaultMaxBodySize = 4096
|
|
defaultHistLimit = 50
|
|
maxHistLimit = 500
|
|
authCookieName = "neoirc_auth"
|
|
)
|
|
|
|
func (hdlr *Handlers) maxBodySize() int64 {
|
|
if hdlr.params.Config.MaxMessageSize > 0 {
|
|
return int64(hdlr.params.Config.MaxMessageSize)
|
|
}
|
|
|
|
return defaultMaxBodySize
|
|
}
|
|
|
|
// clientIP extracts the connecting client's IP address
|
|
// from the request, checking X-Forwarded-For and
|
|
// X-Real-IP headers before falling back to RemoteAddr.
|
|
func clientIP(request *http.Request) string {
|
|
if forwarded := request.Header.Get("X-Forwarded-For"); forwarded != "" {
|
|
// X-Forwarded-For can contain a comma-separated list;
|
|
// the first entry is the original client.
|
|
parts := strings.SplitN(forwarded, ",", 2) //nolint:mnd
|
|
ip := strings.TrimSpace(parts[0])
|
|
|
|
if ip != "" {
|
|
return ip
|
|
}
|
|
}
|
|
|
|
if realIP := request.Header.Get("X-Real-IP"); realIP != "" {
|
|
return strings.TrimSpace(realIP)
|
|
}
|
|
|
|
host, _, err := net.SplitHostPort(request.RemoteAddr)
|
|
if err != nil {
|
|
return request.RemoteAddr
|
|
}
|
|
|
|
return host
|
|
}
|
|
|
|
// resolveHostname performs a reverse DNS lookup on the
|
|
// given IP address. Returns the first PTR record with the
|
|
// trailing dot stripped, or the raw IP if lookup fails.
|
|
func resolveHostname(
|
|
reqCtx context.Context,
|
|
addr string,
|
|
) string {
|
|
resolver := &net.Resolver{} //nolint:exhaustruct // using default resolver
|
|
|
|
ctx, cancel := context.WithTimeout(
|
|
reqCtx, dnsLookupTimeout,
|
|
)
|
|
defer cancel()
|
|
|
|
names, err := resolver.LookupAddr(ctx, addr)
|
|
if err != nil || len(names) == 0 {
|
|
return addr
|
|
}
|
|
|
|
return strings.TrimSuffix(names[0], ".")
|
|
}
|
|
|
|
// authSession extracts the session from the auth cookie.
|
|
func (hdlr *Handlers) authSession(
|
|
request *http.Request,
|
|
) (int64, int64, string, error) {
|
|
cookie, err := request.Cookie(authCookieName)
|
|
if err != nil || cookie.Value == "" {
|
|
return 0, 0, "", errUnauthorized
|
|
}
|
|
|
|
sessionID, clientID, nick, err :=
|
|
hdlr.params.Database.GetSessionByToken(
|
|
request.Context(), cookie.Value,
|
|
)
|
|
if err != nil {
|
|
return 0, 0, "", fmt.Errorf("auth: %w", err)
|
|
}
|
|
|
|
return sessionID, clientID, nick, nil
|
|
}
|
|
|
|
// setAuthCookie sets the authentication cookie on the
|
|
// response.
|
|
func (hdlr *Handlers) setAuthCookie(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
token string,
|
|
) {
|
|
secure := request.TLS != nil ||
|
|
request.Header.Get("X-Forwarded-Proto") == "https"
|
|
|
|
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
|
|
Name: authCookieName,
|
|
Value: token,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteStrictMode,
|
|
})
|
|
}
|
|
|
|
// clearAuthCookie removes the authentication cookie from
|
|
// the client.
|
|
func (hdlr *Handlers) clearAuthCookie(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
secure := request.TLS != nil ||
|
|
request.Header.Get("X-Forwarded-Proto") == "https"
|
|
|
|
http.SetCookie(writer, &http.Cookie{ //nolint:exhaustruct // optional fields
|
|
Name: authCookieName,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: secure,
|
|
SameSite: http.SameSiteStrictMode,
|
|
MaxAge: -1,
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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"`
|
|
Username string `json:"username,omitempty"`
|
|
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
|
|
}
|
|
|
|
if !hdlr.validateHashcash(
|
|
writer, request, payload.Hashcash,
|
|
) {
|
|
return
|
|
}
|
|
|
|
payload.Nick = strings.TrimSpace(payload.Nick)
|
|
|
|
if !validNickRe.MatchString(payload.Nick) {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid nick format",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
username := resolveUsername(
|
|
payload.Username, payload.Nick,
|
|
)
|
|
|
|
if !validUsernameRe.MatchString(username) {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid username format",
|
|
http.StatusBadRequest,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeCreateSession(
|
|
writer, request, payload.Nick, username,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeCreateSession(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
nick, username string,
|
|
) {
|
|
remoteIP := clientIP(request)
|
|
|
|
hostname := resolveHostname(
|
|
request.Context(), remoteIP,
|
|
)
|
|
|
|
sessionID, clientID, token, err :=
|
|
hdlr.params.Database.CreateSession(
|
|
request.Context(),
|
|
nick, username, hostname, remoteIP,
|
|
)
|
|
if err != nil {
|
|
hdlr.handleCreateSessionError(
|
|
writer, request, err,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.stats.IncrSessions()
|
|
hdlr.stats.IncrConnections()
|
|
|
|
hdlr.deliverMOTD(request, clientID, sessionID, nick)
|
|
|
|
hdlr.setAuthCookie(writer, request, token)
|
|
|
|
hdlr.respondJSON(writer, request, map[string]any{
|
|
"id": sessionID,
|
|
"nick": nick,
|
|
}, http.StatusCreated)
|
|
}
|
|
|
|
// validateHashcash validates a hashcash stamp if required.
|
|
// Returns false if validation failed and a response was
|
|
// already sent.
|
|
func (hdlr *Handlers) validateHashcash(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
stamp string,
|
|
) bool {
|
|
if hdlr.params.Config.HashcashBits == 0 {
|
|
return true
|
|
}
|
|
|
|
if stamp == "" {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"hashcash proof-of-work required",
|
|
http.StatusPaymentRequired,
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
err := hdlr.hashcashVal.Validate(
|
|
stamp, hdlr.params.Config.HashcashBits,
|
|
)
|
|
if err != nil {
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"invalid hashcash stamp: "+err.Error(),
|
|
http.StatusPaymentRequired,
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// resolveUsername returns the trimmed username, defaulting
|
|
// to the nick if empty.
|
|
func resolveUsername(username, nick string) string {
|
|
username = strings.TrimSpace(username)
|
|
if username == "" {
|
|
return nick
|
|
}
|
|
|
|
return username
|
|
}
|
|
|
|
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, "", "ikmnostl"},
|
|
"",
|
|
)
|
|
|
|
// 005 RPL_ISUPPORT
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplIsupport, nick,
|
|
[]string{
|
|
"CHANTYPES=#",
|
|
"NICKLEN=32",
|
|
"PREFIX=(ov)@+",
|
|
"CHANMODES=b,k,Hl,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
|
|
operCount, operErr := hdlr.params.Database.
|
|
GetOperCount(ctx)
|
|
if operErr != nil {
|
|
hdlr.log.Error(
|
|
"lusers oper count", "error", operErr,
|
|
)
|
|
|
|
operCount = 0
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplLuserOp, nick,
|
|
[]string{strconv.FormatInt(operCount, 10)},
|
|
"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, payload.Meta, bodyLines,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (hdlr *Handlers) dispatchCommand(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
meta 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, meta, bodyLines,
|
|
)
|
|
case irc.CmdJoin:
|
|
hdlr.handleJoin(
|
|
writer, request,
|
|
sessionID, clientID, nick, target,
|
|
bodyLines,
|
|
)
|
|
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.CmdPass:
|
|
hdlr.handlePass(
|
|
writer, request,
|
|
sessionID, clientID, nick, bodyLines,
|
|
)
|
|
case irc.CmdTopic:
|
|
hdlr.handleTopic(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
target, body, bodyLines,
|
|
)
|
|
case irc.CmdInvite:
|
|
hdlr.handleInvite(
|
|
writer, request,
|
|
sessionID, clientID, nick, bodyLines,
|
|
)
|
|
case irc.CmdKick:
|
|
hdlr.handleKick(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
target, body, bodyLines,
|
|
)
|
|
case irc.CmdQuit:
|
|
hdlr.handleQuit(
|
|
writer, request, sessionID, nick, body,
|
|
)
|
|
case irc.CmdOper:
|
|
hdlr.handleOper(
|
|
writer, request,
|
|
sessionID, clientID, nick, bodyLines,
|
|
)
|
|
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,
|
|
meta 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
|
|
}
|
|
|
|
hdlr.stats.IncrMessages()
|
|
|
|
if strings.HasPrefix(target, "#") {
|
|
hdlr.handleChannelMsg(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, body, meta,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.handleDirectMsg(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
command, target, body, meta,
|
|
)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// handleServiceError maps a service-layer error to an IRC
|
|
// numeric reply or a generic HTTP 500 response. Returns
|
|
// true if an error was handled (response sent).
|
|
func (hdlr *Handlers) handleServiceError(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
nick string,
|
|
err error,
|
|
) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
var ircErr *service.IRCError
|
|
if errors.As(err, &ircErr) {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
ircErr.Code, nick, ircErr.Params,
|
|
ircErr.Message,
|
|
)
|
|
|
|
return true
|
|
}
|
|
|
|
hdlr.log.Error(
|
|
"service error", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return true
|
|
}
|
|
|
|
func (hdlr *Handlers) handleChannelMsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
meta json.RawMessage,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
// Hashcash validation is HTTP-specific; needs chID.
|
|
isNotice := command == irc.CmdNotice
|
|
|
|
if !isNotice {
|
|
chID, chErr := hdlr.params.Database.
|
|
GetChannelByName(ctx, target)
|
|
if chErr == nil {
|
|
hashcashErr := hdlr.validateChannelHashcash(
|
|
request, clientID, sessionID,
|
|
writer, nick, target, body, meta, chID,
|
|
)
|
|
if hashcashErr != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Delegate validation + fan-out to service layer.
|
|
dbID, uuid, err := hdlr.svc.SendChannelMessage(
|
|
ctx, sessionID, nick,
|
|
command, target, body, meta,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
// HTTP echo: enqueue to sender so all their clients
|
|
// see the message in long-poll responses.
|
|
_ = hdlr.params.Database.EnqueueToSession(
|
|
ctx, sessionID, dbID,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"id": uuid, "status": "sent"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// validateChannelHashcash checks whether the channel
|
|
// requires hashcash proof-of-work for messages and
|
|
// validates the stamp from the message meta field.
|
|
// Returns nil on success or if the channel has no
|
|
// hashcash requirement. On failure, it sends the
|
|
// appropriate IRC error and returns a non-nil error.
|
|
func (hdlr *Handlers) validateChannelHashcash(
|
|
request *http.Request,
|
|
clientID, sessionID int64,
|
|
writer http.ResponseWriter,
|
|
nick, target string,
|
|
body json.RawMessage,
|
|
meta json.RawMessage,
|
|
chID int64,
|
|
) error {
|
|
ctx := request.Context()
|
|
|
|
bits, bitsErr := hdlr.params.Database.GetChannelHashcashBits(
|
|
ctx, chID,
|
|
)
|
|
if bitsErr != nil {
|
|
hdlr.log.Error(
|
|
"get channel hashcash bits", "error", bitsErr,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return fmt.Errorf("channel hashcash bits: %w", bitsErr)
|
|
}
|
|
|
|
if bits <= 0 {
|
|
return nil
|
|
}
|
|
|
|
stamp := hdlr.extractHashcashFromMeta(meta)
|
|
if stamp == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrCannotSendToChan, nick, []string{target},
|
|
"Channel requires hashcash proof-of-work",
|
|
)
|
|
|
|
return errHashcashRequired
|
|
}
|
|
|
|
return hdlr.verifyChannelStamp(
|
|
request, writer,
|
|
clientID, sessionID,
|
|
nick, target, body, stamp, bits,
|
|
)
|
|
}
|
|
|
|
// verifyChannelStamp validates a channel hashcash stamp
|
|
// and checks for replay attacks.
|
|
func (hdlr *Handlers) verifyChannelStamp(
|
|
request *http.Request,
|
|
writer http.ResponseWriter,
|
|
clientID, sessionID int64,
|
|
nick, target string,
|
|
body json.RawMessage,
|
|
stamp string,
|
|
bits int,
|
|
) error {
|
|
ctx := request.Context()
|
|
bodyHashStr := hashcash.BodyHash(body)
|
|
|
|
valErr := hdlr.channelHashcash.ValidateStamp(
|
|
stamp, bits, target, bodyHashStr,
|
|
)
|
|
if valErr != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrCannotSendToChan, nick, []string{target},
|
|
"Invalid hashcash: "+valErr.Error(),
|
|
)
|
|
|
|
return fmt.Errorf("channel hashcash: %w", valErr)
|
|
}
|
|
|
|
stampKey := hashcash.StampHash(stamp)
|
|
|
|
spent, spentErr := hdlr.params.Database.IsHashcashSpent(
|
|
ctx, stampKey,
|
|
)
|
|
if spentErr != nil {
|
|
hdlr.log.Error(
|
|
"check spent hashcash", "error", spentErr,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return fmt.Errorf("check spent hashcash: %w", spentErr)
|
|
}
|
|
|
|
if spent {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrCannotSendToChan, nick, []string{target},
|
|
"Hashcash stamp already used",
|
|
)
|
|
|
|
return errHashcashReused
|
|
}
|
|
|
|
recordErr := hdlr.params.Database.RecordSpentHashcash(
|
|
ctx, stampKey,
|
|
)
|
|
if recordErr != nil {
|
|
hdlr.log.Error(
|
|
"record spent hashcash", "error", recordErr,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractHashcashFromMeta parses the meta JSON and
|
|
// returns the hashcash stamp string, or empty string
|
|
// if not present.
|
|
func (hdlr *Handlers) extractHashcashFromMeta(
|
|
meta json.RawMessage,
|
|
) string {
|
|
if len(meta) == 0 {
|
|
return ""
|
|
}
|
|
|
|
var metaMap map[string]json.RawMessage
|
|
|
|
err := json.Unmarshal(meta, &metaMap)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
raw, ok := metaMap["hashcash"]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
var stamp string
|
|
|
|
err = json.Unmarshal(raw, &stamp)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return stamp
|
|
}
|
|
|
|
func (hdlr *Handlers) handleDirectMsg(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, command, target string,
|
|
body json.RawMessage,
|
|
meta json.RawMessage,
|
|
) {
|
|
result, err := hdlr.svc.SendDirectMessage(
|
|
request.Context(), sessionID, nick,
|
|
command, target, body, meta,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
// Per RFC 2812: NOTICE must NOT trigger auto-replies
|
|
// including RPL_AWAY.
|
|
if command != irc.CmdNotice && result.AwayMsg != "" {
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.RplAway, nick,
|
|
[]string{target}, result.AwayMsg,
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"id": result.UUID, "status": "sent",
|
|
},
|
|
http.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleJoin(
|
|
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.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
|
|
}
|
|
|
|
// Extract key from body lines (JOIN #channel key).
|
|
var suppliedKey string
|
|
|
|
lines := bodyLines()
|
|
if len(lines) > 0 {
|
|
suppliedKey = lines[0]
|
|
}
|
|
|
|
hdlr.executeJoin(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, suppliedKey,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) executeJoin(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel, suppliedKey string,
|
|
) {
|
|
result, err := hdlr.svc.JoinChannel(
|
|
request.Context(), sessionID, nick,
|
|
channel, suppliedKey,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
hdlr.deliverJoinNumerics(
|
|
request, clientID, sessionID, nick,
|
|
channel, result.ChannelID,
|
|
)
|
|
|
|
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",
|
|
)
|
|
}
|
|
}
|
|
|
|
// memberPrefix returns the IRC prefix character for a
|
|
// channel member: "@" for operators, "+" for voiced, or
|
|
// "" for regular members.
|
|
func memberPrefix(mem *db.MemberInfo) string {
|
|
if mem.IsOperator {
|
|
return "@"
|
|
}
|
|
|
|
if mem.IsVoiced {
|
|
return "+"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// 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 {
|
|
entries := make([]string, 0, len(members))
|
|
|
|
for idx := range members {
|
|
prefix := memberPrefix(&members[idx])
|
|
entries = append(
|
|
entries,
|
|
prefix+members[idx].Hostmask(),
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNamReply, nick,
|
|
[]string{"=", channel},
|
|
strings.Join(entries, " "),
|
|
)
|
|
}
|
|
|
|
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.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdPart},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
// Extract reason from body for the service call.
|
|
reason := ""
|
|
if body != nil {
|
|
var lines []string
|
|
if json.Unmarshal(body, &lines) == nil &&
|
|
len(lines) > 0 {
|
|
reason = lines[0]
|
|
}
|
|
}
|
|
|
|
err := hdlr.svc.PartChannel(
|
|
request.Context(), sessionID,
|
|
nick, channel, reason,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
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.svc.ChangeNick(
|
|
request.Context(), sessionID, nick, newNick,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"status": "ok", "nick": newNick,
|
|
},
|
|
http.StatusOK)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleTopic(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
_ 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
|
|
}
|
|
|
|
topic := strings.Join(lines, " ")
|
|
|
|
err := hdlr.svc.SetTopic(
|
|
request.Context(), sessionID,
|
|
nick, channel, topic,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
hdlr.deliverSetTopicNumerics(
|
|
request.Context(), clientID, sessionID,
|
|
nick, channel, topic,
|
|
)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{
|
|
"status": "ok", "topic": topic,
|
|
},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// deliverSetTopicNumerics sends RPL_TOPIC and
|
|
// RPL_TOPICWHOTIME to the client after a topic change.
|
|
func (hdlr *Handlers) deliverSetTopicNumerics(
|
|
ctx context.Context,
|
|
clientID, sessionID int64,
|
|
nick, channel, topic string,
|
|
) {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplTopic, nick,
|
|
[]string{channel}, topic,
|
|
)
|
|
|
|
chID, chErr := hdlr.params.Database.GetChannelByName(
|
|
ctx, channel,
|
|
)
|
|
if chErr == nil {
|
|
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,
|
|
),
|
|
},
|
|
"",
|
|
)
|
|
}
|
|
}
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
}
|
|
|
|
// 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,
|
|
) {
|
|
reason := "Client quit"
|
|
if body != nil {
|
|
var lines []string
|
|
if json.Unmarshal(body, &lines) == nil &&
|
|
len(lines) > 0 {
|
|
reason = lines[0]
|
|
}
|
|
}
|
|
|
|
hdlr.svc.BroadcastQuit(
|
|
request.Context(), sessionID, nick, reason,
|
|
)
|
|
|
|
hdlr.clearAuthCookie(writer, request)
|
|
|
|
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
|
|
}
|
|
|
|
hdlr.handleChannelMode(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel,
|
|
bodyLines,
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) handleChannelMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
bodyLines func() []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
|
|
}
|
|
|
|
lines := bodyLines()
|
|
if len(lines) > 0 {
|
|
hdlr.applyChannelMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, lines,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.queryChannelMode(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
)
|
|
}
|
|
|
|
// buildChannelModeString constructs the current mode
|
|
// string for a channel by delegating to the service
|
|
// layer's QueryChannelMode, which returns the complete
|
|
// mode string including all flags and parameters.
|
|
func (hdlr *Handlers) buildChannelModeString(
|
|
ctx context.Context,
|
|
chID int64,
|
|
) string {
|
|
return hdlr.svc.QueryChannelMode(ctx, chID)
|
|
}
|
|
|
|
// queryChannelMode sends RPL_CHANNELMODEIS and
|
|
// RPL_CREATIONTIME for a channel. Includes +t, +m, +H
|
|
// as appropriate.
|
|
func (hdlr *Handlers) queryChannelMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
modeStr := hdlr.buildChannelModeString(ctx, chID)
|
|
|
|
// 324 RPL_CHANNELMODEIS
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplChannelModeIs, nick,
|
|
[]string{channel, modeStr}, "",
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// requireChannelOp checks that the session has +o in the
|
|
// channel. If not, it sends ERR_CHANOPRIVSNEEDED and
|
|
// returns false.
|
|
// applyChannelMode handles setting channel modes.
|
|
// Supports +o/-o, +v/-v, +m/-m, +t/-t, +H/-H.
|
|
func (hdlr *Handlers) applyChannelMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
) {
|
|
modeStr := modeArgs[0]
|
|
|
|
if hdlr.applyUserModeIfMatched(
|
|
writer, request, sessionID, clientID,
|
|
nick, channel, chID, modeStr, modeArgs,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if hdlr.applyChannelFlagIfMatched(
|
|
writer, request, sessionID, clientID,
|
|
nick, channel, chID, modeStr,
|
|
) {
|
|
return
|
|
}
|
|
|
|
hdlr.applyParameterizedMode(
|
|
writer, request, sessionID, clientID,
|
|
nick, channel, chID, modeStr, modeArgs,
|
|
)
|
|
}
|
|
|
|
// applyUserModeIfMatched handles +o/-o and +v/-v.
|
|
// Returns true if the mode was handled.
|
|
func (hdlr *Handlers) applyUserModeIfMatched(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeStr string,
|
|
modeArgs []string,
|
|
) bool {
|
|
switch modeStr {
|
|
case "+o", "-o":
|
|
hdlr.applyUserMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs, true,
|
|
)
|
|
|
|
return true
|
|
case "+v", "-v":
|
|
hdlr.applyUserMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs, false,
|
|
)
|
|
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// applyChannelFlagIfMatched handles simple boolean modes
|
|
// (+m/-m, +t/-t, +i/-i, +s/-s).
|
|
// Returns true if the mode was handled.
|
|
func (hdlr *Handlers) applyChannelFlagIfMatched(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeStr string,
|
|
) bool {
|
|
flagMap := map[string]struct {
|
|
flag string
|
|
setting bool
|
|
}{
|
|
"+m": {"m", true}, "-m": {"m", false},
|
|
"+t": {"t", true}, "-t": {"t", false},
|
|
"+i": {"i", true}, "-i": {"i", false},
|
|
"+s": {"s", true}, "-s": {"s", false},
|
|
}
|
|
|
|
entry, exists := flagMap[modeStr]
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
hdlr.setChannelFlag(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, entry.flag, entry.setting,
|
|
)
|
|
|
|
return true
|
|
}
|
|
|
|
// applyParameterizedMode handles modes that take
|
|
// parameters (+k/-k, +l/-l, +b/-b, +H/-H) and
|
|
// unknown modes.
|
|
func (hdlr *Handlers) applyParameterizedMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeStr string,
|
|
modeArgs []string,
|
|
) {
|
|
switch modeStr {
|
|
case "+k":
|
|
hdlr.setChannelKeyMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs,
|
|
)
|
|
case "-k":
|
|
hdlr.clearChannelKeyMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID,
|
|
)
|
|
case "+l":
|
|
hdlr.setChannelLimitMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs,
|
|
)
|
|
case "-l":
|
|
hdlr.clearChannelLimitMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID,
|
|
)
|
|
case "+b":
|
|
hdlr.handleBanMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs, true,
|
|
)
|
|
case "-b":
|
|
hdlr.handleBanMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs, false,
|
|
)
|
|
case "+H":
|
|
hdlr.setHashcashMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID, modeArgs,
|
|
)
|
|
case "-H":
|
|
hdlr.clearHashcashMode(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
channel, chID,
|
|
)
|
|
default:
|
|
hdlr.enqueueNumeric(
|
|
request.Context(), clientID,
|
|
irc.ErrUnknownMode, nick,
|
|
[]string{modeStr},
|
|
"is unknown mode char to me",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "error"},
|
|
http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// applyUserMode handles +o/-o and +v/-v mode changes.
|
|
// isOperMode=true for +o/-o, false for +v/-v.
|
|
func (hdlr *Handlers) applyUserMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
isOperMode bool,
|
|
) {
|
|
// Validate operator status via service.
|
|
_, opErr := hdlr.svc.ValidateChannelOp(
|
|
request.Context(), sessionID, channel,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, opErr,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if len(modeArgs) < 2 { //nolint:mnd // mode + nick
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdMode},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
targetNick := modeArgs[1]
|
|
setting := strings.HasPrefix(modeArgs[0], "+")
|
|
|
|
var modeChar rune
|
|
if isOperMode {
|
|
modeChar = 'o'
|
|
} else {
|
|
modeChar = 'v'
|
|
}
|
|
|
|
err := hdlr.svc.ApplyMemberMode(
|
|
request.Context(), chID, channel,
|
|
targetNick, modeChar, setting,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
modeText := modeArgs[0] + " " + targetNick
|
|
hdlr.svc.BroadcastMode(
|
|
request.Context(), nick, channel, chID,
|
|
modeText,
|
|
)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// setChannelFlag handles +m/-m and +t/-t mode changes.
|
|
func (hdlr *Handlers) setChannelFlag(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
flag string,
|
|
setting bool,
|
|
) {
|
|
// Validate operator status via service.
|
|
_, opErr := hdlr.svc.ValidateChannelOp(
|
|
request.Context(), sessionID, channel,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, opErr,
|
|
) {
|
|
return
|
|
}
|
|
|
|
err := hdlr.svc.SetChannelFlag(
|
|
request.Context(), chID, rune(flag[0]), setting,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
modeStr := "+" + flag
|
|
if !setting {
|
|
modeStr = "-" + flag
|
|
}
|
|
|
|
hdlr.svc.BroadcastMode(
|
|
request.Context(), nick, channel, chID,
|
|
modeStr,
|
|
)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
const (
|
|
// minHashcashBits is the minimum allowed hashcash
|
|
// difficulty for channels.
|
|
minHashcashBits = 1
|
|
// maxHashcashBits is the maximum allowed hashcash
|
|
// difficulty for channels.
|
|
maxHashcashBits = 40
|
|
)
|
|
|
|
// setHashcashMode handles MODE #channel +H <bits>.
|
|
func (hdlr *Handlers) setHashcashMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
// Validate operator status via service.
|
|
_, opErr := hdlr.svc.ValidateChannelOp(
|
|
ctx, sessionID, channel,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, opErr,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if len(modeArgs) < 2 { //nolint:mnd // +H requires a bits arg
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick, []string{irc.CmdMode},
|
|
"Not enough parameters (+H requires bits)",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
bits, err := strconv.Atoi(modeArgs[1])
|
|
if err != nil || bits < minHashcashBits ||
|
|
bits > maxHashcashBits {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrUnknownMode, nick, []string{"+H"},
|
|
fmt.Sprintf(
|
|
"Invalid hashcash bits (must be %d-%d)",
|
|
minHashcashBits, maxHashcashBits,
|
|
),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err = hdlr.params.Database.SetChannelHashcashBits(
|
|
ctx, chID, bits,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"set channel hashcash bits", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
modeStr := hdlr.buildChannelModeString(ctx, chID)
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplChannelModeIs, nick,
|
|
[]string{channel, modeStr}, "",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// clearHashcashMode handles MODE #channel -H.
|
|
func (hdlr *Handlers) clearHashcashMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
// Validate operator status via service.
|
|
_, opErr := hdlr.svc.ValidateChannelOp(
|
|
ctx, sessionID, channel,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, opErr,
|
|
) {
|
|
return
|
|
}
|
|
|
|
err := hdlr.params.Database.SetChannelHashcashBits(
|
|
ctx, chID, 0,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"clear channel hashcash bits", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
modeStr := hdlr.buildChannelModeString(ctx, chID)
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplChannelModeIs, nick,
|
|
[]string{channel, modeStr}, "",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleBanMode handles MODE #channel +b/-b [mask].
|
|
// +b with no argument lists bans; +b with argument adds
|
|
// a ban; -b removes a ban.
|
|
func (hdlr *Handlers) handleBanMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
adding bool,
|
|
) {
|
|
// +b with no argument: list bans.
|
|
if adding && len(modeArgs) < 2 {
|
|
hdlr.listBans(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if len(modeArgs) < 2 { //nolint:mnd // mode + mask
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdMode},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.executeBanChange(
|
|
writer, request, nick, channel,
|
|
chID, modeArgs[1], adding,
|
|
)
|
|
}
|
|
|
|
// executeBanChange applies a ban add/remove and
|
|
// broadcasts the mode change.
|
|
func (hdlr *Handlers) executeBanChange(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
nick, channel string,
|
|
chID int64,
|
|
mask string,
|
|
adding bool,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
var err error
|
|
if adding {
|
|
err = hdlr.params.Database.AddChannelBan(
|
|
ctx, chID, mask, nick,
|
|
)
|
|
} else {
|
|
err = hdlr.params.Database.RemoveChannelBan(
|
|
ctx, chID, mask,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"ban mode change failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
modePrefix := "+"
|
|
if !adding {
|
|
modePrefix = "-"
|
|
}
|
|
|
|
memberIDs, _ := hdlr.params.Database.
|
|
GetChannelMemberIDs(ctx, chID)
|
|
|
|
modeBody, mErr := json.Marshal(
|
|
[]string{modePrefix + "b", mask},
|
|
)
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdMode, nick, channel,
|
|
json.RawMessage(modeBody), memberIDs,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// listBans sends RPL_BANLIST (367) and
|
|
// RPL_ENDOFBANLIST (368) for a channel.
|
|
func (hdlr *Handlers) listBans(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
bans, err := hdlr.params.Database.ListChannelBans(
|
|
ctx, chID,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"list bans failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
for _, ban := range bans {
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplBanList, nick,
|
|
[]string{
|
|
channel,
|
|
ban.Mask,
|
|
ban.SetBy,
|
|
strconv.FormatInt(
|
|
ban.CreatedAt.Unix(), 10,
|
|
),
|
|
},
|
|
"",
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplEndOfBanList, nick,
|
|
[]string{channel},
|
|
"End of channel ban list",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// setChannelKeyMode handles MODE #channel +k <key>.
|
|
func (hdlr *Handlers) setChannelKeyMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if len(modeArgs) < 2 { //nolint:mnd // +k requires key arg
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdMode},
|
|
"Not enough parameters (+k requires a key)",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
key := modeArgs[1]
|
|
|
|
err := hdlr.params.Database.SetChannelKey(
|
|
ctx, chID, key,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"set channel key failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// Broadcast +k mode change.
|
|
memberIDs, _ := hdlr.params.Database.
|
|
GetChannelMemberIDs(ctx, chID)
|
|
|
|
modeBody, mErr := json.Marshal([]string{"+k", key})
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdMode, nick, channel,
|
|
json.RawMessage(modeBody), memberIDs,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// clearChannelKeyMode handles MODE #channel -k *.
|
|
func (hdlr *Handlers) clearChannelKeyMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return
|
|
}
|
|
|
|
err := hdlr.params.Database.SetChannelKey(
|
|
ctx, chID, "",
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"clear channel key failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
memberIDs, _ := hdlr.params.Database.
|
|
GetChannelMemberIDs(ctx, chID)
|
|
|
|
modeBody, mErr := json.Marshal([]string{"-k", "*"})
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdMode, nick, channel,
|
|
json.RawMessage(modeBody), memberIDs,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// setChannelLimitMode handles MODE #channel +l <limit>.
|
|
func (hdlr *Handlers) setChannelLimitMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
modeArgs []string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if len(modeArgs) < 2 { //nolint:mnd // +l requires limit arg
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdMode},
|
|
"Not enough parameters (+l requires a limit)",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
limit, parseErr := strconv.Atoi(modeArgs[1])
|
|
if parseErr != nil || limit <= 0 {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrUnknownMode, nick, []string{"+l"},
|
|
"Invalid user limit (must be positive integer)",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := hdlr.params.Database.SetChannelUserLimit(
|
|
ctx, chID, limit,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"set channel user limit failed", "error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
// Broadcast +l mode change.
|
|
memberIDs, _ := hdlr.params.Database.
|
|
GetChannelMemberIDs(ctx, chID)
|
|
|
|
modeBody, mErr := json.Marshal(
|
|
[]string{"+l", modeArgs[1]},
|
|
)
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdMode, nick, channel,
|
|
json.RawMessage(modeBody), memberIDs,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// clearChannelLimitMode handles MODE #channel -l.
|
|
func (hdlr *Handlers) clearChannelLimitMode(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return
|
|
}
|
|
|
|
err := hdlr.params.Database.SetChannelUserLimit(
|
|
ctx, chID, 0,
|
|
)
|
|
if err != nil {
|
|
hdlr.log.Error(
|
|
"clear channel user limit failed",
|
|
"error", err,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
memberIDs, _ := hdlr.params.Database.
|
|
GetChannelMemberIDs(ctx, chID)
|
|
|
|
modeBody, mErr := json.Marshal([]string{"-l"})
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdMode, nick, channel,
|
|
json.RawMessage(modeBody), memberIDs,
|
|
)
|
|
}
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
|
|
// handleInvite processes the INVITE command.
|
|
func (hdlr *Handlers) handleInvite(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick string,
|
|
bodyLines func() []string,
|
|
) {
|
|
lines := bodyLines()
|
|
if len(lines) < 2 { //nolint:mnd // nick + channel
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdInvite},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
targetNick := lines[0]
|
|
channel := lines[1]
|
|
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
chID, targetSID, ok := hdlr.validateInvite(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
targetNick, channel,
|
|
)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
hdlr.executeInvite(
|
|
writer, request,
|
|
sessionID, clientID, nick,
|
|
targetNick, channel, chID, targetSID,
|
|
)
|
|
}
|
|
|
|
// validateInvite checks channel, membership, permissions,
|
|
// and target for an INVITE command.
|
|
func (hdlr *Handlers) validateInvite(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, targetNick, channel string,
|
|
) (int64, int64, bool) {
|
|
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 0, 0, false
|
|
}
|
|
|
|
isMember, memErr := hdlr.params.Database.IsChannelMember(
|
|
ctx, chID, sessionID,
|
|
)
|
|
if memErr != nil || !isMember {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNotOnChannel, nick,
|
|
[]string{channel},
|
|
"You're not on that channel",
|
|
)
|
|
|
|
return 0, 0, false
|
|
}
|
|
|
|
isInviteOnly, ioErr := hdlr.params.Database.
|
|
IsChannelInviteOnly(ctx, chID)
|
|
if ioErr == nil && isInviteOnly {
|
|
if !hdlr.requireChannelOp(
|
|
writer, request,
|
|
sessionID, clientID, nick, channel, chID,
|
|
) {
|
|
return 0, 0, false
|
|
}
|
|
}
|
|
|
|
targetSID, nickErr := hdlr.params.Database.
|
|
GetSessionByNick(ctx, targetNick)
|
|
if nickErr != nil {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNoSuchNick, nick,
|
|
[]string{targetNick},
|
|
"No such nick/channel",
|
|
)
|
|
|
|
return 0, 0, false
|
|
}
|
|
|
|
alreadyIn, aiErr := hdlr.params.Database.
|
|
IsChannelMember(ctx, chID, targetSID)
|
|
if aiErr == nil && alreadyIn {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrUserOnChannel, nick,
|
|
[]string{targetNick, channel},
|
|
"is already on channel",
|
|
)
|
|
|
|
return 0, 0, false
|
|
}
|
|
|
|
return chID, targetSID, true
|
|
}
|
|
|
|
// executeInvite records the invite and sends
|
|
// notifications.
|
|
func (hdlr *Handlers) executeInvite(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, targetNick, channel string,
|
|
chID, targetSID int64,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
invErr := hdlr.params.Database.AddChannelInvite(
|
|
ctx, chID, targetSID, nick,
|
|
)
|
|
if invErr != nil {
|
|
hdlr.log.Error(
|
|
"add invite failed", "error", invErr,
|
|
)
|
|
hdlr.respondError(
|
|
writer, request,
|
|
"internal error",
|
|
http.StatusInternalServerError,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplInviting, nick,
|
|
[]string{targetNick, channel}, "",
|
|
)
|
|
hdlr.broker.Notify(sessionID)
|
|
|
|
invBody, mErr := json.Marshal([]string{channel})
|
|
if mErr == nil {
|
|
_ = hdlr.fanOutSilent(
|
|
request, irc.CmdInvite, nick, targetNick,
|
|
json.RawMessage(invBody),
|
|
[]int64{targetSID},
|
|
)
|
|
}
|
|
|
|
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 {
|
|
entries := make([]string, 0, len(members))
|
|
|
|
for idx := range members {
|
|
prefix := memberPrefix(&members[idx])
|
|
entries = append(
|
|
entries,
|
|
prefix+members[idx].Hostmask(),
|
|
)
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplNamReply, nick,
|
|
[]string{"=", channel},
|
|
strings.Join(entries, " "),
|
|
)
|
|
}
|
|
|
|
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()
|
|
|
|
// Use filtered list that hides +s channels
|
|
// from non-members.
|
|
channels, err := hdlr.params.Database.
|
|
ListAllChannelsWithCountsFiltered(ctx, sessionID)
|
|
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()
|
|
|
|
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
|
ctx, queryNick,
|
|
)
|
|
if err != nil {
|
|
hdlr.whoisNotFound(
|
|
ctx, writer, request,
|
|
sessionID, clientID, nick, queryNick,
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
hdlr.deliverWhoisUser(
|
|
ctx, clientID, nick, queryNick, targetSID,
|
|
)
|
|
|
|
// 313 RPL_WHOISOPERATOR — show if target is oper.
|
|
hdlr.deliverWhoisOperator(
|
|
ctx, clientID, nick, queryNick, targetSID,
|
|
)
|
|
|
|
hdlr.deliverWhoisIdle(
|
|
ctx, clientID, nick, queryNick, targetSID,
|
|
)
|
|
|
|
hdlr.deliverWhoisChannels(
|
|
ctx, clientID, nick, queryNick,
|
|
sessionID, targetSID,
|
|
)
|
|
|
|
// 338 RPL_WHOISACTUALLY — oper-only.
|
|
hdlr.deliverWhoisActually(
|
|
ctx, clientID, nick, queryNick,
|
|
sessionID, targetSID,
|
|
)
|
|
|
|
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)
|
|
}
|
|
|
|
// whoisNotFound sends the error+end numerics when the
|
|
// target nick is not found.
|
|
func (hdlr *Handlers) whoisNotFound(
|
|
ctx context.Context,
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, queryNick string,
|
|
) {
|
|
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)
|
|
}
|
|
|
|
// deliverWhoisUser sends RPL_WHOISUSER (311) and
|
|
// RPL_WHOISSERVER (312).
|
|
func (hdlr *Handlers) deliverWhoisUser(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
targetSID int64,
|
|
) {
|
|
srvName := hdlr.serverName()
|
|
|
|
username := queryNick
|
|
hostname := srvName
|
|
|
|
hostInfo, hostErr := hdlr.params.Database.
|
|
GetSessionHostInfo(ctx, targetSID)
|
|
if hostErr == nil && hostInfo != nil {
|
|
if hostInfo.Username != "" {
|
|
username = hostInfo.Username
|
|
}
|
|
|
|
if hostInfo.Hostname != "" {
|
|
hostname = hostInfo.Hostname
|
|
}
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisUser, nick,
|
|
[]string{queryNick, username, hostname, "*"},
|
|
queryNick,
|
|
)
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisServer, nick,
|
|
[]string{queryNick, srvName},
|
|
"neoirc server",
|
|
)
|
|
}
|
|
|
|
// deliverWhoisOperator sends RPL_WHOISOPERATOR (313) if
|
|
// the target has server oper status.
|
|
func (hdlr *Handlers) deliverWhoisOperator(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
targetSID int64,
|
|
) {
|
|
targetIsOper, err := hdlr.params.Database.
|
|
IsSessionOper(ctx, targetSID)
|
|
if err != nil || !targetIsOper {
|
|
return
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisOperator, nick,
|
|
[]string{queryNick},
|
|
"is an IRC operator",
|
|
)
|
|
}
|
|
|
|
func (hdlr *Handlers) deliverWhoisChannels(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
querierSID, targetSID int64,
|
|
) {
|
|
// Use filtered query that hides +s channels from
|
|
// non-members.
|
|
channels, chanErr := hdlr.params.Database.
|
|
GetSessionChannelsFiltered(
|
|
ctx, targetSID, querierSID,
|
|
)
|
|
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, " "),
|
|
)
|
|
}
|
|
|
|
// deliverWhoisActually sends RPL_WHOISACTUALLY (338)
|
|
// with the target's current client IP and hostname, but
|
|
// only when the querying session has server oper status
|
|
// (o-line). Non-opers see nothing extra.
|
|
func (hdlr *Handlers) deliverWhoisActually(
|
|
ctx context.Context,
|
|
clientID int64,
|
|
nick, queryNick string,
|
|
querierSID, targetSID int64,
|
|
) {
|
|
isOper, err := hdlr.params.Database.IsSessionOper(
|
|
ctx, querierSID,
|
|
)
|
|
if err != nil || !isOper {
|
|
return
|
|
}
|
|
|
|
clientInfo, clErr := hdlr.params.Database.
|
|
GetLatestClientForSession(ctx, targetSID)
|
|
if clErr != nil {
|
|
return
|
|
}
|
|
|
|
actualHost := clientInfo.Hostname
|
|
if actualHost == "" {
|
|
actualHost = clientInfo.IP
|
|
}
|
|
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoisActually, nick,
|
|
[]string{
|
|
queryNick,
|
|
clientInfo.IP,
|
|
},
|
|
"is actually using host "+actualHost,
|
|
)
|
|
}
|
|
|
|
// 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 {
|
|
username := mem.Username
|
|
if username == "" {
|
|
username = mem.Nick
|
|
}
|
|
|
|
hostname := mem.Hostname
|
|
if hostname == "" {
|
|
hostname = srvName
|
|
}
|
|
|
|
// 352 RPL_WHOREPLY
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplWhoReply, nick,
|
|
[]string{
|
|
channel, username, hostname,
|
|
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.clearAuthCookie(writer, request)
|
|
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
http.StatusOK)
|
|
}
|
|
}
|
|
|
|
// cleanupUser parts the user from all channels (notifying
|
|
// members) and deletes the session via the shared service
|
|
// layer.
|
|
func (hdlr *Handlers) cleanupUser(
|
|
ctx context.Context,
|
|
sessionID int64,
|
|
nick string,
|
|
) {
|
|
hdlr.svc.BroadcastQuit(
|
|
ctx, sessionID, nick, "Connection closed",
|
|
)
|
|
}
|
|
|
|
// 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,
|
|
)
|
|
}
|
|
}
|
|
|
|
// handleOper handles the OPER command for server operator
|
|
// authentication.
|
|
func (hdlr *Handlers) handleOper(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick string,
|
|
bodyLines func() []string,
|
|
) {
|
|
ctx := request.Context()
|
|
|
|
lines := bodyLines()
|
|
if len(lines) < 2 { //nolint:mnd // name + password
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdOper},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
err := hdlr.svc.Oper(
|
|
ctx, sessionID, lines[0], lines[1],
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
// 381 RPL_YOUREOPER
|
|
hdlr.enqueueNumeric(
|
|
ctx, clientID, irc.RplYoureOper, nick,
|
|
nil, "You are now an IRC operator",
|
|
)
|
|
|
|
hdlr.broker.Notify(sessionID)
|
|
hdlr.respondJSON(writer, request,
|
|
map[string]string{"status": "ok"},
|
|
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, " ")
|
|
}
|
|
|
|
cleared, err := hdlr.svc.SetAway(
|
|
ctx, sessionID, awayMsg,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
if cleared {
|
|
// 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)
|
|
}
|
|
|
|
// handleKick handles the KICK command.
|
|
func (hdlr *Handlers) handleKick(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, target string,
|
|
body json.RawMessage,
|
|
bodyLines func() []string,
|
|
) {
|
|
_ = body
|
|
|
|
if target == "" {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdKick},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
channel := target
|
|
if !strings.HasPrefix(channel, "#") {
|
|
channel = "#" + channel
|
|
}
|
|
|
|
lines := bodyLines()
|
|
if len(lines) == 0 {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrNeedMoreParams, nick,
|
|
[]string{irc.CmdKick},
|
|
"Not enough parameters",
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
targetNick := lines[0]
|
|
|
|
reason := nick
|
|
if len(lines) > 1 {
|
|
reason = lines[1]
|
|
}
|
|
|
|
err := hdlr.svc.KickUser(
|
|
request.Context(), sessionID, nick,
|
|
channel, targetNick, reason,
|
|
)
|
|
if hdlr.handleServiceError(
|
|
writer, request,
|
|
clientID, sessionID, nick, err,
|
|
) {
|
|
return
|
|
}
|
|
|
|
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",
|
|
)
|
|
}
|
|
|
|
// fanOut inserts a message and enqueues it to all given
|
|
// sessions.
|
|
func (hdlr *Handlers) fanOut(
|
|
request *http.Request,
|
|
command, from, target string,
|
|
body json.RawMessage,
|
|
meta json.RawMessage,
|
|
sessionIDs []int64,
|
|
) (string, error) {
|
|
dbID, msgUUID, err := hdlr.params.Database.InsertMessage(
|
|
request.Context(), command, from, target,
|
|
nil, body, meta,
|
|
)
|
|
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, nil, sessionIDs,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// requireChannelOp checks if the session is a channel
|
|
// operator and sends ERR_CHANOPRIVSNEEDED if not.
|
|
func (hdlr *Handlers) requireChannelOp(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
sessionID, clientID int64,
|
|
nick, channel string,
|
|
chID int64,
|
|
) bool {
|
|
isOp, err := hdlr.params.Database.IsChannelOperator(
|
|
request.Context(), chID, sessionID,
|
|
)
|
|
if err != nil || !isOp {
|
|
hdlr.respondIRCError(
|
|
writer, request, clientID, sessionID,
|
|
irc.ErrChanOpPrivsNeeded, nick,
|
|
[]string{channel},
|
|
"You're not channel operator",
|
|
)
|
|
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|