update to draft/metadata-3 (#2416)

temporarily update irctest to metadata-3 branch
This commit is contained in:
Shivaram Lingamneni
2026-07-07 00:28:04 -07:00
committed by GitHub
parent c6b2f1285c
commit 26cf806545
11 changed files with 179 additions and 45 deletions
+7 -3
View File
@@ -238,7 +238,7 @@ CAPDEFS = [
standard="Soju/Goguma vendor",
),
CapDef(
identifier="Metadata",
identifier="MetadataDraft2",
name="draft/metadata-2",
url="https://ircv3.net/specs/extensions/metadata",
standard="draft IRCv3",
@@ -249,8 +249,12 @@ CAPDEFS = [
url="https://github.com/ircv3/ircv3-specifications/pull/602",
standard="proposed IRCv3",
),
CapDef(
identifier="Metadata",
name="draft/metadata-3",
url="https://github.com/ircv3/ircv3-specifications/pull/613",
standard="draft IRCv3",
),
]
def validate_defs():
+2
View File
@@ -70,6 +70,8 @@ const (
// authtoken draft: https://github.com/ircv3/ircv3-specifications/pull/602
AuthTokenBatchType = "draft/authtoken"
AuthToken005 = "draft/AUTHTOKEN"
MetadataSyncAllTarget = "*ALL"
)
func init() {
+7 -2
View File
@@ -7,7 +7,7 @@ package caps
const (
// number of recognized capabilities:
numCapabs = 39
numCapabs = 40
// length of the uint32 array that represents the bitset:
bitsetLen = 2
)
@@ -69,8 +69,12 @@ const (
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
MessageRedaction Capability = iota
// Metadata is the draft IRCv3 capability named "draft/metadata-2":
// MetadataDraft2 is the draft IRCv3 capability named "draft/metadata-2":
// https://ircv3.net/specs/extensions/metadata
MetadataDraft2 Capability = iota
// Metadata is the draft IRCv3 capability named "draft/metadata-3":
// https://github.com/ircv3/ircv3-specifications/pull/613
Metadata Capability = iota
// Multiline is the proposed IRCv3 capability named "draft/multiline":
@@ -188,6 +192,7 @@ var (
"draft/languages",
"draft/message-redaction",
"draft/metadata-2",
"draft/metadata-3",
"draft/multiline",
"draft/persistence",
"draft/pre-away",
+5
View File
@@ -822,6 +822,11 @@ func (client *Client) performReattach(session *Session) {
rb.Send(true)
}
session.autoreplayMissedSince = time.Time{}
if session.capabilities.Has(caps.Metadata) {
rb := NewResponseBuffer(session)
syncAllMetadata(client.server, rb)
rb.Send(true)
}
}
func (client *Client) applyPreregMetadata(session *Session) {
+2
View File
@@ -1778,6 +1778,7 @@ func LoadConfig(filename string) (config *Config, err error) {
if !config.Metadata.Enabled {
config.Server.supportedCaps.Disable(caps.Metadata)
config.Server.supportedCaps.Disable(caps.MetadataDraft2)
} else {
metadataValues := make([]string, 0, 4)
metadataValues = append(metadataValues, "before-connect")
@@ -1795,6 +1796,7 @@ func LoadConfig(filename string) (config *Config, err error) {
metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes))
}
config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",")
config.Server.capValues[caps.MetadataDraft2] = strings.Join(metadataValues, ",")
}
err = config.processExtjwt()
+32
View File
@@ -923,6 +923,22 @@ func (channel *Channel) ListMetadata() map[string]string {
return maps.Clone(channel.metadata)
}
func (channel *Channel) ListSubscribedMetadata(subscriptions utils.HashSet[string]) (result map[string]string) {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
for k, v := range channel.metadata {
if subscriptions.Has(k) {
if result == nil {
result = make(map[string]string)
}
result[k] = v
}
}
return result
}
func (channel *Channel) DeleteMetadata(key string) (updated bool) {
defer channel.MarkDirty(IncludeAllAttrs)
@@ -1032,6 +1048,22 @@ func (client *Client) ListMetadata() map[string]string {
return maps.Clone(client.metadata)
}
func (client *Client) ListSubscribedMetadata(subscriptions utils.HashSet[string]) (result map[string]string) {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
for k, v := range client.metadata {
if subscriptions.Has(k) {
if result == nil {
result = make(map[string]string)
}
result[k] = v
}
}
return result
}
func (client *Client) DeleteMetadata(key string) (updated bool) {
defer func() {
if updated {
+41 -29
View File
@@ -765,6 +765,12 @@ func capHandler(server *Server, client *Client, msg ircmsg.Message, rb *Response
rb.session.capabilities.Subtract(toRemove)
rb.Add(nil, server.name, "CAP", details.nick, "ACK", capString)
// XXX compatibility hack, if draft/metadata-2 was requested, silently enable draft/metadata-3
// this way we only have to check for one cap at runtime
if rb.session.capabilities.Has(caps.MetadataDraft2) {
rb.session.capabilities.Add(caps.Metadata)
}
case "END":
if !client.registered {
rb.session.capState = caps.NegotiatedState
@@ -3275,7 +3281,7 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
return metadataUnregisteredHandler(client, config, subcommand, msg.Params, rb)
}
default:
rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand"))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_PARAMS", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand"))
return
}
}
@@ -3296,7 +3302,10 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
var targetObj MetadataHaver
var targetClient *Client
var targetChannel *Channel
if strings.HasPrefix(target, "#") {
var syncAll bool
if subcommand == "sync" && strings.EqualFold(target, caps.MetadataSyncAllTarget) {
syncAll = true
} else if strings.HasPrefix(target, "#") {
targetChannel = server.channels.Get(target)
if targetChannel != nil {
targetObj = targetChannel
@@ -3309,7 +3318,7 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
target = targetClient.Nick() // canonicalize case
}
}
if targetObj == nil {
if !syncAll && targetObj == nil {
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", target, client.t("Invalid metadata target"))
return
}
@@ -3318,7 +3327,7 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
case "set":
key := params[2]
if metadataKeyIsEvil(key) {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_KEY", utils.SafeErrorParam(key), client.t("Invalid key name"))
return
}
@@ -3339,19 +3348,19 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
}
}
if len(params) > 3 {
if len(params) > 3 && params[3] != "" {
value := params[3]
config := client.server.Config()
if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" {
rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_VALUE", utils.SafeErrorParam(key), client.t(failMsg))
return
}
updated, err := targetObj.SetMetadata(key, value, config.Metadata.MaxKeys)
if err != nil {
// errLimitExceeded is the only possible error
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys"))
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", target, strconv.Itoa(config.Metadata.MaxKeys), client.t("Too many metadata keys"))
return
}
// echo the value to the client whether or not there was a real update
@@ -3360,11 +3369,10 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
notifySubscribers(server, rb.session, targetObj, target, key, value, true)
}
} else {
if updated := targetObj.DeleteMetadata(key); updated {
updated := targetObj.DeleteMetadata(key)
rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted"))
if updated {
notifySubscribers(server, rb.session, targetObj, target, key, "", false)
rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted"))
} else {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set"))
}
}
@@ -3389,7 +3397,7 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
for _, key := range params[2:] {
if metadataKeyIsEvil(key) {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_KEY", utils.SafeErrorParam(key), client.t("Invalid key name"))
continue
}
@@ -3412,6 +3420,10 @@ func metadataRegisteredHandler(client *Client, config *Config, subcommand string
playMetadataList(rb, client.Nick(), target, targetObj.ListMetadata())
case "sync":
if syncAll {
syncAllMetadata(server, rb)
return
}
if !metadataCanISeeThisTarget(client, targetObj) {
noKeyPerms("*")
return
@@ -3446,32 +3458,27 @@ func metadataUnregisteredHandler(client *Client, config *Config, subcommand stri
}
key := params[2]
if metadataKeyIsEvil(key) {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_KEY", utils.SafeErrorParam(key), client.t("Invalid key name"))
return
}
if len(params) >= 4 {
if len(params) > 3 && params[3] != "" {
value := params[3]
// enforce a sane limit on prereg keys. we don't need to enforce the exact limit,
// that will be done when applying the buffer after registration
if len(rb.session.metadataPreregVals) > config.Metadata.MaxKeys {
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.t("Too many metadata keys"))
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", "*", strconv.Itoa(config.Metadata.MaxKeys), client.t("Too many metadata keys"))
return
}
if failMsg := metadataValueIsEvil(config, key, value); failMsg != "" {
rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t(failMsg))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_VALUE", utils.SafeErrorParam(key), client.t(failMsg))
return
}
rb.session.metadataPreregVals[key] = value
rb.Add(nil, server.name, RPL_KEYVALUE, "*", "*", key, "*", value)
} else {
// unset
_, present := rb.session.metadataPreregVals[key]
if present {
delete(rb.session.metadataPreregVals, key)
rb.Add(nil, server.name, RPL_KEYNOTSET, "*", "*", key, client.t("Key deleted"))
} else {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NOT_SET", utils.SafeErrorParam(key), client.t("Metadata key not set"))
}
delete(rb.session.metadataPreregVals, key)
rb.Add(nil, server.name, RPL_KEYNOTSET, "*", "*", key, client.t("Key deleted"))
}
case "list":
playMetadataList(rb, "*", "*", rb.session.metadataPreregVals)
@@ -3480,7 +3487,7 @@ func metadataUnregisteredHandler(client *Client, config *Config, subcommand stri
rb.session.metadataPreregVals = nil
playMetadataList(rb, "*", "*", oldMetadata)
case "sync":
rb.Add(nil, server.name, RPL_METADATASYNCLATER, "*", utils.SafeErrorParam(params[1]), "60") // lol
rb.Add(nil, server.name, RPL_METADATASYNCLATER, "*", utils.SafeErrorParam(params[1]), "60", "Try again later") // lol
}
return false
}
@@ -3494,24 +3501,29 @@ func metadataSubsHandler(client *Client, subcommand string, params []string, rb
keys := params[2:]
for _, key := range keys {
if metadataKeyIsEvil(key) {
rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name"))
rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_KEY", utils.SafeErrorParam(key), client.t("Invalid key name"))
return
}
}
added, err := rb.session.SubscribeTo(keys...)
if err == errMetadataTooManySubs {
bad := keys[len(added)] // get the key that broke the camel's back
rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", utils.SafeErrorParam(bad), client.t("Too many subscriptions"))
limit := strconv.Itoa(server.Config().Metadata.MaxSubs)
rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", utils.SafeErrorParam(bad), limit, client.t("Too many subscriptions"))
}
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10
nick := client.Nick()
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(nick) - 10
chunked := utils.ChunkifyParams(slices.Values(added), lineLength)
for _, line := range chunked {
params := append([]string{client.Nick()}, line...)
params := append([]string{nick}, line...)
rb.Add(nil, server.name, RPL_METADATASUBOK, params...)
}
if client.registered && len(added) != 0 {
rb.Add(nil, server.name, RPL_METADATASYNCLATER, "*", caps.MetadataSyncAllTarget, "0", client.t("Try again later"))
}
case "unsub":
keys := params[2:]
removed := rb.session.UnsubscribeFrom(keys...)
+63 -6
View File
@@ -59,9 +59,9 @@ func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], origin
}
if set {
s.Send(nil, server.name, "METADATA", target, key, "*", value)
s.Send(nil, server.name, RPL_KEYVALUE, "*", target, key, "*", value)
} else {
s.Send(nil, server.name, "METADATA", target, key, "*")
s.Send(nil, server.name, RPL_KEYNOTSET, "*", target, key, s.client.t("Key deleted"))
}
}
}
@@ -75,7 +75,7 @@ func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) {
for k, v := range values {
if subs.Has(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", target.Nick(), k, visibility, v)
rb.Add(nil, server.name, RPL_KEYVALUE, "*", target.Nick(), k, visibility, v)
}
}
}
@@ -91,7 +91,7 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) {
for k, v := range values {
if subs.Has(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", chname, k, visibility, v)
rb.Add(nil, server.name, RPL_KEYVALUE, "*", chname, k, visibility, v)
}
}
@@ -100,12 +100,63 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) {
for k, v := range values {
if subs.Has(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v)
rb.Add(nil, server.name, RPL_KEYVALUE, "*", client.Nick(), k, visibility, v)
}
}
}
}
func syncAllMetadata(server *Server, rb *ResponseBuffer) {
client := rb.session.client
batchID := rb.StartNestedBatch(nil, "metadata", "*ALL")
defer rb.EndNestedBatch(batchID)
subs := rb.session.MetadataSubscriptions()
if len(subs) == 0 {
return
}
// deduplicate friends across channels
friendsWithMetadata := make(map[*Client]map[string]string)
// include the client even if they have no channels
friendsWithMetadata[client] = client.ListSubscribedMetadata(subs)
visibility := "*"
for _, channel := range client.Channels() {
chname := channel.Name()
values := channel.ListSubscribedMetadata(subs)
for k, v := range values {
rb.Add(nil, server.name, RPL_KEYVALUE, "*", chname, k, visibility, v)
}
for _, member := range channel.Members() {
if _, ok := friendsWithMetadata[member]; !ok {
friendsWithMetadata[member] = member.ListSubscribedMetadata(subs)
}
}
}
for _, watchedNick := range server.monitorManager.List(rb.session) {
if watched := server.clients.Get(watchedNick); watched != nil {
if _, ok := friendsWithMetadata[watched]; !ok {
friendsWithMetadata[watched] = watched.ListSubscribedMetadata(subs)
}
}
}
for friend, friendMetadata := range friendsWithMetadata {
if friendMetadata == nil {
continue
}
nick := friend.Nick()
for k, v := range friendMetadata {
rb.Add(nil, server.name, RPL_KEYVALUE, "*", nick, k, visibility, v)
}
}
}
func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) {
batchId := rb.StartNestedBatch(nil, "metadata", target)
defer rb.EndNestedBatch(batchId)
@@ -131,7 +182,7 @@ func playMetadataVerbBatch(rb *ResponseBuffer, target string, values map[string]
for key, val := range values {
visibility := "*"
rb.Add(nil, rb.session.client.server.name, "METADATA", target, key, visibility, val)
rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, "*", target, key, visibility, val)
}
}
@@ -146,6 +197,12 @@ func metadataValueIsEvil(config *Config, key, value string) (failMsg string) {
return `METADATA values must be UTF-8`
}
if value == "" {
// we should never hit this case,
// since the command handlers should interpret empty value as "delete"
return `METADATA value MUST NOT be empty`
}
if len(key)+len(value) > maxCombinedMetadataLenBytes ||
(config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) {
+13 -4
View File
@@ -64,12 +64,19 @@ func (manager *MonitorManager) AlertAbout(nick, cfnick string, online bool, clie
for _, session := range watchers {
session.Send(nil, session.client.server.name, command, session.client.Nick(), nick)
if metadata != nil && session.capabilities.Has(caps.Metadata) {
for key := range session.MetadataSubscriptions() {
if online && session.capabilities.Has(caps.Metadata) {
// even if there is no user metadata, or no subscriptions,
// we still need to send an empty metadata batch alongside RPL_MONONLINE
subs := session.MetadataSubscriptions()
rb := NewResponseBuffer(session)
batchID := rb.StartNestedBatch(nil, "metadata", nick)
for key := range subs {
if val, ok := metadata[key]; ok {
session.Send(nil, client.server.name, "METADATA", nick, key, "*", val)
rb.Add(nil, client.server.name, RPL_KEYVALUE, "*", nick, key, "*", val)
}
}
rb.EndNestedBatch(batchID)
rb.Send(false)
}
}
}
@@ -129,7 +136,9 @@ func (manager *MonitorManager) RemoveAll(session *Session) {
func (manager *MonitorManager) List(session *Session) (nicks []string) {
manager.RLock()
defer manager.RUnlock()
for _, nick := range manager.watching[session] {
watching := manager.watching[session]
nicks = make([]string, 0, len(watching))
for _, nick := range watching {
nicks = append(nicks, nick)
}
return nicks
+6
View File
@@ -478,6 +478,12 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
server.handleAutojoins(session, config.Channels.AutoJoin)
}
if session.capabilities.Has(caps.Metadata) {
rb := NewResponseBuffer(session)
syncAllMetadata(server, rb)
rb.Send(true)
}
return false
}
+1 -1
Submodule irctest updated: d18d6e35fe...21c620bc9c