feat: implement IRC numerics batch 2 — connection registration, channel ops, user queries
All checks were successful
check / check (push) Successful in 2m50s
All checks were successful
check / check (push) Successful in 2m50s
Add comprehensive IRC numeric reply support: Connection registration (001-005): - 002 RPL_YOURHOST, 003 RPL_CREATED, 004 RPL_MYINFO, 005 RPL_ISUPPORT - All sent automatically during session creation after RPL_WELCOME Server statistics (251-255): - RPL_LUSERCLIENT, RPL_LUSEROP, RPL_LUSERCHANNELS, RPL_LUSERME - Sent during connection registration and via LUSERS command Channel operations: - MODE command: query channel modes (324 RPL_CHANNELMODEIS, 329 RPL_CREATIONTIME) - MODE command: query user modes (221 RPL_UMODEIS) - NAMES command: query channel member list (353/366) - LIST command: list all channels (322 RPL_LIST, 323 end of list) User queries: - WHOIS command: 311/312/318/319 numerics - WHO command: 352 RPL_WHOREPLY, 315 RPL_ENDOFWHO Database additions: - GetChannelCount, ListAllChannelsWithCounts - GetChannelCreatedAt, GetSessionCreatedAt Also adds StartTime to Globals for RPL_CREATED and updates README with comprehensive documentation of all new commands and numerics. closes #52
This commit is contained in:
@@ -219,19 +219,130 @@ func (hdlr *Handlers) handleCreateSessionError(
|
||||
)
|
||||
}
|
||||
|
||||
// deliverWelcome sends the RPL_WELCOME (001) numeric to a
|
||||
// new client.
|
||||
// deliverWelcome sends connection registration numerics
|
||||
// (001-005) to a new client.
|
||||
func (hdlr *Handlers) deliverWelcome(
|
||||
request *http.Request,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
version := hdlr.serverVersion()
|
||||
|
||||
// 001 RPL_WELCOME
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "001", nick, nil,
|
||||
"Welcome to the network, "+nick,
|
||||
)
|
||||
|
||||
// 002 RPL_YOURHOST
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "002", nick, nil,
|
||||
"Your host is "+srvName+
|
||||
", running version "+version,
|
||||
)
|
||||
|
||||
// 003 RPL_CREATED
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "003", nick, nil,
|
||||
"This server was created "+
|
||||
hdlr.params.Globals.StartTime.
|
||||
Format("2006-01-02"),
|
||||
)
|
||||
|
||||
// 004 RPL_MYINFO
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "004", nick,
|
||||
[]string{srvName, version, "", "imnst"},
|
||||
"",
|
||||
)
|
||||
|
||||
// 005 RPL_ISUPPORT
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "005", nick,
|
||||
[]string{
|
||||
"CHANTYPES=#",
|
||||
"NICKLEN=32",
|
||||
"CHANMODES=,,," + "imnst",
|
||||
"NETWORK=neoirc",
|
||||
"CASEMAPPING=ascii",
|
||||
},
|
||||
"are supported by this server",
|
||||
)
|
||||
|
||||
// LUSERS
|
||||
hdlr.deliverLusers(ctx, clientID, nick)
|
||||
}
|
||||
|
||||
// deliverLusers sends RPL_LUSERCLIENT (251),
|
||||
// RPL_LUSEROP (252), RPL_LUSERCHANNELS (254), and
|
||||
// RPL_LUSERME (255) to the client.
|
||||
func (hdlr *Handlers) deliverLusers(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
userCount, err := hdlr.params.Database.GetUserCount(ctx)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"lusers user count", "error", err,
|
||||
)
|
||||
|
||||
userCount = 0
|
||||
}
|
||||
|
||||
chanCount, err := hdlr.params.Database.GetChannelCount(
|
||||
ctx,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"lusers channel count", "error", err,
|
||||
)
|
||||
|
||||
chanCount = 0
|
||||
}
|
||||
|
||||
// 251 RPL_LUSERCLIENT
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "251", nick, nil,
|
||||
fmt.Sprintf(
|
||||
"There are %d users and 0 invisible on 1 servers",
|
||||
userCount,
|
||||
),
|
||||
)
|
||||
|
||||
// 252 RPL_LUSEROP
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "252", nick,
|
||||
[]string{"0"},
|
||||
"operator(s) online",
|
||||
)
|
||||
|
||||
// 254 RPL_LUSERCHANNELS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "254", nick,
|
||||
[]string{strconv.FormatInt(chanCount, 10)},
|
||||
"channels formed",
|
||||
)
|
||||
|
||||
// 255 RPL_LUSERME
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "255", nick, nil,
|
||||
fmt.Sprintf(
|
||||
"I have %d clients and 1 servers",
|
||||
userCount,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) serverVersion() string {
|
||||
ver := hdlr.params.Globals.Version
|
||||
if ver == "" {
|
||||
return "dev"
|
||||
}
|
||||
|
||||
return ver
|
||||
}
|
||||
|
||||
// deliverMOTD sends the MOTD as IRC numeric messages to a
|
||||
@@ -677,6 +788,55 @@ func (hdlr *Handlers) dispatchCommand(
|
||||
"from": hdlr.serverName(),
|
||||
},
|
||||
http.StatusOK)
|
||||
default:
|
||||
hdlr.dispatchQueryCommand(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
command, target, bodyLines,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) dispatchQueryCommand(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, command, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
switch command {
|
||||
case "MODE":
|
||||
hdlr.handleMode(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, bodyLines,
|
||||
)
|
||||
case "NAMES":
|
||||
hdlr.handleNames(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case "LIST":
|
||||
hdlr.handleList(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
)
|
||||
case "WHOIS":
|
||||
hdlr.handleWhois(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
target, bodyLines,
|
||||
)
|
||||
case "WHO":
|
||||
hdlr.handleWho(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, target,
|
||||
)
|
||||
case "LUSERS":
|
||||
hdlr.handleLusers(
|
||||
writer, request,
|
||||
sessionID, clientID, nick,
|
||||
)
|
||||
default:
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
@@ -1450,6 +1610,424 @@ func (hdlr *Handlers) handleQuit(
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleMode handles the MODE command for channels and
|
||||
// users. Currently supports query-only (no mode changes).
|
||||
func (hdlr *Handlers) handleMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"MODE"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
// User mode query — return empty modes.
|
||||
hdlr.enqueueNumeric(
|
||||
request.Context(), clientID,
|
||||
"221", nick, nil, "+",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = bodyLines
|
||||
|
||||
hdlr.handleChannelMode(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, channel,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) handleChannelMode(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, channel string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 324 RPL_CHANNELMODEIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "324", nick,
|
||||
[]string{channel, "+n"}, "",
|
||||
)
|
||||
|
||||
// 329 RPL_CREATIONTIME
|
||||
createdAt, timeErr := hdlr.params.Database.
|
||||
GetChannelCreatedAt(ctx, chID)
|
||||
if timeErr == nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "329", nick,
|
||||
[]string{
|
||||
channel,
|
||||
strconv.FormatInt(
|
||||
createdAt.Unix(), 10,
|
||||
),
|
||||
},
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleNames sends NAMES reply for a channel.
|
||||
func (hdlr *Handlers) handleNames(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"NAMES"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
ctx := request.Context()
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"403", nick, []string{channel},
|
||||
"No such channel",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleList sends the LIST response with 322/323
|
||||
// numerics.
|
||||
func (hdlr *Handlers) handleList(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
|
||||
channels, err := hdlr.params.Database.
|
||||
ListAllChannelsWithCounts(ctx)
|
||||
if err != nil {
|
||||
hdlr.log.Error(
|
||||
"list channels failed", "error", err,
|
||||
)
|
||||
hdlr.respondError(
|
||||
writer, request,
|
||||
"internal error",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
// 322 RPL_LIST
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "322", nick,
|
||||
[]string{
|
||||
chanInfo.Name,
|
||||
strconv.FormatInt(
|
||||
chanInfo.MemberCount, 10,
|
||||
),
|
||||
},
|
||||
chanInfo.Topic,
|
||||
)
|
||||
}
|
||||
|
||||
// 323 — end of channel list.
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "323", nick, nil,
|
||||
"End of /LIST",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleWhois handles the WHOIS command.
|
||||
func (hdlr *Handlers) handleWhois(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
bodyLines func() []string,
|
||||
) {
|
||||
queryNick := target
|
||||
|
||||
// If target is empty, check body for the nick.
|
||||
if queryNick == "" {
|
||||
lines := bodyLines()
|
||||
if len(lines) > 0 {
|
||||
queryNick = strings.TrimSpace(lines[0])
|
||||
}
|
||||
}
|
||||
|
||||
if queryNick == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"WHOIS"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hdlr.executeWhois(
|
||||
writer, request,
|
||||
sessionID, clientID, nick, queryNick,
|
||||
)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) executeWhois(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, queryNick string,
|
||||
) {
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
targetSID, err := hdlr.params.Database.GetSessionByNick(
|
||||
ctx, queryNick,
|
||||
)
|
||||
if err != nil {
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "401", nick,
|
||||
[]string{queryNick},
|
||||
"No such nick/channel",
|
||||
)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 311 RPL_WHOISUSER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "311", nick,
|
||||
[]string{queryNick, queryNick, srvName, "*"},
|
||||
queryNick,
|
||||
)
|
||||
|
||||
// 312 RPL_WHOISSERVER
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "312", nick,
|
||||
[]string{queryNick, srvName},
|
||||
"neoirc server",
|
||||
)
|
||||
|
||||
// 319 RPL_WHOISCHANNELS
|
||||
hdlr.deliverWhoisChannels(
|
||||
ctx, clientID, nick, queryNick, targetSID,
|
||||
)
|
||||
|
||||
// 318 RPL_ENDOFWHOIS
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "318", nick,
|
||||
[]string{queryNick},
|
||||
"End of /WHOIS list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
func (hdlr *Handlers) deliverWhoisChannels(
|
||||
ctx context.Context,
|
||||
clientID int64,
|
||||
nick, queryNick string,
|
||||
targetSID int64,
|
||||
) {
|
||||
channels, chanErr := hdlr.params.Database.
|
||||
GetSessionChannels(ctx, targetSID)
|
||||
if chanErr != nil || len(channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
chanNames := make([]string, 0, len(channels))
|
||||
|
||||
for _, chanInfo := range channels {
|
||||
chanNames = append(chanNames, chanInfo.Name)
|
||||
}
|
||||
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "319", nick,
|
||||
[]string{queryNick},
|
||||
strings.Join(chanNames, " "),
|
||||
)
|
||||
}
|
||||
|
||||
// handleWho handles the WHO command.
|
||||
func (hdlr *Handlers) handleWho(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick, target string,
|
||||
) {
|
||||
if target == "" {
|
||||
hdlr.respondIRCError(
|
||||
writer, request, clientID, sessionID,
|
||||
"461", nick, []string{"WHO"},
|
||||
"Not enough parameters",
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := request.Context()
|
||||
srvName := hdlr.serverName()
|
||||
|
||||
channel := target
|
||||
if !strings.HasPrefix(channel, "#") {
|
||||
channel = "#" + channel
|
||||
}
|
||||
|
||||
chID, err := hdlr.params.Database.GetChannelByName(
|
||||
ctx, channel,
|
||||
)
|
||||
if err != nil {
|
||||
// 315 RPL_ENDOFWHO (empty result)
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{target},
|
||||
"End of /WHO list",
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
members, memErr := hdlr.params.Database.ChannelMembers(
|
||||
ctx, chID,
|
||||
)
|
||||
if memErr == nil {
|
||||
for _, mem := range members {
|
||||
// 352 RPL_WHOREPLY
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "352", nick,
|
||||
[]string{
|
||||
channel, mem.Nick, srvName,
|
||||
srvName, mem.Nick, "H",
|
||||
},
|
||||
"0 "+mem.Nick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 315 RPL_ENDOFWHO
|
||||
hdlr.enqueueNumeric(
|
||||
ctx, clientID, "315", nick,
|
||||
[]string{channel},
|
||||
"End of /WHO list",
|
||||
)
|
||||
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// handleLusers handles the LUSERS command.
|
||||
func (hdlr *Handlers) handleLusers(
|
||||
writer http.ResponseWriter,
|
||||
request *http.Request,
|
||||
sessionID, clientID int64,
|
||||
nick string,
|
||||
) {
|
||||
hdlr.deliverLusers(
|
||||
request.Context(), clientID, nick,
|
||||
)
|
||||
hdlr.broker.Notify(sessionID)
|
||||
hdlr.respondJSON(writer, request,
|
||||
map[string]string{"status": "ok"},
|
||||
http.StatusOK)
|
||||
}
|
||||
|
||||
// HandleGetHistory returns message history for a target.
|
||||
func (hdlr *Handlers) HandleGetHistory() http.HandlerFunc {
|
||||
return func(
|
||||
|
||||
@@ -115,8 +115,9 @@ func newTestServer(
|
||||
|
||||
func newTestGlobals() *globals.Globals {
|
||||
return &globals.Globals{
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
Appname: "neoirc-test",
|
||||
Version: "test",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user