Show an IRC listener user what their session sends from other clients (closes #179)
check / check (push) Waiting to run
check / check (push) Waiting to run
The relay dropped every PRIVMSG, NOTICE, JOIN, PART, NICK, QUIT, TOPIC, MODE and KICK sent under the connection's own nick, so an IRC listener connection never saw what its session did from another client. The relay now drops nothing by nick. Instead, the service methods that fan out a user's own command take the client whose queue to skip: the IRC listener passes its own client, which has already written the echo, and the HTTP API passes 0. Channel messages and PARTs now also go to the sender's own session, so the HTTP handler no longer queues the channel-message echo itself, SendChannelMessage no longer returns the message's row ID, and neoirc-cli ignores the PART event for its own part, which it has already shown. Model: opus-5-5
This commit is contained in:
@@ -268,10 +268,15 @@ rules). Case-sensitive comparison is unambiguous.
|
|||||||
A single user session can have multiple clients (phone, laptop, terminal).
|
A single user session can have multiple clients (phone, laptop, terminal).
|
||||||
|
|
||||||
- Each client gets a **separate server-to-client (S2C) message queue**.
|
- Each client gets a **separate server-to-client (S2C) message queue**.
|
||||||
- The server fans out messages and events from other users, and the user's own
|
- The server fans out messages and events from other users to every client queue
|
||||||
DMs and the channel messages it sends over the HTTP API, to every client queue
|
for that user session.
|
||||||
for that user session. Replies to a command (numerics) go only to the client
|
- When the user sends a `PRIVMSG`, `NOTICE`, `JOIN`, `PART`, `NICK`, `TOPIC`,
|
||||||
that sent it.
|
`MODE` or `KICK` from one of its clients, the resulting event goes to every
|
||||||
|
client queue of the user's own session, as well as to the other users it is
|
||||||
|
meant for. The one exception is an IRC listener connection that sent the
|
||||||
|
command, which gets nothing queued for it: it has already sent its user the
|
||||||
|
echo, or, for `PRIVMSG` and `NOTICE`, IRC sends none.
|
||||||
|
- Replies to a command (numerics) go only to the client that sent it.
|
||||||
- `GET /api/v1/messages` delivers from the calling client's specific queue,
|
- `GET /api/v1/messages` delivers from the calling client's specific queue,
|
||||||
identified by the auth cookie.
|
identified by the auth cookie.
|
||||||
- Client queues have **independent expiry/pruning** — one client going offline
|
- Client queues have **independent expiry/pruning** — one client going offline
|
||||||
@@ -744,7 +749,7 @@ Leave a channel.
|
|||||||
{"command": "PART", "to": "#general", "body": ["goodbye"]}
|
{"command": "PART", "to": "#general", "body": ["goodbye"]}
|
||||||
```
|
```
|
||||||
|
|
||||||
**S2C (broadcast to the other channel members):**
|
**S2C (broadcast to all channel members, including the departing user):**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -760,9 +765,8 @@ Leave a channel.
|
|||||||
|
|
||||||
**Behavior:**
|
**Behavior:**
|
||||||
|
|
||||||
- The PART event goes to the other channel members, not to the departing user;
|
- The PART event goes to every channel member, the departing user's own clients
|
||||||
the HTTP response confirms the part. (The IRC listener echoes the PART to its
|
included. (An IRC listener connection that sends a PART echoes it itself.)
|
||||||
own connection.)
|
|
||||||
- If the channel is empty after the user leaves, the channel is **deleted**
|
- If the channel is empty after the user leaves, the channel is **deleted**
|
||||||
(ephemeral channels).
|
(ephemeral channels).
|
||||||
- If the user is not in the channel, the server returns an error.
|
- If the user is not in the channel, the server returns an error.
|
||||||
|
|||||||
+7
-3
@@ -757,7 +757,7 @@ func (a *App) handleServerMessage(msg *api.Message) {
|
|||||||
case irc.CmdJoin:
|
case irc.CmdJoin:
|
||||||
a.handleJoinEvent(msg, timestamp)
|
a.handleJoinEvent(msg, timestamp)
|
||||||
case irc.CmdPart:
|
case irc.CmdPart:
|
||||||
a.handlePartEvent(msg, timestamp)
|
a.handlePartEvent(msg, timestamp, myNick)
|
||||||
case irc.CmdQuit:
|
case irc.CmdQuit:
|
||||||
a.handleQuitEvent(msg, timestamp)
|
a.handleQuitEvent(msg, timestamp)
|
||||||
case irc.CmdNick:
|
case irc.CmdNick:
|
||||||
@@ -813,10 +813,14 @@ func (a *App) handleJoinEvent(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handlePartEvent shows another user's part. The server
|
||||||
|
// also sends the user's own part back to it, which cmdPart
|
||||||
|
// has already shown; showing it again would add a second
|
||||||
|
// line and mark the channel just left as unread.
|
||||||
func (a *App) handlePartEvent(
|
func (a *App) handlePartEvent(
|
||||||
msg *api.Message, timestamp string,
|
msg *api.Message, timestamp, myNick string,
|
||||||
) {
|
) {
|
||||||
if msg.To == "" {
|
if msg.To == "" || msg.From == myNick {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
api "sneak.berlin/go/neoirc/internal/cli/api"
|
api "sneak.berlin/go/neoirc/internal/cli/api"
|
||||||
|
"sneak.berlin/go/neoirc/pkg/irc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMemberPrefix(t *testing.T) {
|
func TestMemberPrefix(t *testing.T) {
|
||||||
@@ -49,3 +51,66 @@ func TestMemberPrefix(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPartEvent checks that a PART event adds a line to the
|
||||||
|
// channel's buffer and marks it unread, except for the
|
||||||
|
// user's own part, which cmdPart has already shown.
|
||||||
|
func TestPartEvent(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
from string
|
||||||
|
wantLines int
|
||||||
|
wantUnread int
|
||||||
|
}{
|
||||||
|
{name: "own part", from: "alice", wantLines: 0, wantUnread: 0},
|
||||||
|
{name: "other member", from: "bob", wantLines: 1, wantUnread: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
app := &App{ui: NewUI(), nick: "alice"}
|
||||||
|
runUI(t, app.ui)
|
||||||
|
|
||||||
|
app.handleServerMessage(&api.Message{
|
||||||
|
Command: irc.CmdPart, From: test.from, To: "#chan",
|
||||||
|
})
|
||||||
|
|
||||||
|
var lines, unread int
|
||||||
|
|
||||||
|
app.ui.app.QueueUpdate(func() {
|
||||||
|
buf := app.ui.getOrCreateBuffer("#chan")
|
||||||
|
lines = len(buf.Lines)
|
||||||
|
unread = buf.Unread
|
||||||
|
})
|
||||||
|
|
||||||
|
if lines != test.wantLines || unread != test.wantUnread {
|
||||||
|
t.Fatalf("got %d lines and %d unread, want %d and %d",
|
||||||
|
lines, unread, test.wantLines, test.wantUnread)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runUI runs ui on a simulated screen until the test ends.
|
||||||
|
func runUI(t *testing.T, ui *UI) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
ui.app.SetScreen(tcell.NewSimulationScreen(""))
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
|
||||||
|
go func() { done <- ui.Run() }()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ui.Stop()
|
||||||
|
|
||||||
|
err := <-done
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("run ui: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+11
-8
@@ -1208,11 +1208,12 @@ func insertMessageInTx(
|
|||||||
return dbID, msgUUID, nil
|
return dbID, msgUUID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnqueueToSession adds a message to all clients of a
|
// EnqueueToSession adds a message to the queues of all
|
||||||
// session's queues.
|
// clients of a session except client skipClientID, or of all
|
||||||
|
// of them when skipClientID is 0.
|
||||||
func (database *Database) EnqueueToSession(
|
func (database *Database) EnqueueToSession(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID, messageID int64,
|
sessionID, messageID, skipClientID int64,
|
||||||
) error {
|
) error {
|
||||||
transaction, err := database.conn.BeginTx(ctx, nil)
|
transaction, err := database.conn.BeginTx(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1221,7 +1222,9 @@ func (database *Database) EnqueueToSession(
|
|||||||
|
|
||||||
defer func() { _ = transaction.Rollback() }()
|
defer func() { _ = transaction.Rollback() }()
|
||||||
|
|
||||||
err = enqueueToSessionInTx(ctx, transaction, sessionID, messageID)
|
err = enqueueToSessionInTx(
|
||||||
|
ctx, transaction, sessionID, messageID, skipClientID,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1239,15 +1242,15 @@ func (database *Database) EnqueueToSession(
|
|||||||
func enqueueToSessionInTx(
|
func enqueueToSessionInTx(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
transaction *sql.Tx,
|
transaction *sql.Tx,
|
||||||
sessionID, messageID int64,
|
sessionID, messageID, skipClientID int64,
|
||||||
) error {
|
) error {
|
||||||
_, err := transaction.ExecContext(ctx,
|
_, err := transaction.ExecContext(ctx,
|
||||||
`INSERT OR IGNORE INTO client_queues
|
`INSERT OR IGNORE INTO client_queues
|
||||||
(client_id, message_id, created_at)
|
(client_id, message_id, created_at)
|
||||||
SELECT c.id, ?, ?
|
SELECT c.id, ?, ?
|
||||||
FROM clients c
|
FROM clients c
|
||||||
WHERE c.session_id = ?`,
|
WHERE c.session_id = ? AND c.id != ?`,
|
||||||
messageID, time.Now(), sessionID)
|
messageID, time.Now(), sessionID, skipClientID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"enqueue to session: %w", err,
|
"enqueue to session: %w", err,
|
||||||
@@ -1673,7 +1676,7 @@ func quitSessionInTx(
|
|||||||
|
|
||||||
for _, peerID := range peerIDs {
|
for _, peerID := range peerIDs {
|
||||||
err = enqueueToSessionInTx(
|
err = enqueueToSessionInTx(
|
||||||
ctx, transaction, peerID, messageID,
|
ctx, transaction, peerID, messageID, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -675,7 +675,7 @@ func TestPollMessages(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = database.EnqueueToSession(ctx, sid, dbID)
|
err = database.EnqueueToSession(ctx, sid, dbID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-19
@@ -1257,9 +1257,11 @@ func (hdlr *Handlers) handleChannelMsg(
|
|||||||
) {
|
) {
|
||||||
ctx := request.Context()
|
ctx := request.Context()
|
||||||
|
|
||||||
// Delegate validation + fan-out to service layer.
|
// Delegate validation + fan-out to service layer. The
|
||||||
dbID, uuid, err := hdlr.svc.SendChannelMessage(
|
// fan-out includes the sender's own session, so all
|
||||||
ctx, sessionID, nick,
|
// their clients see the message in long-poll responses.
|
||||||
|
uuid, err := hdlr.svc.SendChannelMessage(
|
||||||
|
ctx, sessionID, 0, nick,
|
||||||
command, target, body, meta,
|
command, target, body, meta,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -1269,13 +1271,6 @@ func (hdlr *Handlers) handleChannelMsg(
|
|||||||
return
|
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,
|
hdlr.respondJSON(writer, request,
|
||||||
map[string]string{"id": uuid, statusKey: "sent"},
|
map[string]string{"id": uuid, statusKey: "sent"},
|
||||||
http.StatusOK)
|
http.StatusOK)
|
||||||
@@ -1290,7 +1285,7 @@ func (hdlr *Handlers) handleDirectMsg(
|
|||||||
meta json.RawMessage,
|
meta json.RawMessage,
|
||||||
) {
|
) {
|
||||||
result, err := hdlr.svc.SendDirectMessage(
|
result, err := hdlr.svc.SendDirectMessage(
|
||||||
request.Context(), sessionID, nick,
|
request.Context(), sessionID, 0, nick,
|
||||||
command, target, body, meta,
|
command, target, body, meta,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -1361,7 +1356,7 @@ func (hdlr *Handlers) executeJoin(
|
|||||||
nick, channel, suppliedKey string,
|
nick, channel, suppliedKey string,
|
||||||
) {
|
) {
|
||||||
result, err := hdlr.svc.JoinChannel(
|
result, err := hdlr.svc.JoinChannel(
|
||||||
request.Context(), sessionID, nick,
|
request.Context(), sessionID, 0, nick,
|
||||||
channel, suppliedKey,
|
channel, suppliedKey,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -1545,7 +1540,7 @@ func (hdlr *Handlers) handlePart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := hdlr.svc.PartChannel(
|
err := hdlr.svc.PartChannel(
|
||||||
request.Context(), sessionID,
|
request.Context(), sessionID, 0,
|
||||||
nick, channel, reason,
|
nick, channel, reason,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -1616,7 +1611,7 @@ func (hdlr *Handlers) executeNickChange(
|
|||||||
nick, newNick string,
|
nick, newNick string,
|
||||||
) {
|
) {
|
||||||
err := hdlr.svc.ChangeNick(
|
err := hdlr.svc.ChangeNick(
|
||||||
request.Context(), sessionID, nick, newNick,
|
request.Context(), sessionID, 0, nick, newNick,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
writer, request,
|
writer, request,
|
||||||
@@ -1671,7 +1666,7 @@ func (hdlr *Handlers) handleTopic(
|
|||||||
topic := strings.Join(lines, " ")
|
topic := strings.Join(lines, " ")
|
||||||
|
|
||||||
err := hdlr.svc.SetTopic(
|
err := hdlr.svc.SetTopic(
|
||||||
request.Context(), sessionID,
|
request.Context(), sessionID, 0,
|
||||||
nick, channel, topic,
|
nick, channel, topic,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -2157,7 +2152,7 @@ func (hdlr *Handlers) applyUserMode(
|
|||||||
|
|
||||||
modeText := modeArgs[0] + " " + targetNick
|
modeText := modeArgs[0] + " " + targetNick
|
||||||
hdlr.svc.BroadcastMode(
|
hdlr.svc.BroadcastMode(
|
||||||
request.Context(), nick, channel, chID,
|
request.Context(), 0, nick, channel, chID,
|
||||||
modeText,
|
modeText,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2203,7 +2198,7 @@ func (hdlr *Handlers) setChannelFlag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
hdlr.svc.BroadcastMode(
|
hdlr.svc.BroadcastMode(
|
||||||
request.Context(), nick, channel, chID,
|
request.Context(), 0, nick, channel, chID,
|
||||||
modeStr,
|
modeStr,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3650,7 +3645,7 @@ func (hdlr *Handlers) handleKick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := hdlr.svc.KickUser(
|
err := hdlr.svc.KickUser(
|
||||||
request.Context(), sessionID, nick,
|
request.Context(), sessionID, 0, nick,
|
||||||
channel, targetNick, reason,
|
channel, targetNick, reason,
|
||||||
)
|
)
|
||||||
if hdlr.handleServiceError(
|
if hdlr.handleServiceError(
|
||||||
@@ -3723,7 +3718,7 @@ func (hdlr *Handlers) fanOut(
|
|||||||
|
|
||||||
for _, sid := range sessionIDs {
|
for _, sid := range sessionIDs {
|
||||||
enqErr := hdlr.params.Database.EnqueueToSession(
|
enqErr := hdlr.params.Database.EnqueueToSession(
|
||||||
request.Context(), sid, dbID,
|
request.Context(), sid, dbID, 0,
|
||||||
)
|
)
|
||||||
if enqErr != nil {
|
if enqErr != nil {
|
||||||
hdlr.log.Error("enqueue failed",
|
hdlr.log.Error("enqueue failed",
|
||||||
|
|||||||
@@ -2905,7 +2905,7 @@ func (tserver *testServer) queueMessage(
|
|||||||
return fmt.Errorf("insert message: %w", err)
|
return fmt.Errorf("insert message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tserver.database.EnqueueToSession(ctx, sessionID, msgID)
|
err = tserver.database.EnqueueToSession(ctx, sessionID, msgID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("enqueue: %w", err)
|
return fmt.Errorf("enqueue: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ func (c *Conn) handleNick(
|
|||||||
oldNick := c.nick
|
oldNick := c.nick
|
||||||
|
|
||||||
err := c.svc.ChangeNick(
|
err := c.svc.ChangeNick(
|
||||||
ctx, c.sessionID, oldNick, newNick,
|
ctx, c.sessionID, c.clientID, oldNick, newNick,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendIRCError(err)
|
c.sendIRCError(err)
|
||||||
@@ -128,8 +128,8 @@ func (c *Conn) handlePrivmsg(
|
|||||||
body, _ := json.Marshal([]string{text}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{text}) //nolint:errchkjson
|
||||||
|
|
||||||
if strings.HasPrefix(target, "#") {
|
if strings.HasPrefix(target, "#") {
|
||||||
_, _, err := c.svc.SendChannelMessage(
|
_, err := c.svc.SendChannelMessage(
|
||||||
ctx, c.sessionID, c.nick,
|
ctx, c.sessionID, c.clientID, c.nick,
|
||||||
msg.Command, target, body, nil,
|
msg.Command, target, body, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -137,7 +137,7 @@ func (c *Conn) handlePrivmsg(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result, err := c.svc.SendDirectMessage(
|
result, err := c.svc.SendDirectMessage(
|
||||||
ctx, c.sessionID, c.nick,
|
ctx, c.sessionID, c.clientID, c.nick,
|
||||||
msg.Command, target, body, nil,
|
msg.Command, target, body, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,7 +199,7 @@ func (c *Conn) joinChannel(
|
|||||||
channel, key string,
|
channel, key string,
|
||||||
) {
|
) {
|
||||||
result, err := c.svc.JoinChannel(
|
result, err := c.svc.JoinChannel(
|
||||||
ctx, c.sessionID, c.nick, channel, key,
|
ctx, c.sessionID, c.clientID, c.nick, channel, key,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendIRCError(err)
|
c.sendIRCError(err)
|
||||||
@@ -337,7 +337,7 @@ func (c *Conn) partChannel(
|
|||||||
channel, reason string,
|
channel, reason string,
|
||||||
) {
|
) {
|
||||||
err := c.svc.PartChannel(
|
err := c.svc.PartChannel(
|
||||||
ctx, c.sessionID, c.nick, channel, reason,
|
ctx, c.sessionID, c.clientID, c.nick, channel, reason,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendIRCError(err)
|
c.sendIRCError(err)
|
||||||
@@ -401,7 +401,7 @@ func (c *Conn) handleTopic(
|
|||||||
newTopic := msg.Params[1]
|
newTopic := msg.Params[1]
|
||||||
|
|
||||||
err := c.svc.SetTopic(
|
err := c.svc.SetTopic(
|
||||||
ctx, c.sessionID, c.nick, channel, newTopic,
|
ctx, c.sessionID, c.clientID, c.nick, channel, newTopic,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.sendIRCError(err)
|
c.sendIRCError(err)
|
||||||
@@ -708,7 +708,7 @@ func (c *Conn) applyChannelModes(
|
|||||||
c.send(FormatMessage(c.hostmask(), "MODE", params...))
|
c.send(FormatMessage(c.hostmask(), "MODE", params...))
|
||||||
|
|
||||||
c.svc.BroadcastMode(
|
c.svc.BroadcastMode(
|
||||||
ctx, c.nick, channel, chID, modeReply,
|
ctx, c.clientID, c.nick, channel, chID, modeReply,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1185,7 @@ func (c *Conn) handleKick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := c.svc.KickUser(
|
err := c.svc.KickUser(
|
||||||
ctx, c.sessionID, c.nick,
|
ctx, c.sessionID, c.clientID, c.nick,
|
||||||
channel, targetNick, reason,
|
channel, targetNick, reason,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1267,7 +1267,7 @@ func (c *Conn) handleInvite(
|
|||||||
|
|
||||||
_, _, _ = c.svc.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = c.svc.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, "INVITE", c.nick, targetNick, targetID, 0,
|
ctx, "INVITE", c.nick, targetNick, targetID, 0,
|
||||||
nil, body, nil, []int64{targetID},
|
nil, body, nil, []int64{targetID}, 0,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ func (s *Server) Listener() net.Listener {
|
|||||||
return s.listener
|
return s.listener
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Service returns the service the server's connections use.
|
||||||
|
func (s *Server) Service() *service.Service {
|
||||||
|
return s.svc
|
||||||
|
}
|
||||||
|
|
||||||
// PingTimes returns how long the server's connections wait
|
// PingTimes returns how long the server's connections wait
|
||||||
// with nothing read before they are sent a PING, and after it
|
// with nothing read before they are sent a PING, and after it
|
||||||
// before they are closed.
|
// before they are closed.
|
||||||
|
|||||||
@@ -198,15 +198,6 @@ func (c *Conn) deliverTextMessage(
|
|||||||
from := msg.From
|
from := msg.From
|
||||||
target := msg.To
|
target := msg.To
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// Don't echo our own messages back.
|
|
||||||
if from == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := from
|
prefix := from
|
||||||
if !strings.Contains(prefix, "!") {
|
if !strings.Contains(prefix, "!") {
|
||||||
prefix = from + "!" + from + "@*"
|
prefix = from + "!" + from + "@*"
|
||||||
@@ -219,16 +210,6 @@ func (c *Conn) deliverTextMessage(
|
|||||||
|
|
||||||
// deliverJoin sends a JOIN notification.
|
// deliverJoin sends a JOIN notification.
|
||||||
func (c *Conn) deliverJoin(msg *db.IRCMessage) {
|
func (c *Conn) deliverJoin(msg *db.IRCMessage) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// Don't echo our own JOINs (we already sent them
|
|
||||||
// during joinChannel).
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
channel := msg.To
|
channel := msg.To
|
||||||
|
|
||||||
@@ -237,14 +218,6 @@ func (c *Conn) deliverJoin(msg *db.IRCMessage) {
|
|||||||
|
|
||||||
// deliverPart sends a PART notification.
|
// deliverPart sends a PART notification.
|
||||||
func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
|
func (c *Conn) deliverPart(msg *db.IRCMessage, text string) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
channel := msg.To
|
channel := msg.To
|
||||||
|
|
||||||
@@ -262,14 +235,6 @@ func (c *Conn) deliverNickChange(
|
|||||||
msg *db.IRCMessage,
|
msg *db.IRCMessage,
|
||||||
newNick string,
|
newNick string,
|
||||||
) {
|
) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
|
|
||||||
c.send(FormatMessage(prefix, "NICK", newNick))
|
c.send(FormatMessage(prefix, "NICK", newNick))
|
||||||
@@ -280,14 +245,6 @@ func (c *Conn) deliverQuitMsg(
|
|||||||
msg *db.IRCMessage,
|
msg *db.IRCMessage,
|
||||||
text string,
|
text string,
|
||||||
) {
|
) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
|
|
||||||
if text != "" {
|
if text != "" {
|
||||||
@@ -304,15 +261,6 @@ func (c *Conn) deliverTopicChange(
|
|||||||
msg *db.IRCMessage,
|
msg *db.IRCMessage,
|
||||||
text string,
|
text string,
|
||||||
) {
|
) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// handleTopic already echoed our own topic change.
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
channel := msg.To
|
channel := msg.To
|
||||||
|
|
||||||
@@ -324,15 +272,6 @@ func (c *Conn) deliverKickMsg(
|
|||||||
msg *db.IRCMessage,
|
msg *db.IRCMessage,
|
||||||
text string,
|
text string,
|
||||||
) {
|
) {
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// handleKick already echoed our own kick.
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
channel := msg.To
|
channel := msg.To
|
||||||
|
|
||||||
@@ -403,15 +342,6 @@ func (c *Conn) deliverMode(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
nick := c.nick
|
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
// applyChannelModes already echoed our own mode change.
|
|
||||||
if msg.From == nick {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix := msg.From + "!" + msg.From + "@*"
|
prefix := msg.From + "!" + msg.From + "@*"
|
||||||
|
|
||||||
params := slices.Concat(
|
params := slices.Concat(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ircserver_test
|
package ircserver_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sneak.berlin/go/neoirc/internal/ircserver"
|
"sneak.berlin/go/neoirc/internal/ircserver"
|
||||||
|
"sneak.berlin/go/neoirc/internal/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
// queueMessage queues a message for the user with nick
|
// queueMessage queues a message for the user with nick
|
||||||
@@ -39,7 +41,7 @@ func (env *testEnv) queueMessage(
|
|||||||
t.Fatalf("get session of %s: %v", recipient, err)
|
t.Fatalf("get session of %s: %v", recipient, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.database.EnqueueToSession(ctx, sessionID, msgID)
|
err = env.database.EnqueueToSession(ctx, sessionID, msgID, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("enqueue: %v", err)
|
t.Fatalf("enqueue: %v", err)
|
||||||
}
|
}
|
||||||
@@ -391,3 +393,211 @@ func TestRelayOwnChangeOnce(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRelayOwnSessionOnce checks that a command a user makes
|
||||||
|
// from one client of their session reaches each of the
|
||||||
|
// session's clients once: an IRC listener connection and an
|
||||||
|
// HTTP API client. The connection that sent a command gets
|
||||||
|
// only its own echo, and none for PRIVMSG and NOTICE, which
|
||||||
|
// IRC does not echo.
|
||||||
|
//
|
||||||
|
//nolint:funlen // table-driven test
|
||||||
|
func TestRelayOwnSessionOnce(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hello := json.RawMessage(`["hello"]`)
|
||||||
|
|
||||||
|
for _, step := range []struct {
|
||||||
|
// line is the command as alice sends it on the IRC
|
||||||
|
// listener.
|
||||||
|
line string
|
||||||
|
// api makes the same command for her session the way
|
||||||
|
// the HTTP API does.
|
||||||
|
api func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error
|
||||||
|
// echoes is how many copies alice's connection gets of
|
||||||
|
// line when it sends line itself.
|
||||||
|
echoes int
|
||||||
|
}{
|
||||||
|
{"PRIVMSG #chan :hello", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
_, err := svc.SendChannelMessage(
|
||||||
|
ctx, sessionID, 0, "alice", "PRIVMSG", "#chan", hello, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, 0},
|
||||||
|
{"NOTICE bob :hello", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
_, err := svc.SendDirectMessage(
|
||||||
|
ctx, sessionID, 0, "alice", "NOTICE", "bob", hello, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, 0},
|
||||||
|
{"JOIN #other", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
_, err := svc.JoinChannel(ctx, sessionID, 0, "alice", "#other", "")
|
||||||
|
|
||||||
|
return err
|
||||||
|
}, 1},
|
||||||
|
{"PART #chan :bye", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
return svc.PartChannel(ctx, sessionID, 0, "alice", "#chan", "bye")
|
||||||
|
}, 1},
|
||||||
|
{"NICK alice2", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
return svc.ChangeNick(ctx, sessionID, 0, "alice", "alice2")
|
||||||
|
}, 1},
|
||||||
|
{"TOPIC #chan :new topic", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
return svc.SetTopic(
|
||||||
|
ctx, sessionID, 0, "alice", "#chan", "new topic",
|
||||||
|
)
|
||||||
|
}, 1},
|
||||||
|
{"MODE #chan +m", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
chID, err := svc.ValidateChannelOp(ctx, sessionID, "#chan")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.SetChannelFlag(ctx, chID, 'm', true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.BroadcastMode(ctx, 0, "alice", "#chan", chID, "+m")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, 1},
|
||||||
|
{"KICK #chan bob :bye", func(
|
||||||
|
ctx context.Context, svc *service.Service, sessionID int64,
|
||||||
|
) error {
|
||||||
|
return svc.KickUser(
|
||||||
|
ctx, sessionID, 0, "alice", "#chan", "bob", "bye",
|
||||||
|
)
|
||||||
|
}, 1},
|
||||||
|
} {
|
||||||
|
command := strings.Fields(step.line)[0]
|
||||||
|
|
||||||
|
t.Run(command+" from IRC", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ircCopies, httpCopies := ownSessionCopies(t, step.line, nil)
|
||||||
|
if ircCopies != step.echoes || httpCopies != 1 {
|
||||||
|
t.Errorf("connection got %d, HTTP client got %d, want %d and 1",
|
||||||
|
ircCopies, httpCopies, step.echoes)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(command+" from the HTTP API", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ircCopies, httpCopies := ownSessionCopies(t, step.line, step.api)
|
||||||
|
if ircCopies != 1 || httpCopies != 1 {
|
||||||
|
t.Errorf("connection got %d, HTTP client got %d, want 1 and 1",
|
||||||
|
ircCopies, httpCopies)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ownSessionCopies connects alice to the IRC listener and has
|
||||||
|
// her join #chan, which bob is on too, then adds an HTTP API
|
||||||
|
// client to her session with the password her connection set,
|
||||||
|
// as POST /api/v1/login does. It makes the command line from
|
||||||
|
// her connection, or with api from the HTTP API client when api
|
||||||
|
// is not nil, and returns how many copies of it her connection
|
||||||
|
// and her HTTP API client got.
|
||||||
|
func ownSessionCopies(
|
||||||
|
t *testing.T,
|
||||||
|
line string,
|
||||||
|
api func(ctx context.Context, svc *service.Service, sessionID int64) error,
|
||||||
|
) (int, int) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const password = "alicepass"
|
||||||
|
|
||||||
|
ctx := t.Context()
|
||||||
|
env := newTestEnv(t)
|
||||||
|
|
||||||
|
alice := env.dial(t)
|
||||||
|
alice.send("PASS " + password)
|
||||||
|
alice.register("alice")
|
||||||
|
// Registration stores the password before the JOIN runs.
|
||||||
|
alice.joinAndDrain("#chan")
|
||||||
|
|
||||||
|
bob := env.dial(t)
|
||||||
|
bob.register("bob")
|
||||||
|
bob.joinAndDrain("#chan")
|
||||||
|
|
||||||
|
sessionID, httpClientID, _, err := env.database.LoginUser(
|
||||||
|
ctx, "alice", password, "127.0.0.1", "localhost",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("log in as alice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
if api == nil {
|
||||||
|
alice.send(line)
|
||||||
|
|
||||||
|
// Her echo comes before the reply to her next command.
|
||||||
|
lines = alice.sendAndExpect("PING :done", "PONG")
|
||||||
|
} else {
|
||||||
|
err = api(ctx, env.srv.Service(), sessionID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s over the HTTP API: %v", line, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, _, err := env.database.PollMessages(ctx, httpClientID, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("poll the HTTP API client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpCopies := 0
|
||||||
|
|
||||||
|
for _, msg := range msgs {
|
||||||
|
if msg.Command == strings.Fields(line)[0] {
|
||||||
|
httpCopies++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The relay sends a queue in order, so this arrives after any
|
||||||
|
// copy. It is queued by session, as NICK changes her nick.
|
||||||
|
endID, _, err := env.database.InsertMessage(
|
||||||
|
ctx, "PRIVMSG", "dave", "alice", 0, 0,
|
||||||
|
nil, json.RawMessage(`["the end"]`), nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = env.database.EnqueueToSession(ctx, sessionID, endID, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("enqueue: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env.brk.Notify(sessionID)
|
||||||
|
|
||||||
|
lines = append(lines, alice.readUntil(func(l string) bool {
|
||||||
|
return strings.HasSuffix(l, " :the end")
|
||||||
|
})...)
|
||||||
|
|
||||||
|
// A copy starts with the command and its first parameter.
|
||||||
|
start := " " + strings.Join(strings.Fields(line)[:2], " ")
|
||||||
|
ircCopies := strings.Count(strings.Join(lines, "\n"), start)
|
||||||
|
|
||||||
|
return ircCopies, httpCopies
|
||||||
|
}
|
||||||
|
|||||||
+64
-68
@@ -312,7 +312,7 @@ func (s *Service) SendWallops(
|
|||||||
|
|
||||||
_, _, err = s.FanOut(
|
_, _, err = s.FanOut(
|
||||||
ctx, irc.CmdWallops, nick, "*", 0, 0,
|
ctx, irc.CmdWallops, nick, "*", 0, 0,
|
||||||
nil, body, nil, recipients,
|
nil, body, nil, recipients, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wallops: %w", err)
|
return fmt.Errorf("wallops: %w", err)
|
||||||
@@ -347,12 +347,19 @@ type DirectMsgResult struct {
|
|||||||
// session IDs, notifying each via the broker. toSessionID
|
// session IDs, notifying each via the broker. toSessionID
|
||||||
// and toChannelID are the recipient InsertMessage records
|
// and toChannelID are the recipient InsertMessage records
|
||||||
// for history, 0 when there is none.
|
// for history, 0 when there is none.
|
||||||
|
//
|
||||||
|
// skipClientID is the client whose queue the message is
|
||||||
|
// not added to: the IRC listener connection that sent the
|
||||||
|
// command, which has already shown it to its user. The HTTP
|
||||||
|
// API passes 0, as its clients see their own commands
|
||||||
|
// through their queues.
|
||||||
func (s *Service) FanOut(
|
func (s *Service) FanOut(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
command, from, to string,
|
command, from, to string,
|
||||||
toSessionID, toChannelID int64,
|
toSessionID, toChannelID int64,
|
||||||
params, body, meta json.RawMessage,
|
params, body, meta json.RawMessage,
|
||||||
sessionIDs []int64,
|
sessionIDs []int64,
|
||||||
|
skipClientID int64,
|
||||||
) (int64, string, error) {
|
) (int64, string, error) {
|
||||||
dbID, msgUUID, err := s.db.InsertMessage(
|
dbID, msgUUID, err := s.db.InsertMessage(
|
||||||
ctx, command, from, to,
|
ctx, command, from, to,
|
||||||
@@ -363,58 +370,40 @@ func (s *Service) FanOut(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, sid := range sessionIDs {
|
for _, sid := range sessionIDs {
|
||||||
_ = s.db.EnqueueToSession(ctx, sid, dbID)
|
_ = s.db.EnqueueToSession(ctx, sid, dbID, skipClientID)
|
||||||
s.broker.Notify(sid)
|
s.broker.Notify(sid)
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbID, msgUUID, nil
|
return dbID, msgUUID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// excludeSession returns a copy of ids without the given
|
|
||||||
// session.
|
|
||||||
func excludeSession(
|
|
||||||
ids []int64,
|
|
||||||
exclude int64,
|
|
||||||
) []int64 {
|
|
||||||
out := make([]int64, 0, len(ids))
|
|
||||||
|
|
||||||
for _, id := range ids {
|
|
||||||
if id != exclude {
|
|
||||||
out = append(out, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendChannelMessage checks membership, bans and
|
// SendChannelMessage checks membership, bans and
|
||||||
// moderation, then the hashcash stamp of a PRIVMSG to a +H
|
// moderation, then the hashcash stamp of a PRIVMSG to a +H
|
||||||
// channel, then fans out a message to all channel members
|
// channel, then fans out a message to all channel members,
|
||||||
// except the sender. The stamp is checked after the other
|
// the sender's session included, skipping client
|
||||||
// checks so that a message they refuse does not spend it.
|
// skipClientID as FanOut does. The stamp is checked after
|
||||||
// Returns the database row ID, message UUID, and any error.
|
// the other checks so that a message they refuse does not
|
||||||
// The dbID lets callers enqueue the same message to the
|
// spend it. Returns the message UUID and any error.
|
||||||
// sender when echo is needed (HTTP transport).
|
|
||||||
func (s *Service) SendChannelMessage(
|
func (s *Service) SendChannelMessage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, command, channel string,
|
nick, command, channel string,
|
||||||
body, meta json.RawMessage,
|
body, meta json.RawMessage,
|
||||||
) (int64, string, error) {
|
) (string, error) {
|
||||||
noSuchChannel := &IRCError{
|
noSuchChannel := &IRCError{
|
||||||
irc.ErrNoSuchChannel, []string{channel}, msgNoSuchChannel,
|
irc.ErrNoSuchChannel, []string{channel}, msgNoSuchChannel,
|
||||||
}
|
}
|
||||||
|
|
||||||
chID, err := s.db.GetChannelByName(ctx, channel)
|
chID, err := s.db.GetChannelByName(ctx, channel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", noSuchChannel
|
return "", noSuchChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
isMember, _ := s.db.IsChannelMember(
|
isMember, _ := s.db.IsChannelMember(
|
||||||
ctx, chID, sessionID,
|
ctx, chID, sessionID,
|
||||||
)
|
)
|
||||||
if !isMember {
|
if !isMember {
|
||||||
return 0, "", &IRCError{
|
return "", &IRCError{
|
||||||
irc.ErrCannotSendToChan,
|
irc.ErrCannotSendToChan,
|
||||||
[]string{channel},
|
[]string{channel},
|
||||||
"Cannot send to channel",
|
"Cannot send to channel",
|
||||||
@@ -426,7 +415,7 @@ func (s *Service) SendChannelMessage(
|
|||||||
ctx, chID, sessionID,
|
ctx, chID, sessionID,
|
||||||
)
|
)
|
||||||
if banErr == nil && isBanned {
|
if banErr == nil && isBanned {
|
||||||
return 0, "", &IRCError{
|
return "", &IRCError{
|
||||||
irc.ErrCannotSendToChan,
|
irc.ErrCannotSendToChan,
|
||||||
[]string{channel},
|
[]string{channel},
|
||||||
"Cannot send to channel (+b)",
|
"Cannot send to channel (+b)",
|
||||||
@@ -443,7 +432,7 @@ func (s *Service) SendChannelMessage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if !isOp && !isVoiced {
|
if !isOp && !isVoiced {
|
||||||
return 0, "", &IRCError{
|
return "", &IRCError{
|
||||||
irc.ErrCannotSendToChan,
|
irc.ErrCannotSendToChan,
|
||||||
[]string{channel},
|
[]string{channel},
|
||||||
"Cannot send to channel (+m)",
|
"Cannot send to channel (+m)",
|
||||||
@@ -456,35 +445,35 @@ func (s *Service) SendChannelMessage(
|
|||||||
ctx, chID, channel, body, meta,
|
ctx, chID, channel, body, meta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID)
|
memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID)
|
||||||
recipients := excludeSession(memberIDs, sessionID)
|
|
||||||
|
|
||||||
dbID, uuid, fanErr := s.FanOut(
|
_, uuid, fanErr := s.FanOut(
|
||||||
ctx, command, nick, channel, 0, chID,
|
ctx, command, nick, channel, 0, chID,
|
||||||
nil, body, meta, recipients,
|
nil, body, meta, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
// The channel was deleted after the lookup above.
|
// The channel was deleted after the lookup above.
|
||||||
if db.IsForeignKeyConstraintError(fanErr) {
|
if db.IsForeignKeyConstraintError(fanErr) {
|
||||||
return 0, "", noSuchChannel
|
return "", noSuchChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
if fanErr != nil {
|
if fanErr != nil {
|
||||||
return 0, "", fanErr
|
return "", fanErr
|
||||||
}
|
}
|
||||||
|
|
||||||
return dbID, uuid, nil
|
return uuid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendDirectMessage validates the target and sends a
|
// SendDirectMessage validates the target and sends a
|
||||||
// direct message, returning the message UUID and any away
|
// direct message to the target and the sender's session,
|
||||||
// message set on the target.
|
// skipping client skipClientID as FanOut does, and returns
|
||||||
|
// the message UUID and any away message set on the target.
|
||||||
func (s *Service) SendDirectMessage(
|
func (s *Service) SendDirectMessage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, command, target string,
|
nick, command, target string,
|
||||||
body, meta json.RawMessage,
|
body, meta json.RawMessage,
|
||||||
) (*DirectMsgResult, error) {
|
) (*DirectMsgResult, error) {
|
||||||
@@ -506,7 +495,7 @@ func (s *Service) SendDirectMessage(
|
|||||||
|
|
||||||
_, uuid, fanErr := s.FanOut(
|
_, uuid, fanErr := s.FanOut(
|
||||||
ctx, command, nick, target, targetSID, 0,
|
ctx, command, nick, target, targetSID, 0,
|
||||||
nil, body, meta, recipients,
|
nil, body, meta, recipients, skipClientID,
|
||||||
)
|
)
|
||||||
// The target's session was deleted after the lookup above.
|
// The target's session was deleted after the lookup above.
|
||||||
if db.IsForeignKeyConstraintError(fanErr) {
|
if db.IsForeignKeyConstraintError(fanErr) {
|
||||||
@@ -522,11 +511,12 @@ func (s *Service) SendDirectMessage(
|
|||||||
|
|
||||||
// JoinChannel creates or joins a channel, making the
|
// JoinChannel creates or joins a channel, making the
|
||||||
// first joiner the operator. Fans out the JOIN to all
|
// first joiner the operator. Fans out the JOIN to all
|
||||||
// channel members. A channel name that validChannelRe
|
// channel members, skipping client skipClientID as FanOut
|
||||||
// does not match gets ERR_NOSUCHCHANNEL.
|
// does. A channel name that validChannelRe does not match
|
||||||
|
// gets ERR_NOSUCHCHANNEL.
|
||||||
func (s *Service) JoinChannel(
|
func (s *Service) JoinChannel(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, channel, suppliedKey string,
|
nick, channel, suppliedKey string,
|
||||||
) (*JoinResult, error) {
|
) (*JoinResult, error) {
|
||||||
if !validChannelRe.MatchString(channel) {
|
if !validChannelRe.MatchString(channel) {
|
||||||
@@ -549,7 +539,7 @@ func (s *Service) JoinChannel(
|
|||||||
|
|
||||||
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, irc.CmdJoin, nick, channel, 0, chID,
|
ctx, irc.CmdJoin, nick, channel, 0, chID,
|
||||||
nil, body, nil, memberIDs,
|
nil, body, nil, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
|
|
||||||
return &JoinResult{
|
return &JoinResult{
|
||||||
@@ -558,12 +548,12 @@ func (s *Service) JoinChannel(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PartChannel validates membership, broadcasts PART to
|
// PartChannel validates membership, broadcasts PART to all
|
||||||
// remaining members, removes the user, and cleans up empty
|
// channel members, skipping client skipClientID as FanOut
|
||||||
// channels.
|
// does, removes the user, and cleans up empty channels.
|
||||||
func (s *Service) PartChannel(
|
func (s *Service) PartChannel(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, channel, reason string,
|
nick, channel, reason string,
|
||||||
) error {
|
) error {
|
||||||
chID, err := s.db.GetChannelByName(ctx, channel)
|
chID, err := s.db.GetChannelByName(ctx, channel)
|
||||||
@@ -587,12 +577,11 @@ func (s *Service) PartChannel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID)
|
memberIDs, _ := s.db.GetChannelMemberIDs(ctx, chID)
|
||||||
recipients := excludeSession(memberIDs, sessionID)
|
|
||||||
body, _ := json.Marshal([]string{reason}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{reason}) //nolint:errchkjson
|
||||||
|
|
||||||
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, irc.CmdPart, nick, channel, 0, chID,
|
ctx, irc.CmdPart, nick, channel, 0, chID,
|
||||||
nil, body, nil, recipients,
|
nil, body, nil, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
|
|
||||||
s.db.PartChannelAndDeleteIfEmpty(ctx, chID, sessionID) //nolint:errcheck,gosec
|
s.db.PartChannelAndDeleteIfEmpty(ctx, chID, sessionID) //nolint:errcheck,gosec
|
||||||
@@ -601,10 +590,11 @@ func (s *Service) PartChannel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetTopic validates membership and topic-lock, sets the
|
// SetTopic validates membership and topic-lock, sets the
|
||||||
// topic, and broadcasts the change.
|
// topic, and broadcasts the change to all channel members,
|
||||||
|
// skipping client skipClientID as FanOut does.
|
||||||
func (s *Service) SetTopic(
|
func (s *Service) SetTopic(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, channel, topic string,
|
nick, channel, topic string,
|
||||||
) error {
|
) error {
|
||||||
chID, err := s.db.GetChannelByName(ctx, channel)
|
chID, err := s.db.GetChannelByName(ctx, channel)
|
||||||
@@ -653,18 +643,19 @@ func (s *Service) SetTopic(
|
|||||||
|
|
||||||
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, irc.CmdTopic, nick, channel, 0, chID,
|
ctx, irc.CmdTopic, nick, channel, 0, chID,
|
||||||
nil, body, nil, memberIDs,
|
nil, body, nil, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// KickUser validates operator status and target
|
// KickUser validates operator status and target
|
||||||
// membership, broadcasts the KICK, removes the target,
|
// membership, broadcasts the KICK to all channel members,
|
||||||
// and cleans up empty channels.
|
// skipping client skipClientID as FanOut does, removes the
|
||||||
|
// target, and cleans up empty channels.
|
||||||
func (s *Service) KickUser(
|
func (s *Service) KickUser(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
nick, channel, targetNick, reason string,
|
nick, channel, targetNick, reason string,
|
||||||
) error {
|
) error {
|
||||||
chID, err := s.db.GetChannelByName(ctx, channel)
|
chID, err := s.db.GetChannelByName(ctx, channel)
|
||||||
@@ -717,7 +708,7 @@ func (s *Service) KickUser(
|
|||||||
|
|
||||||
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, irc.CmdKick, nick, channel, 0, chID,
|
ctx, irc.CmdKick, nick, channel, 0, chID,
|
||||||
params, body, nil, memberIDs,
|
params, body, nil, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
|
|
||||||
s.db.PartChannelAndDeleteIfEmpty(ctx, chID, targetSID) //nolint:errcheck,gosec
|
s.db.PartChannelAndDeleteIfEmpty(ctx, chID, targetSID) //nolint:errcheck,gosec
|
||||||
@@ -798,10 +789,11 @@ func (s *Service) InviteUser(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ChangeNick changes a user's nickname and broadcasts the
|
// ChangeNick changes a user's nickname and broadcasts the
|
||||||
// change to all users sharing channels.
|
// change to the user's own session and all users sharing
|
||||||
|
// channels, skipping client skipClientID as FanOut does.
|
||||||
func (s *Service) ChangeNick(
|
func (s *Service) ChangeNick(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
oldNick, newNick string,
|
oldNick, newNick string,
|
||||||
) error {
|
) error {
|
||||||
err := s.db.ChangeNick(ctx, sessionID, newNick)
|
err := s.db.ChangeNick(ctx, sessionID, newNick)
|
||||||
@@ -822,7 +814,9 @@ func (s *Service) ChangeNick(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.broadcastNickChange(ctx, sessionID, oldNick, newNick)
|
s.broadcastNickChange(
|
||||||
|
ctx, sessionID, skipClientID, oldNick, newNick,
|
||||||
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1042,9 +1036,10 @@ func (s *Service) SetChannelFlag(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastMode fans out a MODE change to all channel
|
// BroadcastMode fans out a MODE change to all channel
|
||||||
// members.
|
// members, skipping client skipClientID as FanOut does.
|
||||||
func (s *Service) BroadcastMode(
|
func (s *Service) BroadcastMode(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
skipClientID int64,
|
||||||
nick, channel string,
|
nick, channel string,
|
||||||
chID int64,
|
chID int64,
|
||||||
modeText string,
|
modeText string,
|
||||||
@@ -1054,7 +1049,7 @@ func (s *Service) BroadcastMode(
|
|||||||
|
|
||||||
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
_, _, _ = s.FanOut( //nolint:dogsled // fire-and-forget broadcast
|
||||||
ctx, irc.CmdMode, nick, channel, 0, chID,
|
ctx, irc.CmdMode, nick, channel, 0, chID,
|
||||||
nil, body, nil, memberIDs,
|
nil, body, nil, memberIDs, skipClientID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,11 +1301,12 @@ func (s *Service) checkChannelHashcash(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// broadcastNickChange notifies channel peers of a nick
|
// broadcastNickChange notifies the user's own session,
|
||||||
|
// except client skipClientID, and channel peers of a nick
|
||||||
// change.
|
// change.
|
||||||
func (s *Service) broadcastNickChange(
|
func (s *Service) broadcastNickChange(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
sessionID int64,
|
sessionID, skipClientID int64,
|
||||||
oldNick, newNick string,
|
oldNick, newNick string,
|
||||||
) {
|
) {
|
||||||
channels, err := s.db.GetSessionChannels(
|
channels, err := s.db.GetSessionChannels(
|
||||||
@@ -1332,7 +1328,7 @@ func (s *Service) broadcastNickChange(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user themselves (for multi-client sync).
|
// Notify the user themselves (for multi-client sync).
|
||||||
_ = s.db.EnqueueToSession(ctx, sessionID, dbID)
|
_ = s.db.EnqueueToSession(ctx, sessionID, dbID, skipClientID)
|
||||||
s.broker.Notify(sessionID)
|
s.broker.Notify(sessionID)
|
||||||
notified[sessionID] = true
|
notified[sessionID] = true
|
||||||
|
|
||||||
@@ -1351,7 +1347,7 @@ func (s *Service) broadcastNickChange(
|
|||||||
|
|
||||||
notified[mid] = true
|
notified[mid] = true
|
||||||
|
|
||||||
_ = s.db.EnqueueToSession(ctx, mid, dbID)
|
_ = s.db.EnqueueToSession(ctx, mid, dbID, 0)
|
||||||
s.broker.Notify(mid)
|
s.broker.Notify(mid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ func TestFanOut(t *testing.T) {
|
|||||||
dbID, uuid, err := env.svc.FanOut(
|
dbID, uuid, err := env.svc.FanOut(
|
||||||
ctx, irc.CmdPrivmsg, "alice", "#test", 0, 0,
|
ctx, irc.CmdPrivmsg, "alice", "#test", 0, 0,
|
||||||
nil, body, nil,
|
nil, body, nil,
|
||||||
[]int64{sid1, sid2},
|
[]int64{sid1, sid2}, 0,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("FanOut: %v", err)
|
t.Fatalf("FanOut: %v", err)
|
||||||
@@ -190,7 +190,7 @@ func TestJoinChannel(t *testing.T) {
|
|||||||
sid := createSession(ctx, t, env.db, "alice")
|
sid := createSession(ctx, t, env.db, "alice")
|
||||||
|
|
||||||
result, err := env.svc.JoinChannel(
|
result, err := env.svc.JoinChannel(
|
||||||
ctx, sid, "alice", "#general", "",
|
ctx, sid, 0, "alice", "#general", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JoinChannel: %v", err)
|
t.Fatalf("JoinChannel: %v", err)
|
||||||
@@ -208,7 +208,7 @@ func TestJoinChannel(t *testing.T) {
|
|||||||
sid2 := createSession(ctx, t, env.db, "bob")
|
sid2 := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
result2, err := env.svc.JoinChannel(
|
result2, err := env.svc.JoinChannel(
|
||||||
ctx, sid2, "bob", "#general", "",
|
ctx, sid2, 0, "bob", "#general", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JoinChannel bob: %v", err)
|
t.Fatalf("JoinChannel bob: %v", err)
|
||||||
@@ -230,14 +230,14 @@ func TestPartChannel(t *testing.T) {
|
|||||||
sid := createSession(ctx, t, env.db, "alice")
|
sid := createSession(ctx, t, env.db, "alice")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(
|
_, err := env.svc.JoinChannel(
|
||||||
ctx, sid, "alice", "#general", "",
|
ctx, sid, 0, "alice", "#general", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("JoinChannel: %v", err)
|
t.Fatalf("JoinChannel: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.svc.PartChannel(
|
err = env.svc.PartChannel(
|
||||||
ctx, sid, "alice", "#general", "bye",
|
ctx, sid, 0, "alice", "#general", "bye",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("PartChannel: %v", err)
|
t.Fatalf("PartChannel: %v", err)
|
||||||
@@ -245,7 +245,7 @@ func TestPartChannel(t *testing.T) {
|
|||||||
|
|
||||||
// Parting a non-existent channel returns error.
|
// Parting a non-existent channel returns error.
|
||||||
err = env.svc.PartChannel(
|
err = env.svc.PartChannel(
|
||||||
ctx, sid, "alice", "#nonexistent", "",
|
ctx, sid, 0, "alice", "#nonexistent", "",
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("expected error for non-existent channel")
|
t.Error("expected error for non-existent channel")
|
||||||
@@ -309,7 +309,7 @@ func joinConcurrently(
|
|||||||
|
|
||||||
joins[i] = func() error {
|
joins[i] = func() error {
|
||||||
result, err := env.svc.JoinChannel(
|
result, err := env.svc.JoinChannel(
|
||||||
ctx, sessionID, nick, channel, "",
|
ctx, sessionID, 0, nick, channel, "",
|
||||||
)
|
)
|
||||||
results[i] = result
|
results[i] = result
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ func TestConcurrentJoinsRespectUserLimit(t *testing.T) {
|
|||||||
opSID := createSession(ctx, t, env.db, opNick)
|
opSID := createSession(ctx, t, env.db, opNick)
|
||||||
|
|
||||||
created, err := env.svc.JoinChannel(
|
created, err := env.svc.JoinChannel(
|
||||||
ctx, opSID, opNick, channel, "",
|
ctx, opSID, 0, opNick, channel, "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", opNick, err)
|
t.Fatalf("join %s: %v", opNick, err)
|
||||||
@@ -448,7 +448,7 @@ func TestJoinConcurrentWithLastPart(t *testing.T) {
|
|||||||
joinerSID := createSession(ctx, t, env.db, joinerNick)
|
joinerSID := createSession(ctx, t, env.db, joinerNick)
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(
|
_, err := env.svc.JoinChannel(
|
||||||
ctx, leaverSID, leaverNick, channel, "",
|
ctx, leaverSID, 0, leaverNick, channel, "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", leaverNick, err)
|
t.Fatalf("join %s: %v", leaverNick, err)
|
||||||
@@ -457,12 +457,12 @@ func TestJoinConcurrentWithLastPart(t *testing.T) {
|
|||||||
errs := runConcurrently(
|
errs := runConcurrently(
|
||||||
func() error {
|
func() error {
|
||||||
return env.svc.PartChannel(
|
return env.svc.PartChannel(
|
||||||
ctx, leaverSID, leaverNick, channel, "",
|
ctx, leaverSID, 0, leaverNick, channel, "",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
func() error {
|
func() error {
|
||||||
_, joinErr := env.svc.JoinChannel(
|
_, joinErr := env.svc.JoinChannel(
|
||||||
ctx, joinerSID, joinerNick, channel, "",
|
ctx, joinerSID, 0, joinerNick, channel, "",
|
||||||
)
|
)
|
||||||
|
|
||||||
return joinErr
|
return joinErr
|
||||||
@@ -513,7 +513,7 @@ func TestJoinConcurrentWithLastQuit(t *testing.T) {
|
|||||||
joinerSID := createSession(ctx, t, env.db, joinerNick)
|
joinerSID := createSession(ctx, t, env.db, joinerNick)
|
||||||
|
|
||||||
created, err := env.svc.JoinChannel(
|
created, err := env.svc.JoinChannel(
|
||||||
ctx, leaverSID, leaverNick, channel, "",
|
ctx, leaverSID, 0, leaverNick, channel, "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", leaverNick, err)
|
t.Fatalf("join %s: %v", leaverNick, err)
|
||||||
@@ -545,7 +545,7 @@ func TestJoinConcurrentWithLastQuit(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
joined, joinErr = env.svc.JoinChannel(
|
joined, joinErr = env.svc.JoinChannel(
|
||||||
ctx, joinerSID, joinerNick, channel, "",
|
ctx, joinerSID, 0, joinerNick, channel, "",
|
||||||
)
|
)
|
||||||
if !errors.As(joinErr, &ircErr) ||
|
if !errors.As(joinErr, &ircErr) ||
|
||||||
ircErr.Code != irc.ErrBadChannelKey {
|
ircErr.Code != irc.ErrBadChannelKey {
|
||||||
@@ -592,7 +592,7 @@ func TestQuitQueuedAheadOfNewNickHoldersJoin(t *testing.T) {
|
|||||||
quitterSID := createSession(ctx, t, env.db, nick)
|
quitterSID := createSession(ctx, t, env.db, nick)
|
||||||
|
|
||||||
for _, channel := range []string{crowd, shared} {
|
for _, channel := range []string{crowd, shared} {
|
||||||
_, err := env.svc.JoinChannel(ctx, quitterSID, nick, channel, "")
|
_, err := env.svc.JoinChannel(ctx, quitterSID, 0, nick, channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", channel, err)
|
t.Fatalf("join %s: %v", channel, err)
|
||||||
}
|
}
|
||||||
@@ -602,7 +602,7 @@ func TestQuitQueuedAheadOfNewNickHoldersJoin(t *testing.T) {
|
|||||||
memberNick := fmt.Sprintf("crowd%dx%d", trial, member)
|
memberNick := fmt.Sprintf("crowd%dx%d", trial, member)
|
||||||
memberSID := createSession(ctx, t, env.db, memberNick)
|
memberSID := createSession(ctx, t, env.db, memberNick)
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(ctx, memberSID, memberNick, crowd, "")
|
_, err := env.svc.JoinChannel(ctx, memberSID, 0, memberNick, crowd, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", memberNick, err)
|
t.Fatalf("join %s: %v", memberNick, err)
|
||||||
}
|
}
|
||||||
@@ -617,7 +617,7 @@ func TestQuitQueuedAheadOfNewNickHoldersJoin(t *testing.T) {
|
|||||||
t.Fatalf("create session %s: %v", watcherNick, err)
|
t.Fatalf("create session %s: %v", watcherNick, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, watcherSID, watcherNick, shared, "")
|
_, err = env.svc.JoinChannel(ctx, watcherSID, 0, watcherNick, shared, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join %s: %v", watcherNick, err)
|
t.Fatalf("join %s: %v", watcherNick, err)
|
||||||
}
|
}
|
||||||
@@ -678,7 +678,7 @@ func takeNickAndJoin(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, sessionID, nick, channel, "")
|
_, err = env.svc.JoinChannel(ctx, sessionID, 0, nick, channel, "")
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -710,7 +710,7 @@ func TestJoinUsesUpInvite(t *testing.T) {
|
|||||||
opSID := createSession(ctx, t, env.db, "op")
|
opSID := createSession(ctx, t, env.db, "op")
|
||||||
guestSID := createSession(ctx, t, env.db, "guest")
|
guestSID := createSession(ctx, t, env.db, "guest")
|
||||||
|
|
||||||
created, err := env.svc.JoinChannel(ctx, opSID, "op", channel, "")
|
created, err := env.svc.JoinChannel(ctx, opSID, 0, "op", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join op: %v", err)
|
t.Fatalf("join op: %v", err)
|
||||||
}
|
}
|
||||||
@@ -725,17 +725,17 @@ func TestJoinUsesUpInvite(t *testing.T) {
|
|||||||
t.Fatalf("invite guest: %v", err)
|
t.Fatalf("invite guest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, guestSID, "guest", channel, "")
|
_, err = env.svc.JoinChannel(ctx, guestSID, 0, "guest", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("invited join: %v", err)
|
t.Fatalf("invited join: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.svc.PartChannel(ctx, guestSID, "guest", channel, "")
|
err = env.svc.PartChannel(ctx, guestSID, 0, "guest", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("part guest: %v", err)
|
t.Fatalf("part guest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, guestSID, "guest", channel, "")
|
_, err = env.svc.JoinChannel(ctx, guestSID, 0, "guest", channel, "")
|
||||||
|
|
||||||
var ircErr *service.IRCError
|
var ircErr *service.IRCError
|
||||||
if !errors.As(err, &ircErr) || ircErr.Code != irc.ErrInviteOnlyChan {
|
if !errors.As(err, &ircErr) || ircErr.Code != irc.ErrInviteOnlyChan {
|
||||||
@@ -754,7 +754,7 @@ func TestJoinRefusedByHostmaskBan(t *testing.T) {
|
|||||||
|
|
||||||
opSID := createSession(ctx, t, env.db, "op")
|
opSID := createSession(ctx, t, env.db, "op")
|
||||||
|
|
||||||
created, err := env.svc.JoinChannel(ctx, opSID, "op", channel, "")
|
created, err := env.svc.JoinChannel(ctx, opSID, 0, "op", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join op: %v", err)
|
t.Fatalf("join op: %v", err)
|
||||||
}
|
}
|
||||||
@@ -773,7 +773,7 @@ func TestJoinRefusedByHostmaskBan(t *testing.T) {
|
|||||||
t.Fatalf("create session guest: %v", err)
|
t.Fatalf("create session guest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, guestSID, "guest", channel, "")
|
_, err = env.svc.JoinChannel(ctx, guestSID, 0, "guest", channel, "")
|
||||||
|
|
||||||
var ircErr *service.IRCError
|
var ircErr *service.IRCError
|
||||||
if !errors.As(err, &ircErr) || ircErr.Code != irc.ErrBannedFromChan {
|
if !errors.As(err, &ircErr) || ircErr.Code != irc.ErrBannedFromChan {
|
||||||
@@ -793,17 +793,17 @@ func TestPartDeletesChannelOnlyWhenEmpty(t *testing.T) {
|
|||||||
aliceSID := createSession(ctx, t, env.db, "alice")
|
aliceSID := createSession(ctx, t, env.db, "alice")
|
||||||
bobSID := createSession(ctx, t, env.db, "bob")
|
bobSID := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(ctx, aliceSID, "alice", channel, "")
|
_, err := env.svc.JoinChannel(ctx, aliceSID, 0, "alice", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, bobSID, "bob", channel, "")
|
_, err = env.svc.JoinChannel(ctx, bobSID, 0, "bob", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join bob: %v", err)
|
t.Fatalf("join bob: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.svc.PartChannel(ctx, aliceSID, "alice", channel, "")
|
err = env.svc.PartChannel(ctx, aliceSID, 0, "alice", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("part alice: %v", err)
|
t.Fatalf("part alice: %v", err)
|
||||||
}
|
}
|
||||||
@@ -818,7 +818,7 @@ func TestPartDeletesChannelOnlyWhenEmpty(t *testing.T) {
|
|||||||
t.Fatal("bob is not on the channel after alice parted")
|
t.Fatal("bob is not on the channel after alice parted")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.svc.PartChannel(ctx, bobSID, "bob", channel, "")
|
err = env.svc.PartChannel(ctx, bobSID, 0, "bob", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("part bob: %v", err)
|
t.Fatalf("part bob: %v", err)
|
||||||
}
|
}
|
||||||
@@ -839,12 +839,12 @@ func TestKickOfLastMemberDeletesChannel(t *testing.T) {
|
|||||||
|
|
||||||
sid := createSession(ctx, t, env.db, "alice")
|
sid := createSession(ctx, t, env.db, "alice")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(ctx, sid, "alice", channel, "")
|
_, err := env.svc.JoinChannel(ctx, sid, 0, "alice", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = env.svc.KickUser(ctx, sid, "alice", channel, "alice", "")
|
err = env.svc.KickUser(ctx, sid, 0, "alice", channel, "alice", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("kick alice: %v", err)
|
t.Fatalf("kick alice: %v", err)
|
||||||
}
|
}
|
||||||
@@ -863,14 +863,14 @@ func TestSendChannelMessage(t *testing.T) {
|
|||||||
sid2 := createSession(ctx, t, env.db, "bob")
|
sid2 := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(
|
_, err := env.svc.JoinChannel(
|
||||||
ctx, sid1, "alice", "#chat", "",
|
ctx, sid1, 0, "alice", "#chat", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(
|
_, err = env.svc.JoinChannel(
|
||||||
ctx, sid2, "bob", "#chat", "",
|
ctx, sid2, 0, "bob", "#chat", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join bob: %v", err)
|
t.Fatalf("join bob: %v", err)
|
||||||
@@ -878,18 +878,14 @@ func TestSendChannelMessage(t *testing.T) {
|
|||||||
|
|
||||||
body, _ := json.Marshal([]string{"hello world"}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{"hello world"}) //nolint:errchkjson
|
||||||
|
|
||||||
dbID, uuid, err := env.svc.SendChannelMessage(
|
uuid, err := env.svc.SendChannelMessage(
|
||||||
ctx, sid1, "alice",
|
ctx, sid1, 0, "alice",
|
||||||
irc.CmdPrivmsg, "#chat", body, nil,
|
irc.CmdPrivmsg, "#chat", body, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("SendChannelMessage: %v", err)
|
t.Fatalf("SendChannelMessage: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dbID == 0 {
|
|
||||||
t.Error("expected non-zero dbID")
|
|
||||||
}
|
|
||||||
|
|
||||||
if uuid == "" {
|
if uuid == "" {
|
||||||
t.Error("expected non-empty UUID")
|
t.Error("expected non-empty UUID")
|
||||||
}
|
}
|
||||||
@@ -897,8 +893,8 @@ func TestSendChannelMessage(t *testing.T) {
|
|||||||
// Non-member cannot send.
|
// Non-member cannot send.
|
||||||
sid3 := createSession(ctx, t, env.db, "charlie")
|
sid3 := createSession(ctx, t, env.db, "charlie")
|
||||||
|
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, sid3, "charlie",
|
ctx, sid3, 0, "charlie",
|
||||||
irc.CmdPrivmsg, "#chat", body, nil,
|
irc.CmdPrivmsg, "#chat", body, nil,
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -914,14 +910,14 @@ func TestBroadcastQuit(t *testing.T) {
|
|||||||
sid2 := createSession(ctx, t, env.db, "bob")
|
sid2 := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(
|
_, err := env.svc.JoinChannel(
|
||||||
ctx, sid1, "alice", "#room", "",
|
ctx, sid1, 0, "alice", "#room", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(
|
_, err = env.svc.JoinChannel(
|
||||||
ctx, sid2, "bob", "#room", "",
|
ctx, sid2, 0, "bob", "#room", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join bob: %v", err)
|
t.Fatalf("join bob: %v", err)
|
||||||
@@ -963,7 +959,7 @@ func TestSendDirectMessageTargetQuits(t *testing.T) {
|
|||||||
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
||||||
|
|
||||||
_, err = env.svc.SendDirectMessage(
|
_, err = env.svc.SendDirectMessage(
|
||||||
ctx, sid, "alice", irc.CmdPrivmsg, "bob", body, nil,
|
ctx, sid, 0, "alice", irc.CmdPrivmsg, "bob", body, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
var ircErr *service.IRCError
|
var ircErr *service.IRCError
|
||||||
@@ -983,7 +979,7 @@ func TestSendChannelMessageChannelDeleted(t *testing.T) {
|
|||||||
|
|
||||||
sid := createSession(ctx, t, env.db, "alice")
|
sid := createSession(ctx, t, env.db, "alice")
|
||||||
|
|
||||||
_, err := env.svc.JoinChannel(ctx, sid, "alice", "#gone", "")
|
_, err := env.svc.JoinChannel(ctx, sid, 0, "alice", "#gone", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1000,8 +996,8 @@ func TestSendChannelMessageChannelDeleted(t *testing.T) {
|
|||||||
|
|
||||||
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{"hi"}) //nolint:errchkjson
|
||||||
|
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, sid, "alice", irc.CmdPrivmsg, "#gone", body, nil,
|
ctx, sid, 0, "alice", irc.CmdPrivmsg, "#gone", body, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
var ircErr *service.IRCError
|
var ircErr *service.IRCError
|
||||||
@@ -1019,14 +1015,14 @@ func TestSendChannelMessage_Moderated(t *testing.T) {
|
|||||||
sid2 := createSession(ctx, t, env.db, "bob")
|
sid2 := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
result, err := env.svc.JoinChannel(
|
result, err := env.svc.JoinChannel(
|
||||||
ctx, sid1, "alice", "#modchat", "",
|
ctx, sid1, 0, "alice", "#modchat", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(
|
_, err = env.svc.JoinChannel(
|
||||||
ctx, sid2, "bob", "#modchat", "",
|
ctx, sid2, 0, "bob", "#modchat", "",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join bob: %v", err)
|
t.Fatalf("join bob: %v", err)
|
||||||
@@ -1039,8 +1035,8 @@ func TestSendChannelMessage_Moderated(t *testing.T) {
|
|||||||
body, _ := json.Marshal([]string{"test"}) //nolint:errchkjson
|
body, _ := json.Marshal([]string{"test"}) //nolint:errchkjson
|
||||||
|
|
||||||
// Bob (non-op, non-voiced) should fail to send.
|
// Bob (non-op, non-voiced) should fail to send.
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, sid2, "bob",
|
ctx, sid2, 0, "bob",
|
||||||
irc.CmdPrivmsg, "#modchat", body, nil,
|
irc.CmdPrivmsg, "#modchat", body, nil,
|
||||||
)
|
)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -1048,8 +1044,8 @@ func TestSendChannelMessage_Moderated(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Alice (operator) should succeed.
|
// Alice (operator) should succeed.
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, sid1, "alice",
|
ctx, sid1, 0, "alice",
|
||||||
irc.CmdPrivmsg, "#modchat", body, nil,
|
irc.CmdPrivmsg, "#modchat", body, nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1074,7 +1070,7 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
|||||||
refuse func(ctx context.Context, env *testEnv, chID, bobSID int64) error
|
refuse func(ctx context.Context, env *testEnv, chID, bobSID int64) error
|
||||||
}{
|
}{
|
||||||
{"not a member", func(ctx context.Context, env *testEnv, _, bobSID int64) error {
|
{"not a member", func(ctx context.Context, env *testEnv, _, bobSID int64) error {
|
||||||
return env.svc.PartChannel(ctx, bobSID, "bob", channel, "")
|
return env.svc.PartChannel(ctx, bobSID, 0, "bob", channel, "")
|
||||||
}},
|
}},
|
||||||
{"banned", func(ctx context.Context, env *testEnv, chID, _ int64) error {
|
{"banned", func(ctx context.Context, env *testEnv, chID, _ int64) error {
|
||||||
return env.db.AddChannelBan(ctx, chID, "bob!*@*", "alice")
|
return env.db.AddChannelBan(ctx, chID, "bob!*@*", "alice")
|
||||||
@@ -1094,12 +1090,12 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
|||||||
aliceSID := createSession(ctx, t, env.db, "alice")
|
aliceSID := createSession(ctx, t, env.db, "alice")
|
||||||
bobSID := createSession(ctx, t, env.db, "bob")
|
bobSID := createSession(ctx, t, env.db, "bob")
|
||||||
|
|
||||||
joined, err := env.svc.JoinChannel(ctx, aliceSID, "alice", channel, "")
|
joined, err := env.svc.JoinChannel(ctx, aliceSID, 0, "alice", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join alice: %v", err)
|
t.Fatalf("join alice: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = env.svc.JoinChannel(ctx, bobSID, "bob", channel, "")
|
_, err = env.svc.JoinChannel(ctx, bobSID, 0, "bob", channel, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("join bob: %v", err)
|
t.Fatalf("join bob: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1119,8 +1115,8 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
|||||||
)
|
)
|
||||||
meta, _ := json.Marshal(map[string]string{"hashcash": stamp})
|
meta, _ := json.Marshal(map[string]string{"hashcash": stamp})
|
||||||
|
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, bobSID, "bob", irc.CmdPrivmsg, channel, body, meta,
|
ctx, bobSID, 0, "bob", irc.CmdPrivmsg, channel, body, meta,
|
||||||
)
|
)
|
||||||
|
|
||||||
var ircErr *service.IRCError
|
var ircErr *service.IRCError
|
||||||
@@ -1129,8 +1125,8 @@ func TestRefusedChannelMessageLeavesStampUnspent(t *testing.T) {
|
|||||||
t.Fatalf("bob: want ERR_CANNOTSENDTOCHAN, got %v", err)
|
t.Fatalf("bob: want ERR_CANNOTSENDTOCHAN, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err = env.svc.SendChannelMessage(
|
_, err = env.svc.SendChannelMessage(
|
||||||
ctx, aliceSID, "alice", irc.CmdPrivmsg, channel, body, meta,
|
ctx, aliceSID, 0, "alice", irc.CmdPrivmsg, channel, body, meta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("alice with the same stamp: %v", err)
|
t.Fatalf("alice with the same stamp: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user