diff --git a/gencapdefs.py b/gencapdefs.py index e1cfbc7f..a64ef143 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -219,6 +219,12 @@ CAPDEFS = [ url="https://github.com/ircv3/ircv3-specifications/pull/527", standard="proposed IRCv3", ), + CapDef( + identifier="ExtendedISupport", + name="draft/extended-isupport", + url="https://github.com/ircv3/ircv3-specifications/pull/543", + standard="proposed IRCv3", + ), ] def validate_defs(): diff --git a/irc/caps/constants.go b/irc/caps/constants.go index 57f088e3..38497a2b 100644 --- a/irc/caps/constants.go +++ b/irc/caps/constants.go @@ -64,10 +64,11 @@ const ( BotTagName = "bot" // https://ircv3.net/specs/extensions/chathistory ChathistoryTargetsBatchType = "draft/chathistory-targets" + ExtendedISupportBatchType = "draft/extended-isupport" ) func init() { - nameToCapability = make(map[string]Capability) + nameToCapability = make(map[string]Capability, numCapabs) for capab, name := range capabilityNames { nameToCapability[name] = Capability(capab) } diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 18ff5759..9847ae4a 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 34 + numCapabs = 35 // length of the uint32 array that represents the bitset: bitsetLen = 2 ) @@ -53,6 +53,10 @@ const ( // https://github.com/ircv3/ircv3-specifications/pull/362 EventPlayback Capability = iota + // ExtendedISupport is the proposed IRCv3 capability named "draft/extended-isupport": + // https://github.com/ircv3/ircv3-specifications/pull/543 + ExtendedISupport Capability = iota + // Languages is the proposed IRCv3 capability named "draft/languages": // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 Languages Capability = iota @@ -163,6 +167,7 @@ var ( "draft/channel-rename", "draft/chathistory", "draft/event-playback", + "draft/extended-isupport", "draft/languages", "draft/message-redaction", "draft/multiline", diff --git a/irc/client.go b/irc/client.go index 218f4d3d..bb5badde 100644 --- a/irc/client.go +++ b/irc/client.go @@ -179,6 +179,8 @@ type Session struct { batchCounter atomic.Uint32 + isupportSentPrereg bool + quitMessage string awayMessage string diff --git a/irc/commands.go b/irc/commands.go index ccbec383..4bd88dd7 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -152,6 +152,10 @@ func init() { handler: isonHandler, minParams: 1, }, + "ISUPPORT": { + handler: isupportHandler, + usablePreReg: true, + }, "JOIN": { handler: joinHandler, minParams: 1, diff --git a/irc/handlers.go b/irc/handlers.go index ecd23dbc..17fd905f 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1320,6 +1320,15 @@ func isonHandler(server *Server, client *Client, msg ircmsg.Message, rb *Respons return false } +// ISUPPORT +func isupportHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { + server.RplISupport(client, rb) + if !client.registered { + rb.session.isupportSentPrereg = true + } + return false +} + // JOIN {,} [{,}] func joinHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { // #1417: allow `JOIN 0` with a confirmation code diff --git a/irc/help.go b/irc/help.go index 343bd717..a11bb725 100644 --- a/irc/help.go +++ b/irc/help.go @@ -259,6 +259,11 @@ appropriate channel privs.`, text: `ISON { } Returns whether the given nicks exist on the network.`, + }, + "isupport": { + text: `ISUPPORT + +Returns RPL_ISUPPORT lines describing the server's capabilities.`, }, "join": { text: `JOIN {,} [{,}] diff --git a/irc/server.go b/irc/server.go index eef47c2c..906633d2 100644 --- a/irc/server.go +++ b/irc/server.go @@ -434,7 +434,9 @@ func (server *Server) playRegistrationBurst(session *Session) { session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, rplMyInfo1, rplMyInfo2, rplMyInfo3) rb := NewResponseBuffer(session) - server.RplISupport(c, rb) + if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) { + server.RplISupport(c, rb) + } if d.account != "" && session.capabilities.Has(caps.Persistence) { reportPersistenceStatus(c, rb, false) } @@ -456,10 +458,17 @@ func (server *Server) playRegistrationBurst(session *Session) { // RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses. func (server *Server) RplISupport(client *Client, rb *ResponseBuffer) { + server.sendRplISupportLines(client, rb, server.Config().Server.isupport.CachedReply) +} + +func (server *Server) sendRplISupportLines(client *Client, rb *ResponseBuffer, lines [][]string) { + if rb.session.capabilities.Has(caps.ExtendedISupport) { + batchID := rb.StartNestedBatch("chathistory", caps.ExtendedISupportBatchType) + defer rb.EndNestedBatch(batchID) + } translatedISupport := client.t("are supported by this server") nick := client.Nick() - config := server.Config() - for _, cachedTokenLine := range config.Server.isupport.CachedReply { + for _, cachedTokenLine := range lines { length := len(cachedTokenLine) + 2 tokenline := make([]string, length) tokenline[0] = nick @@ -794,13 +803,19 @@ func (server *Server) applyConfig(config *Config) (err error) { } if !initial { - // push new info to all of our clients - for _, sClient := range server.clients.AllClients() { - for _, tokenline := range newISupportReplies { - sClient.Send(nil, server.name, RPL_ISUPPORT, append([]string{sClient.nick}, tokenline...)...) + // send 005 updates (somewhat rare) + if len(newISupportReplies) != 0 { + for _, sClient := range server.clients.AllClients() { + for _, session := range sClient.Sessions() { + rb := NewResponseBuffer(session) + server.sendRplISupportLines(sClient, rb, newISupportReplies) + rb.Send(false) + } } + } - if sendRawOutputNotice { + if sendRawOutputNotice { + for _, sClient := range server.clients.AllClients() { sClient.Notice(sClient.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) } }