Compare commits
2 Commits
fix/irc-nu
...
8157b6d280
| Author | SHA1 | Date | |
|---|---|---|---|
| 8157b6d280 | |||
|
|
f9c145ad09 |
55
README.md
55
README.md
@@ -845,10 +845,11 @@ the server to the client (never C2S) and use 3-digit string codes in the
|
||||
| `442` | ERR_NOTONCHANNEL | Action on unjoined channel | `{"command":"442","to":"alice","params":["#general"],"body":["You're not on that channel"]}` |
|
||||
| `482` | ERR_CHANOPRIVSNEEDED | Non-op tries op action | `{"command":"482","to":"alice","params":["#general"],"body":["You're not channel operator"]}` |
|
||||
|
||||
**Note:** Numeric replies are planned for full implementation. The current MVP
|
||||
returns standard HTTP error responses (4xx/5xx with JSON error bodies) instead
|
||||
of numeric replies for error conditions. Numeric replies in the message queue
|
||||
will be added post-MVP.
|
||||
**Note:** Numeric replies are now implemented. All IRC command responses
|
||||
(success and error) are delivered as numeric replies through the message queue.
|
||||
HTTP error codes are reserved for transport-level issues (auth failures,
|
||||
malformed requests, server errors). The `params` field in the message envelope
|
||||
carries IRC-style parameters (e.g., channel name, target nick).
|
||||
|
||||
### Channel Modes
|
||||
|
||||
@@ -1054,8 +1055,8 @@ reference with all required and optional fields.
|
||||
|
||||
| Command | Required Fields | Optional | Response Status |
|
||||
|-----------|---------------------|---------------|-----------------|
|
||||
| `PRIVMSG` | `to`, `body` | `meta` | 201 Created |
|
||||
| `NOTICE` | `to`, `body` | `meta` | 201 Created |
|
||||
| `PRIVMSG` | `to`, `body` | `meta` | 200 OK |
|
||||
| `NOTICE` | `to`, `body` | `meta` | 200 OK |
|
||||
| `JOIN` | `to` | | 200 OK |
|
||||
| `PART` | `to` | `body` | 200 OK |
|
||||
| `NICK` | `body` | | 200 OK |
|
||||
@@ -1063,18 +1064,44 @@ reference with all required and optional fields.
|
||||
| `QUIT` | | `body` | 200 OK |
|
||||
| `PING` | | | 200 OK |
|
||||
|
||||
**Errors (all commands):**
|
||||
All IRC commands return HTTP 200 OK. IRC-level success and error responses
|
||||
are delivered as **numeric replies** through the message queue (see
|
||||
[Numeric Replies](#numeric-replies) below). HTTP error codes (4xx/5xx) are
|
||||
reserved for transport-level problems: malformed JSON (400), missing/invalid
|
||||
auth tokens (401), and server errors (500).
|
||||
|
||||
**HTTP errors (transport-level only):**
|
||||
|
||||
| Status | Error | When |
|
||||
|--------|-------|------|
|
||||
| 400 | `invalid request` | Malformed JSON |
|
||||
| 400 | `to field required` | Missing `to` for commands that need it |
|
||||
| 400 | `body required` | Missing `body` for commands that need it |
|
||||
| 400 | `unknown command: X` | Unrecognized command |
|
||||
| 400 | `invalid request` | Malformed JSON or empty command |
|
||||
| 401 | `unauthorized` | Missing or invalid auth token |
|
||||
| 404 | `channel not found` | Target channel doesn't exist |
|
||||
| 404 | `user not found` | DM target nick doesn't exist |
|
||||
| 409 | `nick already in use` | NICK target is taken |
|
||||
| 500 | `internal error` | Server-side failure |
|
||||
|
||||
**IRC numeric error replies (delivered via message queue):**
|
||||
|
||||
| Numeric | Name | When |
|
||||
|---------|------|------|
|
||||
| 401 | ERR_NOSUCHNICK | DM target nick doesn't exist |
|
||||
| 403 | ERR_NOSUCHCHANNEL | Target channel doesn't exist or invalid name |
|
||||
| 421 | ERR_UNKNOWNCOMMAND | Unrecognized command |
|
||||
| 432 | ERR_ERRONEUSNICKNAME | Invalid nickname format |
|
||||
| 433 | ERR_NICKNAMEINUSE | NICK target is taken |
|
||||
| 442 | ERR_NOTONCHANNEL | Not a member of the target channel |
|
||||
| 461 | ERR_NEEDMOREPARAMS | Missing required fields (to, body) |
|
||||
|
||||
**IRC numeric success replies (delivered via message queue):**
|
||||
|
||||
| Numeric | Name | When |
|
||||
|---------|------|------|
|
||||
| 001 | RPL_WELCOME | Sent on session creation/login |
|
||||
| 331 | RPL_NOTOPIC | Channel has no topic (on JOIN) |
|
||||
| 332 | RPL_TOPIC | Channel topic (on JOIN, TOPIC set) |
|
||||
| 353 | RPL_NAMREPLY | Channel member list (on JOIN) |
|
||||
| 366 | RPL_ENDOFNAMES | End of NAMES list (on JOIN) |
|
||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
||||
| 372 | RPL_MOTD | MOTD line |
|
||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
||||
|
||||
### GET /api/v1/history — Message History
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ type IRCMessage struct {
|
||||
Command string `json:"command"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
Body json.RawMessage `json:"body,omitempty"`
|
||||
TS string `json:"ts"`
|
||||
Meta json.RawMessage `json:"meta,omitempty"`
|
||||
@@ -491,12 +492,17 @@ func (database *Database) GetSessionChannelIDs(
|
||||
func (database *Database) InsertMessage(
|
||||
ctx context.Context,
|
||||
command, from, target string,
|
||||
params json.RawMessage,
|
||||
body json.RawMessage,
|
||||
meta json.RawMessage,
|
||||
) (int64, string, error) {
|
||||
msgUUID := uuid.New().String()
|
||||
now := time.Now().UTC()
|
||||
|
||||
if params == nil {
|
||||
params = json.RawMessage("[]")
|
||||
}
|
||||
|
||||
if body == nil {
|
||||
body = json.RawMessage("[]")
|
||||
}
|
||||
@@ -508,10 +514,10 @@ func (database *Database) InsertMessage(
|
||||
res, err := database.conn.ExecContext(ctx,
|
||||
`INSERT INTO messages
|
||||
(uuid, command, msg_from, msg_to,
|
||||
body, meta, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
params, body, meta, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
msgUUID, command, from, target,
|
||||
string(body), string(meta), now)
|
||||
string(params), string(body), string(meta), now)
|
||||
if err != nil {
|
||||
return 0, "", fmt.Errorf(
|
||||
"insert message: %w", err,
|
||||
@@ -578,7 +584,7 @@ func (database *Database) PollMessages(
|
||||
rows, err := database.conn.QueryContext(ctx,
|
||||
`SELECT cq.id, m.uuid, m.command,
|
||||
m.msg_from, m.msg_to,
|
||||
m.body, m.meta, m.created_at
|
||||
m.params, m.body, m.meta, m.created_at
|
||||
FROM client_queues cq
|
||||
INNER JOIN messages m
|
||||
ON m.id = cq.message_id
|
||||
@@ -642,7 +648,7 @@ func (database *Database) queryHistory(
|
||||
if beforeID > 0 {
|
||||
rows, err := database.conn.QueryContext(ctx,
|
||||
`SELECT id, uuid, command, msg_from,
|
||||
msg_to, body, meta, created_at
|
||||
msg_to, params, body, meta, created_at
|
||||
FROM messages
|
||||
WHERE msg_to = ? AND id < ?
|
||||
AND command = 'PRIVMSG'
|
||||
@@ -659,7 +665,7 @@ func (database *Database) queryHistory(
|
||||
|
||||
rows, err := database.conn.QueryContext(ctx,
|
||||
`SELECT id, uuid, command, msg_from,
|
||||
msg_to, body, meta, created_at
|
||||
msg_to, params, body, meta, created_at
|
||||
FROM messages
|
||||
WHERE msg_to = ?
|
||||
AND command = 'PRIVMSG'
|
||||
@@ -686,14 +692,14 @@ func scanMessages(
|
||||
var (
|
||||
msg IRCMessage
|
||||
qID int64
|
||||
body, meta string
|
||||
params, body, meta string
|
||||
createdAt time.Time
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&qID, &msg.ID, &msg.Command,
|
||||
&msg.From, &msg.To,
|
||||
&body, &meta, &createdAt,
|
||||
¶ms, &body, &meta, &createdAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fallbackQID, fmt.Errorf(
|
||||
@@ -701,6 +707,10 @@ func scanMessages(
|
||||
)
|
||||
}
|
||||
|
||||
if params != "" && params != "[]" {
|
||||
msg.Params = json.RawMessage(params)
|
||||
}
|
||||
|
||||
msg.Body = json.RawMessage(body)
|
||||
msg.Meta = json.RawMessage(meta)
|
||||
msg.TS = createdAt.Format(time.RFC3339Nano)
|
||||
|
||||
@@ -383,7 +383,7 @@ func TestInsertMessage(t *testing.T) {
|
||||
body := json.RawMessage(`["hello"]`)
|
||||
|
||||
dbID, msgUUID, err := database.InsertMessage(
|
||||
ctx, "PRIVMSG", "poller", "#test", body, nil,
|
||||
ctx, "PRIVMSG", "poller", "#test", nil, body, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -417,7 +417,7 @@ func TestPollMessages(t *testing.T) {
|
||||
body := json.RawMessage(`["hello"]`)
|
||||
|
||||
dbID, _, err := database.InsertMessage(
|
||||
ctx, "PRIVMSG", "poller", "#test", body, nil,
|
||||
ctx, "PRIVMSG", "poller", "#test", nil, body, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -475,7 +475,7 @@ func TestGetHistory(t *testing.T) {
|
||||
for range msgCount {
|
||||
_, _, err := database.InsertMessage(
|
||||
ctx, "PRIVMSG", "user", "#hist",
|
||||
json.RawMessage(`["msg"]`), nil,
|
||||
nil, json.RawMessage(`["msg"]`), nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -627,7 +627,7 @@ func TestEnqueueToClient(t *testing.T) {
|
||||
body := json.RawMessage(`["test"]`)
|
||||
|
||||
dbID, _, err := database.InsertMessage(
|
||||
ctx, "PRIVMSG", "sender", "#ch", body, nil,
|
||||
ctx, "PRIVMSG", "sender", "#ch", nil, body, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -50,6 +50,7 @@ CREATE TABLE IF NOT EXISTS messages (
|
||||
command TEXT NOT NULL DEFAULT 'PRIVMSG',
|
||||
msg_from TEXT NOT NULL DEFAULT '',
|
||||
msg_to TEXT NOT NULL DEFAULT '',
|
||||
params TEXT NOT NULL DEFAULT '[]',
|
||||
body TEXT NOT NULL DEFAULT '[]',
|
||||
meta TEXT NOT NULL DEFAULT '{}',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
@@ -28,18 +28,6 @@ const (
|
||||
defaultHistLimit = 50
|
||||
maxHistLimit = 500
|
||||
cmdPrivmsg = "PRIVMSG"
|
||||
|
||||
// IRC numeric reply codes per RFC 1459/2812.
|
||||
ircErrNoSuchNick = "401"
|
||||
ircErrNoSuchChannel = "403"
|
||||
ircErrNoRecipient = "411"
|
||||
ircErrNoTextToSend = "412"
|
||||
ircErrUnknownCommand = "421"
|
||||
ircErrNoNicknameGiven = "431"
|
||||
ircErrErroneusNickname = "432"
|
||||
ircErrNicknameInUse = "433"
|
||||
ircErrNotOnChannel = "442"
|
||||
ircErrNeedMoreParams = "461"
|
||||
)
|
||||
|
||||
func (hdlr *Handlers) maxBodySize() int64 {
|
||||
@@ -75,43 +63,6 @@ func (hdlr *Handlers) authSession(
|
||||
return sessionID, clientID, nick, nil
|
||||
}
|
||||
|
||||
// serverName returns the configured server name or the
|
||||
// default "neoirc".
|
||||
func (hdlr *Handlers) serverName() string {
|
||||
if hdlr.params.Config.ServerName != "" {
|
||||
return hdlr.params.Config.ServerName
|
||||
}
|
||||
|
||||
return "neoirc"
|
||||
}
|
||||
|
||||
// respondIRCError sends an IRC numeric error reply as a
|
||||
// JSON response with HTTP 200 OK. This is used for errors
|
||||
// in IRC command processing (as opposed to HTTP transport
|
||||
// errors). The params slice contains IRC-style positional
|
||||
// parameters (e.g. the target nick or channel).
|
||||
func (hdlr *Handlers) respondIRCError(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
nick, numeric, msg string,
|
||||
params ...string,
|
||||
) {
|
||||
resp := map[string]any{
|
||||
"command": numeric,
|
||||
"from": hdlr.serverName(),
|
||||
"to": nick,
|
||||
"body": []string{msg},
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
resp["params"] = params
|
||||
}
|
||||
|
||||
hdlr.respondJSON(
|
||||
writer, request, resp, http.StatusOK,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) requireAuth(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
@@ -140,7 +91,7 @@ func (hdlr *Handlers) fanOut(
|
||||
sessionIDs []int64,
|
||||
) (string, error) {
|
||||
dbID, msgUUID, err := hdlr.params.Database.InsertMessage(
|
||||
request.Context(), command, from, target, body, nil,
|
||||
request.Context(), command, from, target, nil, body, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("insert message: %w", err)
|
||||
@@ -234,7 +185,7 @@ func (hdlr *Handlers) handleCreateSession(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.deliverMOTD(request, clientID, sessionID)
|
||||
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
@@ -268,45 +219,76 @@ func (hdlr *Handlers) handleCreateSessionError(
|
||||
)
|
||||
}
|
||||
|
||||
// deliverWelcome sends the RPL_WELCOME (001) numeric to a
|
||||
// new client.
|
||||
func (hdlr *Handlers) deliverWelcome(
|
||||
request *http.Request,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "001", nick, nil,
|
||||
"Welcome to the network, "+nick,
|
||||
)
|
||||
}
|
||||
|
||||
// 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
|
||||
serverName := hdlr.serverName()
|
||||
|
||||
if motd == "" {
|
||||
return
|
||||
}
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
ctx := request.Context()
|
||||
|
||||
hdlr.deliverWelcome(request, clientID, nick)
|
||||
|
||||
if motd == "" {
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "375", serverName,
|
||||
"- "+serverName+" Message of the Day -",
|
||||
ctx, clientID, "375", nick, nil,
|
||||
"- "+srvName+" Message of the Day -",
|
||||
)
|
||||
|
||||
for line := range strings.SplitSeq(motd, "\n") {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "372", serverName,
|
||||
ctx, clientID, "372", nick, nil,
|
||||
"- "+line,
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "376", serverName,
|
||||
ctx, clientID, "376", 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,
|
||||
command, serverName, text string,
|
||||
command, nick string,
|
||||
params []string,
|
||||
text string,
|
||||
) {
|
||||
body, err := json.Marshal([]string{text})
|
||||
if err != nil {
|
||||
@@ -317,9 +299,22 @@ func (hdlr *Handlers) enqueueNumeric(
|
||||
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, serverName, "",
|
||||
json.RawMessage(body), nil,
|
||||
ctx, command, hdlr.serverName(), nick,
|
||||
paramsJSON, json.RawMessage(body), nil,
|
||||
)
|
||||
if insertErr != nil {
|
||||
hdlr.log.Error(
|
||||
@@ -577,7 +572,7 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
|
||||
writer, request.Body, hdlr.maxBodySize(),
|
||||
)
|
||||
|
||||
sessionID, _, nick, ok :=
|
||||
sessionID, clientID, nick, ok :=
|
||||
hdlr.requireAuth(writer, request)
|
||||
if !ok {
|
||||
return
|
||||
@@ -602,10 +597,10 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
|
||||
payload.To = strings.TrimSpace(payload.To)
|
||||
|
||||
if payload.Command == "" {
|
||||
hdlr.respondIRCError(
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
nick, ircErrUnknownCommand,
|
||||
"No command given",
|
||||
"command required",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
|
||||
return
|
||||
@@ -627,7 +622,8 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
|
||||
}
|
||||
|
||||
hdlr.dispatchCommand(
|
||||
writer, request, sessionID, nick,
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
payload.Command, payload.To,
|
||||
payload.Body, bodyLines,
|
||||
)
|
||||
@@ -637,7 +633,7 @@ func (hdlr *Handlers) HandleSendCommand() http.HandlerFunc {
|
||||
func (hdlr *Handlers) dispatchCommand(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
body json.RawMessage,
|
||||
bodyLines func() []string,
|
||||
@@ -645,24 +641,30 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
switch command {
|
||||
case cmdPrivmsg, "NOTICE":
|
||||
hdlr.handlePrivmsg(
|
||||
writer, request, sessionID, nick,
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, body, bodyLines,
|
||||
)
|
||||
case "JOIN":
|
||||
hdlr.handleJoin(
|
||||
writer, request, sessionID, nick, target,
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case "PART":
|
||||
hdlr.handlePart(
|
||||
writer, request, sessionID, nick, target, body,
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target, body,
|
||||
)
|
||||
case "NICK":
|
||||
hdlr.handleNick(
|
||||
writer, request, sessionID, nick, bodyLines,
|
||||
writer, request,
|
||||
sessionID, clientID, nick, bodyLines,
|
||||
)
|
||||
case "TOPIC":
|
||||
hdlr.handleTopic(
|
||||
writer, request, nick, target, body, bodyLines,
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, body, bodyLines,
|
||||
)
|
||||
case "QUIT":
|
||||
hdlr.handleQuit(
|
||||
@@ -672,51 +674,63 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
"command": "PONG",
|
||||
"from": hdlr.params.Config.ServerName,
|
||||
"from": hdlr.serverName(),
|
||||
},
|
||||
http.StatusOK)
|
||||
default:
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrUnknownCommand,
|
||||
command+" :Unknown command",
|
||||
command,
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"421", 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 int64,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
body json.RawMessage,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoRecipient,
|
||||
"No recipient given ("+command+")",
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"461", nick, []string{command},
|
||||
"Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoTextToSend,
|
||||
"No text to send",
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"461", nick, []string{command},
|
||||
"Not enough parameters",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(target, "#") {
|
||||
hdlr.handleChannelMsg(
|
||||
writer, request, sessionID, nick,
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, body,
|
||||
)
|
||||
|
||||
@@ -724,15 +738,36 @@ func (hdlr *Handlers) handlePrivmsg(
|
||||
}
|
||||
|
||||
hdlr.handleDirectMsg(
|
||||
writer, request, sessionID, nick,
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, body,
|
||||
)
|
||||
}
|
||||
|
||||
// respondIRCError enqueues a numeric error reply, notifies
|
||||
// the broker, and sends HTTP 200 with {"status":"error"}.
|
||||
func (hdlr *Handlers) respondIRCError(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
clientID, sessionID int64,
|
||||
numeric, nick string,
|
||||
params []string,
|
||||
text string,
|
||||
) {
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
numeric, nick, params, text,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleChannelMsg(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
body json.RawMessage,
|
||||
) {
|
||||
@@ -741,10 +776,9 @@ func (hdlr *Handlers) handleChannelMsg(
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoSuchChannel,
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{target},
|
||||
"No such channel",
|
||||
target,
|
||||
)
|
||||
|
||||
return
|
||||
@@ -768,15 +802,26 @@ func (hdlr *Handlers) handleChannelMsg(
|
||||
|
||||
if !isMember {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNotOnChannel,
|
||||
writer, request, clientID, sessionID,
|
||||
"442", nick, []string{target},
|
||||
"You're not on that channel",
|
||||
target,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.sendChannelMsg(
|
||||
writer, request, command, nick, target, body, chID,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) sendChannelMsg(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
command, nick, target string,
|
||||
body json.RawMessage,
|
||||
chID int64,
|
||||
) {
|
||||
memberIDs, err := hdlr.params.Database.GetChannelMemberIDs(
|
||||
request.Context(), chID,
|
||||
)
|
||||
@@ -815,7 +860,7 @@ func (hdlr *Handlers) handleChannelMsg(
|
||||
func (hdlr *Handlers) handleDirectMsg(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
body json.RawMessage,
|
||||
) {
|
||||
@@ -823,12 +868,15 @@ func (hdlr *Handlers) handleDirectMsg(
|
||||
request.Context(), target,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoSuchNick,
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"401", nick, []string{target},
|
||||
"No such nick/channel",
|
||||
target,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -860,15 +908,14 @@ func (hdlr *Handlers) handleDirectMsg(
|
||||
func (hdlr *Handlers) handleJoin(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNeedMoreParams,
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"JOIN"},
|
||||
"Not enough parameters",
|
||||
"JOIN",
|
||||
)
|
||||
|
||||
return
|
||||
@@ -881,15 +928,26 @@ func (hdlr *Handlers) handleJoin(
|
||||
|
||||
if !validChannelRe.MatchString(channel) {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoSuchChannel,
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
channel,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeJoin(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, channel,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) executeJoin(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, channel string,
|
||||
) {
|
||||
chID, err := hdlr.params.Database.GetOrCreateChannel(
|
||||
request.Context(), channel,
|
||||
)
|
||||
@@ -930,6 +988,10 @@ func (hdlr *Handlers) handleJoin(
|
||||
request, "JOIN", nick, channel, nil, memberIDs,
|
||||
)
|
||||
|
||||
hdlr.deliverJoinNumerics(
|
||||
request, clientID, sessionID, nick, channel, chID,
|
||||
)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
"status": "joined",
|
||||
@@ -938,20 +1000,96 @@ func (hdlr *Handlers) handleJoin(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// deliverJoinNumerics sends RPL_TOPIC/RPL_NOTOPIC,
|
||||
// RPL_NAMREPLY, and RPL_ENDOFNAMES to the joining client.
|
||||
func (hdlr *Handlers) deliverJoinNumerics(
|
||||
request *http.Request,
|
||||
clientID, sessionID int64,
|
||||
nick, channel string,
|
||||
chID int64,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
chInfo, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err == nil {
|
||||
_ = chInfo // chInfo is the ID; topic comes from DB.
|
||||
}
|
||||
|
||||
// Get topic from channel info.
|
||||
channels, listErr := hdlr.params.Database.ListChannels(
|
||||
ctx, sessionID,
|
||||
)
|
||||
|
||||
topic := ""
|
||||
|
||||
if listErr == nil {
|
||||
for _, ch := range channels {
|
||||
if ch.Name == channel {
|
||||
topic = ch.Topic
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if topic != "" {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "332", nick,
|
||||
[]string{channel}, topic,
|
||||
)
|
||||
} else {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "331", nick,
|
||||
[]string{channel}, "No topic is set",
|
||||
)
|
||||
}
|
||||
|
||||
// Get member list for NAMES reply.
|
||||
members, memErr := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
|
||||
if memErr == nil && len(members) > 0 {
|
||||
nicks := make([]string, 0, len(members))
|
||||
|
||||
for _, mem := range members {
|
||||
nicks = append(nicks, mem.Nick)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "353", nick,
|
||||
[]string{"=", channel},
|
||||
strings.Join(nicks, " "),
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "366", nick,
|
||||
[]string{channel}, "End of /NAMES list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handlePart(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
body json.RawMessage,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNeedMoreParams,
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"461", nick, []string{"PART"},
|
||||
"Not enough parameters",
|
||||
"PART",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -965,12 +1103,15 @@ func (hdlr *Handlers) handlePart(
|
||||
request.Context(), channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoSuchChannel,
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
channel,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "error"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1014,16 +1155,16 @@ func (hdlr *Handlers) handlePart(
|
||||
func (hdlr *Handlers) handleNick(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID int64,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoNicknameGiven,
|
||||
"No nickname given",
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"NICK"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1033,10 +1174,9 @@ func (hdlr *Handlers) handleNick(
|
||||
|
||||
if !validNickRe.MatchString(newNick) {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrErroneusNickname,
|
||||
writer, request, clientID, sessionID,
|
||||
"432", nick, []string{newNick},
|
||||
"Erroneous nickname",
|
||||
newNick,
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1052,16 +1192,27 @@ func (hdlr *Handlers) handleNick(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeNickChange(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, newNick,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) executeNickChange(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, newNick string,
|
||||
) {
|
||||
err := hdlr.params.Database.ChangeNick(
|
||||
request.Context(), sessionID, newNick,
|
||||
)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "UNIQUE") {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNicknameInUse,
|
||||
writer, request, clientID, sessionID,
|
||||
"433", nick, []string{newNick},
|
||||
"Nickname is already in use",
|
||||
newNick,
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1111,7 +1262,7 @@ func (hdlr *Handlers) broadcastNick(
|
||||
|
||||
dbID, _, _ := hdlr.params.Database.InsertMessage(
|
||||
request.Context(), "NICK", oldNick, "",
|
||||
json.RawMessage(nickBody), nil,
|
||||
nil, json.RawMessage(nickBody), nil,
|
||||
)
|
||||
|
||||
_ = hdlr.params.Database.EnqueueToSession(
|
||||
@@ -1143,16 +1294,16 @@ func (hdlr *Handlers) broadcastNick(
|
||||
func (hdlr *Handlers) handleTopic(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
body json.RawMessage,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNeedMoreParams,
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"TOPIC"},
|
||||
"Not enough parameters",
|
||||
"TOPIC",
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1161,47 +1312,59 @@ func (hdlr *Handlers) handleTopic(
|
||||
lines := bodyLines()
|
||||
if len(lines) == 0 {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNeedMoreParams,
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"TOPIC"},
|
||||
"Not enough parameters",
|
||||
"TOPIC",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
topic := strings.Join(lines, " ")
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
err := hdlr.params.Database.SetTopic(
|
||||
request.Context(), channel, topic,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"set topic failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
request.Context(), channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request,
|
||||
nick, ircErrNoSuchChannel,
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
channel,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeTopic(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
channel, strings.Join(lines, " "),
|
||||
body, chID,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) executeTopic(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, channel, topic string,
|
||||
body json.RawMessage,
|
||||
chID int64,
|
||||
) {
|
||||
setErr := hdlr.params.Database.SetTopic(
|
||||
request.Context(), channel, topic,
|
||||
)
|
||||
if setErr != nil {
|
||||
hdlr.log.Error(
|
||||
"set topic failed", "error", setErr,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
@@ -1215,6 +1378,12 @@ func (hdlr *Handlers) handleTopic(
|
||||
request, "TOPIC", nick, channel, body, memberIDs,
|
||||
)
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"332", nick, []string{channel}, topic,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{
|
||||
"status": "ok", "topic": topic,
|
||||
@@ -1240,7 +1409,8 @@ func (hdlr *Handlers) handleQuit(
|
||||
|
||||
if len(channels) > 0 {
|
||||
dbID, _, _ = hdlr.params.Database.InsertMessage(
|
||||
request.Context(), "QUIT", nick, "", body, nil,
|
||||
request.Context(), "QUIT", nick, "",
|
||||
nil, body, nil,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1489,7 +1659,8 @@ func (hdlr *Handlers) cleanupUser(
|
||||
|
||||
if len(channels) > 0 {
|
||||
quitDBID, _, _ = hdlr.params.Database.InsertMessage(
|
||||
ctx, "QUIT", nick, "", nil, nil,
|
||||
ctx, "QUIT", nick, "",
|
||||
nil, nil, nil,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -462,6 +462,19 @@ func findMessage(
|
||||
return false
|
||||
}
|
||||
|
||||
func findNumeric(
|
||||
msgs []map[string]any,
|
||||
numeric string,
|
||||
) bool {
|
||||
for _, msg := range msgs {
|
||||
if msg[commandKey] == numeric {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// --- Tests ---
|
||||
|
||||
func TestCreateSessionValid(t *testing.T) {
|
||||
@@ -473,6 +486,47 @@ func TestCreateSessionValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWelcomeNumeric(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("welcomer")
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, 0)
|
||||
|
||||
if !findNumeric(msgs, "001") {
|
||||
t.Fatalf(
|
||||
"expected RPL_WELCOME (001), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinNumerics(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("jnumtest")
|
||||
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
tserver.sendCommand(token, map[string]any{
|
||||
commandKey: joinCmd, toKey: "#numtest",
|
||||
})
|
||||
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "353") {
|
||||
t.Fatalf(
|
||||
"expected RPL_NAMREPLY (353), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
|
||||
if !findNumeric(msgs, "366") {
|
||||
t.Fatalf(
|
||||
"expected RPL_ENDOFNAMES (366), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSessionDuplicate(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
tserver.createSession("alice")
|
||||
@@ -668,17 +722,22 @@ func TestJoinMissingTo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("joiner3")
|
||||
|
||||
status, result := tserver.sendCommand(
|
||||
// Drain initial MOTD/welcome numerics.
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(
|
||||
token, map[string]any{commandKey: joinCmd},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "461" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 461, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -735,17 +794,21 @@ func TestMessageMissingBody(t *testing.T) {
|
||||
commandKey: joinCmd, toKey: "#test",
|
||||
})
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: privmsgCmd, toKey: "#test",
|
||||
})
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "412" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 412, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -754,7 +817,9 @@ func TestMessageMissingTo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("noto")
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: privmsgCmd,
|
||||
bodyKey: []string{"hello"},
|
||||
})
|
||||
@@ -762,10 +827,12 @@ func TestMessageMissingTo(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "411" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 411, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -780,8 +847,10 @@ func TestNonMemberCannotSend(t *testing.T) {
|
||||
commandKey: joinCmd, toKey: "#private",
|
||||
})
|
||||
|
||||
_, lastID := tserver.pollMessages(aliceToken, 0)
|
||||
|
||||
// Alice tries to send without joining.
|
||||
status, result := tserver.sendCommand(
|
||||
status, _ := tserver.sendCommand(
|
||||
aliceToken,
|
||||
map[string]any{
|
||||
commandKey: privmsgCmd,
|
||||
@@ -793,10 +862,12 @@ func TestNonMemberCannotSend(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "442" {
|
||||
msgs, _ := tserver.pollMessages(aliceToken, lastID)
|
||||
|
||||
if !findNumeric(msgs, "442") {
|
||||
t.Fatalf(
|
||||
"expected IRC 442, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NOTONCHANNEL (442), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -846,7 +917,9 @@ func TestDMToNonexistentUser(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("dmsender")
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: privmsgCmd,
|
||||
toKey: "nobody",
|
||||
bodyKey: []string{"hello?"},
|
||||
@@ -855,10 +928,12 @@ func TestDMToNonexistentUser(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "401" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "401") {
|
||||
t.Fatalf(
|
||||
"expected IRC 401, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NOSUCHNICK (401), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -906,7 +981,9 @@ func TestNickCollision(t *testing.T) {
|
||||
|
||||
tserver.createSession("taken_nick")
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "NICK",
|
||||
bodyKey: []string{"taken_nick"},
|
||||
})
|
||||
@@ -914,10 +991,12 @@ func TestNickCollision(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "433" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "433") {
|
||||
t.Fatalf(
|
||||
"expected IRC 433, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NICKNAMEINUSE (433), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -926,7 +1005,9 @@ func TestNickInvalid(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("nickval")
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "NICK",
|
||||
bodyKey: []string{"bad nick!"},
|
||||
})
|
||||
@@ -934,10 +1015,12 @@ func TestNickInvalid(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "432" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "432") {
|
||||
t.Fatalf(
|
||||
"expected IRC 432, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_ERRONEUSNICKNAME (432), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -946,17 +1029,21 @@ func TestNickEmptyBody(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("nicknobody")
|
||||
|
||||
status, result := tserver.sendCommand(
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(
|
||||
token, map[string]any{commandKey: "NICK"},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "431" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 431, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -994,7 +1081,9 @@ func TestTopicMissingTo(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("topicnoto")
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "TOPIC",
|
||||
bodyKey: []string{"topic"},
|
||||
})
|
||||
@@ -1002,10 +1091,12 @@ func TestTopicMissingTo(t *testing.T) {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "461" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 461, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1018,17 +1109,21 @@ func TestTopicMissingBody(t *testing.T) {
|
||||
commandKey: joinCmd, toKey: "#topictest",
|
||||
})
|
||||
|
||||
status, result := tserver.sendCommand(token, map[string]any{
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(token, map[string]any{
|
||||
commandKey: "TOPIC", toKey: "#topictest",
|
||||
})
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "461" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "461") {
|
||||
t.Fatalf(
|
||||
"expected IRC 461, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_NEEDMOREPARAMS (461), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1097,17 +1192,21 @@ func TestUnknownCommand(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("cmdtest")
|
||||
|
||||
status, result := tserver.sendCommand(
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
status, _ := tserver.sendCommand(
|
||||
token, map[string]any{commandKey: "BOGUS"},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "421" {
|
||||
msgs, _ := tserver.pollMessages(token, lastID)
|
||||
|
||||
if !findNumeric(msgs, "421") {
|
||||
t.Fatalf(
|
||||
"expected IRC 421, got %v",
|
||||
result[commandKey],
|
||||
"expected ERR_UNKNOWNCOMMAND (421), got %v",
|
||||
msgs,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1116,18 +1215,11 @@ func TestEmptyCommand(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("emptycmd")
|
||||
|
||||
status, result := tserver.sendCommand(
|
||||
status, _ := tserver.sendCommand(
|
||||
token, map[string]any{commandKey: ""},
|
||||
)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", status)
|
||||
}
|
||||
|
||||
if result[commandKey] != "421" {
|
||||
t.Fatalf(
|
||||
"expected IRC 421, got %v",
|
||||
result[commandKey],
|
||||
)
|
||||
if status != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1362,12 +1454,18 @@ func TestLongPollTimeout(t *testing.T) {
|
||||
tserver := newTestServer(t)
|
||||
token := tserver.createSession("lp_timeout")
|
||||
|
||||
// Drain initial welcome/MOTD numerics.
|
||||
_, lastID := tserver.pollMessages(token, 0)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
resp, err := doRequestAuth(
|
||||
t,
|
||||
http.MethodGet,
|
||||
tserver.url(apiMessages+"?timeout=1"),
|
||||
tserver.url(fmt.Sprintf(
|
||||
"%s?timeout=1&after=%d",
|
||||
apiMessages, lastID,
|
||||
)),
|
||||
token,
|
||||
nil,
|
||||
)
|
||||
|
||||
@@ -80,7 +80,7 @@ func (hdlr *Handlers) handleRegister(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.deliverMOTD(request, clientID, sessionID)
|
||||
hdlr.deliverMOTD(request, clientID, sessionID, payload.Nick)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
@@ -162,7 +162,7 @@ func (hdlr *Handlers) handleLogin(
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, _, token, err :=
|
||||
sessionID, clientID, token, err :=
|
||||
hdlr.params.Database.LoginUser(
|
||||
request.Context(),
|
||||
payload.Nick,
|
||||
@@ -178,6 +178,10 @@ func (hdlr *Handlers) handleLogin(
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.deliverMOTD(
|
||||
request, clientID, sessionID, payload.Nick,
|
||||
)
|
||||
|
||||
hdlr.respondJSON(writer, request, map[string]any{
|
||||
"id": sessionID,
|
||||
"nick": payload.Nick,
|
||||
|
||||
Reference in New Issue
Block a user