feat: add /list, /who, /whois commands + CLI parity
All checks were successful
check / check (push) Successful in 2m18s

- Server: add LIST, WHO, WHOIS command handlers in dispatchCommand
  with proper IRC numerics (322/323, 352/315, 311/312/319/318)
- SPA: add /list, /who, /whois command parsing and numeric display
- CLI: add /motd, /who, /whois commands for feature parity
- Rebuild SPA dist from source
This commit is contained in:
user
2026-03-09 14:56:11 -07:00
parent a55127ff7c
commit a48c7f562c
5 changed files with 536 additions and 24 deletions

View File

@@ -670,20 +670,12 @@ func (hdlr *Handlers) dispatchCommand(
hdlr.handleQuit(
writer, request, sessionID, nick, body,
)
case "MOTD":
hdlr.deliverMOTD(
request, clientID, sessionID, nick,
case "MOTD", "LIST", "WHO", "WHOIS", "PING":
hdlr.dispatchInfoCommand(
writer, request,
sessionID, clientID, nick,
command, target, bodyLines,
)
hdlr.respondJSON(writer, request,
map[string]string{"status": "ok"},
http.StatusOK)
case "PING":
hdlr.respondJSON(writer, request,
map[string]string{
"command": "PONG",
"from": hdlr.serverName(),
},
http.StatusOK)
default:
hdlr.enqueueNumeric(
request.Context(), clientID,
@@ -1398,6 +1390,269 @@ func (hdlr *Handlers) executeTopic(
http.StatusOK)
}
// dispatchInfoCommand handles informational IRC commands
// that produce server-side numerics (MOTD, LIST, WHO,
// WHOIS, PING).
func (hdlr *Handlers) dispatchInfoCommand(
writer http.ResponseWriter,
request *http.Request,
sessionID, clientID int64,
nick, command, target string,
bodyLines func() []string,
) {
okResp := map[string]string{"status": "ok"}
switch command {
case "MOTD":
hdlr.deliverMOTD(
request, clientID, sessionID, nick,
)
case "LIST":
hdlr.handleListCmd(
request, clientID, sessionID, nick,
)
case "WHO":
hdlr.handleWhoCmd(
request, clientID, sessionID, nick,
target,
)
case "WHOIS":
hdlr.handleWhoisCmd(
request, clientID, sessionID, nick,
target, bodyLines,
)
case "PING":
hdlr.respondJSON(writer, request,
map[string]string{
"command": "PONG",
"from": hdlr.serverName(),
},
http.StatusOK)
return
}
hdlr.respondJSON(
writer, request, okResp, http.StatusOK,
)
}
// handleListCmd sends RPL_LIST (322) for each channel,
// then sends 323 to signal the end of the list.
func (hdlr *Handlers) handleListCmd(
request *http.Request,
clientID, sessionID int64,
nick string,
) {
ctx := request.Context()
channels, err := hdlr.params.Database.ListAllChannels(
ctx,
)
if err != nil {
hdlr.enqueueNumeric(
ctx, clientID, "323", nick, nil,
"End of /LIST",
)
hdlr.broker.Notify(sessionID)
return
}
for _, channel := range channels {
memberIDs, _ :=
hdlr.params.Database.GetChannelMemberIDs(
ctx, channel.ID,
)
count := strconv.Itoa(len(memberIDs))
topic := channel.Topic
if topic == "" {
topic = " "
}
hdlr.enqueueNumeric(
ctx, clientID, "322", nick,
[]string{channel.Name, count}, topic,
)
}
hdlr.enqueueNumeric(
ctx, clientID, "323", nick, nil,
"End of /LIST",
)
hdlr.broker.Notify(sessionID)
}
// handleWhoCmd sends RPL_WHOREPLY (352) for each member
// of the target channel, followed by RPL_ENDOFWHO (315).
func (hdlr *Handlers) handleWhoCmd(
request *http.Request,
clientID, sessionID int64,
nick, target string,
) {
ctx := request.Context()
if target == "" {
hdlr.enqueueNumeric(
ctx, clientID, "461", nick,
[]string{"WHO"}, "Not enough parameters",
)
hdlr.broker.Notify(sessionID)
return
}
channel := target
if !strings.HasPrefix(channel, "#") {
channel = "#" + channel
}
chID, err := hdlr.params.Database.GetChannelByName(
ctx, channel,
)
if err != nil {
hdlr.enqueueNumeric(
ctx, clientID, "315", nick,
[]string{channel}, "End of /WHO list",
)
hdlr.broker.Notify(sessionID)
return
}
members, err := hdlr.params.Database.ChannelMembers(
ctx, chID,
)
if err == nil {
srvName := hdlr.serverName()
for _, mem := range members {
hdlr.enqueueNumeric(
ctx, clientID, "352", nick,
[]string{
channel, mem.Nick, "neoirc",
srvName, mem.Nick, "H",
},
"0 "+mem.Nick,
)
}
}
hdlr.enqueueNumeric(
ctx, clientID, "315", nick,
[]string{channel}, "End of /WHO list",
)
hdlr.broker.Notify(sessionID)
}
// handleWhoisCmd sends WHOIS reply numerics (311, 312,
// 319, 318) for the target nick.
func (hdlr *Handlers) handleWhoisCmd(
request *http.Request,
clientID, sessionID int64,
nick, target string,
bodyLines func() []string,
) {
ctx := request.Context()
whoisNick := target
if whoisNick == "" {
lines := bodyLines()
if len(lines) > 0 {
whoisNick = strings.TrimSpace(lines[0])
}
}
if whoisNick == "" {
hdlr.enqueueNumeric(
ctx, clientID, "461", nick,
[]string{"WHOIS"}, "Not enough parameters",
)
hdlr.broker.Notify(sessionID)
return
}
targetSID, err :=
hdlr.params.Database.GetSessionByNick(
ctx, whoisNick,
)
if err != nil {
hdlr.enqueueNumeric(
ctx, clientID, "401", nick,
[]string{whoisNick},
"No such nick/channel",
)
hdlr.enqueueNumeric(
ctx, clientID, "318", nick,
[]string{whoisNick},
"End of /WHOIS list",
)
hdlr.broker.Notify(sessionID)
return
}
hdlr.sendWhoisNumerics(
ctx, clientID, sessionID, nick,
whoisNick, targetSID,
)
}
// sendWhoisNumerics emits 311/312/319/318 for a
// resolved WHOIS target.
func (hdlr *Handlers) sendWhoisNumerics(
ctx context.Context,
clientID, sessionID int64,
nick, whoisNick string,
targetSID int64,
) {
srvName := hdlr.serverName()
// 311 RPL_WHOISUSER
hdlr.enqueueNumeric(
ctx, clientID, "311", nick,
[]string{whoisNick, whoisNick, "neoirc", "*"},
whoisNick,
)
// 312 RPL_WHOISSERVER
hdlr.enqueueNumeric(
ctx, clientID, "312", nick,
[]string{whoisNick, srvName},
srvName,
)
// 319 RPL_WHOISCHANNELS
channels, _ := hdlr.params.Database.GetSessionChannels(
ctx, targetSID,
)
if len(channels) > 0 {
names := make([]string, 0, len(channels))
for _, chanInfo := range channels {
names = append(names, chanInfo.Name)
}
hdlr.enqueueNumeric(
ctx, clientID, "319", nick,
[]string{whoisNick},
strings.Join(names, " "),
)
}
// 318 RPL_ENDOFWHOIS
hdlr.enqueueNumeric(
ctx, clientID, "318", nick,
[]string{whoisNick},
"End of /WHOIS list",
)
hdlr.broker.Notify(sessionID)
}
func (hdlr *Handlers) handleQuit(
writer http.ResponseWriter,
request *http.Request,