diff --git a/README.md b/README.md index 5489b9a..0474e54 100644 --- a/README.md +++ b/README.md @@ -1236,7 +1236,7 @@ IRC listener sends the usual 3-digit code. Over the HTTP API, 005 arrives named | `001` | RPL_WELCOME | After session creation or login, on MOTD | `{"command":"RPL_WELCOME","code":1,"to":"alice","body":["Welcome to the network, alice"]}` | | `002` | RPL_YOURHOST | After session creation or login, on MOTD | `{"command":"RPL_YOURHOST","code":2,"to":"alice","body":["Your host is neoirc, running version 0.1"]}` | | `003` | RPL_CREATED | After session creation or login, on MOTD | `{"command":"RPL_CREATED","code":3,"to":"alice","body":["This server was created 2026-02-10"]}` | -| `004` | RPL_MYINFO | After session creation or login, on MOTD | `{"command":"RPL_MYINFO","code":4,"to":"alice","params":["neoirc","0.1","","ikmnostl"]}` | +| `004` | RPL_MYINFO | After session creation or login, on MOTD | `{"command":"RPL_MYINFO","code":4,"to":"alice","params":["neoirc","0.1","ow","ikmnostl"]}` | | `005` | RPL_ISUPPORT | After session creation or login, on MOTD | `{"command":"RPL_BOUNCE","code":5,"to":"alice","params":["CHANTYPES=#","NICKLEN=32","PREFIX=(ov)@+","CHANMODES=b,k,Hl,imnst","NETWORK=neoirc","CASEMAPPING=ascii"],"body":["are supported by this server"]}` | | `221` | RPL_UMODEIS | In response to user MODE | `{"command":"RPL_UMODEIS","code":221,"to":"alice","body":["+"]}` | | `251` | RPL_LUSERCLIENT | On connect, MOTD or LUSERS | `{"command":"RPL_LUSERCLIENT","code":251,"to":"alice","body":["There are 5 users and 0 invisible on 1 servers"]}` | diff --git a/internal/handlers/api.go b/internal/handlers/api.go index 792354d..7c0574c 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -412,7 +412,7 @@ func (hdlr *Handlers) deliverWelcome( // 004 RPL_MYINFO hdlr.enqueueNumeric( ctx, clientID, irc.RplMyInfo, nick, - []string{srvName, version, "", "ikmnostl"}, + []string{srvName, version, service.UserModes, "ikmnostl"}, "", ) diff --git a/internal/handlers/api_test.go b/internal/handlers/api_test.go index a883330..d72d6de 100644 --- a/internal/handlers/api_test.go +++ b/internal/handlers/api_test.go @@ -6797,6 +6797,30 @@ func TestISUPPORT(t *testing.T) { } } +// TestMyInfoListsUserModes verifies the 004 numeric lists +// the user modes o and w as its third parameter. +func TestMyInfoListsUserModes(t *testing.T) { + t.Parallel() + + tserver := newTestServer(t) + token := tserver.createSession("myinfo") + + msgs, _ := tserver.pollMessages(token, 0) + + myInfo := findNumericWithParams(msgs, "004") + if myInfo == nil { + t.Fatal("expected 004 RPL_MYINFO") + } + + params := getNumericParams(myInfo) + if len(params) != 4 || params[2] != "ow" { + t.Fatalf( + "004 params %v, want four with user modes ow third", + params, + ) + } +} + // TestNonOpCannotSetModes verifies non-operators // cannot set +i, +s, +k, +l, +b. func TestNonOpCannotSetModes(t *testing.T) { diff --git a/internal/ircserver/conn.go b/internal/ircserver/conn.go index 3dcca86..83f6cbe 100644 --- a/internal/ircserver/conn.go +++ b/internal/ircserver/conn.go @@ -605,7 +605,7 @@ func (c *Conn) deliverWelcome() { ) c.sendNumeric( irc.RplMyInfo, - c.serverSfx, "neoirc", "", "mnst", + c.serverSfx, "neoirc", service.UserModes, "mnst", ) c.sendNumeric( irc.RplIsupport, diff --git a/internal/ircserver/server_test.go b/internal/ircserver/server_test.go index 1cc4316..229b43c 100644 --- a/internal/ircserver/server_test.go +++ b/internal/ircserver/server_test.go @@ -486,6 +486,22 @@ func TestWelcomeContainsNick(t *testing.T) { } } +// TestMyInfoListsUserModes checks that RPL_MYINFO lists the +// user modes o and w, with no empty parameter before the +// channel modes. +func TestMyInfoListsUserModes(t *testing.T) { + t.Parallel() + + env := newTestEnv(t) + lines := env.dial(t).register("carol") + + assertContains( + t, lines, + ":test.irc 004 carol test.irc neoirc ow mnst", + "RPL_MYINFO with user modes", + ) +} + func TestPingPong(t *testing.T) { t.Parallel() diff --git a/internal/service/service.go b/internal/service/service.go index f16340d..2036ad8 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -1169,6 +1169,12 @@ func (s *Service) ApplyUserMode( return s.QueryUserMode(ctx, sessionID) } +// UserModes lists the user modes the server supports, as +// RPL_MYINFO sends them: o (server operator) and w +// (receives WALLOPS). parseUserModeString must accept the +// same letters. +const UserModes = "ow" + // parseUserModeString parses a user mode string such as // "+w", "-o" or "+w-o" into the new value of each flag it // names, or nil for a flag it does not name; a later letter diff --git a/schema/README.md b/schema/README.md index 6d66f1c..fce0bd8 100644 --- a/schema/README.md +++ b/schema/README.md @@ -144,8 +144,8 @@ Where a schema file and the HTTP API disagree, the HTTP API is right: with `ERR_NEEDMOREPARAMS` (`461`). A `body` of `[""]` clears the topic. - `003.json` describes the server's creation date. The HTTP API sends the date the server last started. -- `004.json` has example mode lists. The HTTP API sends an empty string as the - user modes and `ikmnostl` as the channel modes. +- `004.json` has example mode lists. The HTTP API sends `ow` as the user modes + and `ikmnostl` as the channel modes. - `322.json` says `LIST` gets one reply per channel. The server leaves out a secret channel, one with mode `+s`, unless the user is on it. - `332.json` says `RPL_TOPIC` answers a `TOPIC` query. The HTTP API has no such