From 3b7db7fff757919dee3564f5e4adde46ce77d7be Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 18 Jun 2025 00:22:49 -0400 Subject: [PATCH] round 1 of follow-up for metadata (#2277) * refactoring * send an empty batch if necessary, as per spec * don't broadcast no-op updates * don't trim spaces before validating the key * bump irctest to cover metadata * replay existing metadata to reattaching always-on clients * use canonicalized name everywhere * use utils.SafeErrorParam in FAIL lines * validate key names for sub * fix error for METADATA CLEAR * max-keys is enforced for channels as well * remove unlimited configurations * maintain the limit exactly without off-by-one cases * add final channel registration check --- default.yaml | 10 +-- irc/channel.go | 15 +++++ irc/channelmanager.go | 4 ++ irc/config.go | 19 +++--- irc/errors.go | 1 + irc/getters.go | 40 +++++++++--- irc/handlers.go | 141 +++++++++++++++++++++++------------------- irc/help.go | 2 +- irc/metadata.go | 139 +++++++++++++++++------------------------ irc/server.go | 3 + irctest | 2 +- traditional.yaml | 9 ++- 12 files changed, 213 insertions(+), 172 deletions(-) diff --git a/default.yaml b/default.yaml index 22fd27fd..a8d12236 100644 --- a/default.yaml +++ b/default.yaml @@ -724,6 +724,7 @@ oper-classes: - "history" # modify or delete history messages - "defcon" # use the DEFCON command (restrict server capabilities) - "massmessage" # message all users on the server + - "metadata" # modify arbitrary metadata on channels and users # ircd operators opers: @@ -1087,15 +1088,14 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true -# experimental IRC metadata support for setting key/value data on channels and nicknames. +# metadata support for setting key/value data on channels and nicknames. metadata: # can clients store metadata? enabled: true - # how many keys can a client subscribe to? - # set to 0 to disable subscriptions or -1 to allow unlimited subscriptions. + # how many keys can a client subscribe to? max-subs: 100 - # how many keys can a user store about themselves? set to -1 to allow unlimited keys. - max-keys: 1000 + # how many keys can be stored per entity? + max-keys: 100 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications. diff --git a/irc/channel.go b/irc/channel.go index 1b449a70..ade4b5b4 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -7,6 +7,7 @@ package irc import ( "fmt" + "iter" "maps" "strconv" "strings" @@ -1676,6 +1677,20 @@ func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) { return } +func (channel *Channel) sessionsWithCaps(capabs ...caps.Capability) iter.Seq[*Session] { + return func(yield func(*Session) bool) { + for _, member := range channel.Members() { + for _, sess := range member.Sessions() { + if sess.capabilities.HasAll(capabs...) { + if !yield(sess) { + return + } + } + } + } + } +} + // returns whether the client is visible to unprivileged users in the channel // (i.e., respecting auditorium mode). note that this assumes that the client // is a member; if the client is not, it may return true anyway diff --git a/irc/channelmanager.go b/irc/channelmanager.go index 5934ab43..f5bbed39 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -206,6 +206,10 @@ func (cm *ChannelManager) Cleanup(channel *Channel) { } func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) { + if account == "" { + return errAuthRequired // this is already enforced by ChanServ, but do a final check + } + if cm.server.Defcon() <= 4 { return errFeatureDisabled } diff --git a/irc/config.go b/irc/config.go index 67f2ec03..2ef14681 100644 --- a/irc/config.go +++ b/irc/config.go @@ -728,7 +728,7 @@ type Config struct { Enabled bool MaxSubs int `yaml:"max-subs"` MaxKeys int `yaml:"max-keys"` - MaxValueBytes int `yaml:"max-value-length"` // todo: currently unenforced!! + MaxValueBytes int `yaml:"max-value-length"` } WebPush struct { @@ -1649,19 +1649,20 @@ func LoadConfig(filename string) (config *Config, err error) { config.Server.supportedCaps.Disable(caps.Metadata) } else { var metadataValues []string - if config.Metadata.MaxSubs >= 0 { - metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs)) + // these are required for normal operation, so set sane defaults: + if config.Metadata.MaxSubs == 0 { + config.Metadata.MaxSubs = 10 } - if config.Metadata.MaxKeys > 0 { - metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys)) + metadataValues = append(metadataValues, fmt.Sprintf("max-subs=%d", config.Metadata.MaxSubs)) + if config.Metadata.MaxKeys == 0 { + config.Metadata.MaxKeys = 10 } + metadataValues = append(metadataValues, fmt.Sprintf("max-keys=%d", config.Metadata.MaxKeys)) + // this is not required since we enforce a hardcoded upper bound on key+value if config.Metadata.MaxValueBytes > 0 { metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes)) } - if len(metadataValues) != 0 { - config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",") - } - + config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",") } err = config.processExtjwt() diff --git a/irc/errors.go b/irc/errors.go index aed797eb..6c3a790a 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -33,6 +33,7 @@ var ( errAccountVerificationInvalidCode = errors.New("Invalid account verification code") errAccountUpdateFailed = errors.New(`Error while updating your account information`) errAccountMustHoldNick = errors.New(`You must hold that nickname in order to register it`) + errAuthRequired = errors.New("You must be logged into an account to do this") errAuthzidAuthcidMismatch = errors.New(`authcid and authzid must be the same`) errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") diff --git a/irc/getters.go b/irc/getters.go index 5feadaa3..3bba6115 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -894,7 +894,7 @@ func (channel *Channel) GetMetadata(key string) (string, bool) { return val, ok } -func (channel *Channel) SetMetadata(key string, value string) { +func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) { defer channel.MarkDirty(IncludeAllAttrs) channel.stateMutex.Lock() @@ -904,7 +904,15 @@ func (channel *Channel) SetMetadata(key string, value string) { channel.metadata = make(map[string]string) } - channel.metadata[key] = value + existing, ok := channel.metadata[key] + if !ok && len(channel.metadata) >= limit { + return false, errLimitExceeded + } + updated = !ok || value != existing + if updated { + channel.metadata[key] = value + } + return updated, nil } func (channel *Channel) ListMetadata() map[string]string { @@ -914,13 +922,17 @@ func (channel *Channel) ListMetadata() map[string]string { return maps.Clone(channel.metadata) } -func (channel *Channel) DeleteMetadata(key string) { +func (channel *Channel) DeleteMetadata(key string) (updated bool) { defer channel.MarkDirty(IncludeAllAttrs) channel.stateMutex.Lock() defer channel.stateMutex.Unlock() - delete(channel.metadata, key) + _, updated = channel.metadata[key] + if updated { + delete(channel.metadata, key) + } + return updated } func (channel *Channel) ClearMetadata() map[string]string { @@ -949,7 +961,7 @@ func (client *Client) GetMetadata(key string) (string, bool) { return val, ok } -func (client *Client) SetMetadata(key string, value string) { +func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) { client.stateMutex.Lock() defer client.stateMutex.Unlock() @@ -957,7 +969,15 @@ func (client *Client) SetMetadata(key string, value string) { client.metadata = make(map[string]string) } - client.metadata[key] = value + existing, ok := client.metadata[key] + if !ok && len(client.metadata) >= limit { + return false, errLimitExceeded + } + updated = !ok || value != existing + if updated { + client.metadata[key] = value + } + return updated, nil } func (client *Client) ListMetadata() map[string]string { @@ -967,11 +987,15 @@ func (client *Client) ListMetadata() map[string]string { return maps.Clone(client.metadata) } -func (client *Client) DeleteMetadata(key string) { +func (client *Client) DeleteMetadata(key string) (updated bool) { client.stateMutex.Lock() defer client.stateMutex.Unlock() - delete(client.metadata, key) + _, updated = client.metadata[key] + if updated { + delete(client.metadata, key) + } + return updated } func (client *Client) ClearMetadata() map[string]string { diff --git a/irc/handlers.go b/irc/handlers.go index 6d727268..e2cdf94d 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" "time" + "unicode/utf8" "github.com/ergochat/irc-go/ircfmt" "github.com/ergochat/irc-go/ircmsg" @@ -3101,43 +3102,42 @@ func markReadHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res // METADATA [...] func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) (exiting bool) { - originalTarget := msg.Params[0] - target := originalTarget + target := msg.Params[0] - if !server.Config().Metadata.Enabled { - rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", originalTarget, "Metadata is disabled on this server") + config := server.Config() + if !config.Metadata.Enabled { + rb.Add(nil, server.name, "FAIL", "METADATA", "FORBIDDEN", target, "Metadata is disabled on this server") return } subcommand := strings.ToLower(msg.Params[1]) - invalidTarget := func() { - rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", originalTarget, client.t("Invalid metadata target")) - } noKeyPerms := func(key string) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", originalTarget, key, client.t("You do not have permission to perform this action")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_NO_PERMISSION", target, key, client.t("You do not have permission to perform this action")) } if target == "*" { target = client.Nick() } - targetClient := server.clients.Get(target) - targetChannel := server.channels.Get(target) - if !metadataCanISeeThisTarget(client, target) { - invalidTarget() - return + var targetObj MetadataHaver + var targetClient *Client + var targetChannel *Channel + if strings.HasPrefix(target, "#") { + targetChannel = server.channels.Get(target) + if targetChannel != nil { + targetObj = targetChannel + target = targetChannel.Name() // canonicalize case + } + } else { + targetClient = server.clients.Get(target) + if targetClient != nil { + targetObj = targetClient + target = targetClient.Nick() // canonicalize case + } } - - var t MetadataHaver - if targetClient != nil { - t = targetClient - } - if targetChannel != nil { - t = targetChannel - } - if t == nil { - invalidTarget() + if targetObj == nil { + rb.Add(nil, server.name, "FAIL", "METADATA", "INVALID_TARGET", target, client.t("Invalid metadata target")) return } @@ -3151,101 +3151,105 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res case "set": key := strings.ToLower(msg.Params[2]) if metadataKeyIsEvil(key) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) return } - if !metadataCanIEditThisKey(client, target, key) { + if !metadataCanIEditThisKey(client, targetObj, key) { noKeyPerms(key) return } if len(msg.Params) > 3 { value := msg.Params[3] - const maxCombinedLen = 350 - if len(key)+len(value) > maxCombinedLen { + if !globalUtf8EnforcementSetting && !utf8.ValidString(value) { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("METADATA values must be UTF-8")) + return + } + + if len(key)+len(value) > maxCombinedMetadataLenBytes || + (config.Metadata.MaxValueBytes > 0 && len(value) > config.Metadata.MaxValueBytes) { + rb.Add(nil, server.name, "FAIL", "METADATA", "VALUE_INVALID", client.t("Value is too long")) return } - maxKeys := server.Config().Metadata.MaxKeys - isSelf := targetClient != nil && client == targetClient - - if isSelf && maxKeys > 0 && t.CountMetadata() >= maxKeys { - rb.Add(nil, server.name, "FAIL", "METADATA", "LIMIT_REACHED", client.nick, client.t("You have too many keys set on yourself")) + 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")) return } - - server.logger.Debug("metadata", "setting", key, value, "on", target) - - t.SetMetadata(key, value) - notifySubscribers(server, rb.session, target, key, value) - - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, "*", value) + // echo the value to the client whether or not there was a real update + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, "*", value) + if updated { + notifySubscribers(server, rb.session, targetObj, target, key, value) + } } else { - server.logger.Debug("metadata", "deleting", key, "on", target) - t.DeleteMetadata(key) - notifySubscribers(server, rb.session, target, key, "") - + if updated := targetObj.DeleteMetadata(key); updated { + notifySubscribers(server, rb.session, targetObj, target, key, "") + } + // acknowledge to the client whether or not there was a real update rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key deleted")) } case "get": + if !metadataCanISeeThisTarget(client, targetObj) { + noKeyPerms("*") + return + } + batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) for _, key := range msg.Params[2:] { if metadataKeyIsEvil(key) { - rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", key, client.t("Invalid key name")) + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", utils.SafeErrorParam(key), client.t("Invalid key name")) continue } - val, ok := t.GetMetadata(key) + val, ok := targetObj.GetMetadata(key) if !ok { rb.Add(nil, server.name, RPL_KEYNOTSET, client.Nick(), target, key, client.t("Key is not set")) continue } visibility := "*" - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val) } case "list": - values := t.ListMetadata() - - batchId := rb.StartNestedBatch("metadata") - defer rb.EndNestedBatch(batchId) - - for key, val := range values { - visibility := "*" - - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) - } + playMetadataList(rb, client.Nick(), target, targetObj.ListMetadata()) case "clear": - if !metadataCanIEditThisTarget(client, target) { - invalidTarget() + if !metadataCanIEditThisTarget(client, targetObj) { + noKeyPerms("*") return } - values := t.ClearMetadata() + values := targetObj.ClearMetadata() batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) for key, val := range values { visibility := "*" - rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), originalTarget, key, visibility, val) + rb.Add(nil, server.name, RPL_KEYVALUE, client.Nick(), target, key, visibility, val) } case "sub": keys := msg.Params[2:] - server.logger.Debug("metadata", client.nick, "has subscrumbled to", strings.Join(keys, ", ")) + for _, key := range keys { + if metadataKeyIsEvil(key) { + rb.Add(nil, server.name, "FAIL", "METADATA", "KEY_INVALID", 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", bad, client.t("Too many subscriptions")) + rb.Add(nil, server.name, "FAIL", "METADATA", "TOO_MANY_SUBS", utils.SafeErrorParam(bad), client.t("Too many subscriptions")) } lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10 @@ -3258,7 +3262,6 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res case "unsub": keys := msg.Params[2:] - server.logger.Debug("metadata", client.nick, "has UNsubscrumbled to", strings.Join(keys, ", ")) removed := rb.session.UnsubscribeFrom(keys...) lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10 @@ -3288,12 +3291,22 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res } default: - rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", msg.Params[1], client.t("Invalid subcommand")) + rb.Add(nil, server.name, "FAIL", "METADATA", "SUBCOMMAND_INVALID", utils.SafeErrorParam(msg.Params[1]), client.t("Invalid subcommand")) } return } +func playMetadataList(rb *ResponseBuffer, nick, target string, values map[string]string) { + batchId := rb.StartNestedBatch("metadata") + defer rb.EndNestedBatch(batchId) + + for key, val := range values { + visibility := "*" + rb.Add(nil, rb.session.client.server.name, RPL_KEYVALUE, nick, target, key, visibility, val) + } +} + // REHASH func rehashHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool { nick := client.Nick() diff --git a/irc/help.go b/irc/help.go index d516c43e..55aef6cf 100644 --- a/irc/help.go +++ b/irc/help.go @@ -342,7 +342,7 @@ specification.`, }, "metadata": { text: `METADATA [...] - + Retrieve and meddle with metadata for the given target. Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`, }, diff --git a/irc/metadata.go b/irc/metadata.go index d7094065..b19962c0 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -2,12 +2,17 @@ package irc import ( "errors" + "iter" + "maps" "regexp" - "strings" "github.com/ergochat/ergo/irc/caps" "github.com/ergochat/ergo/irc/modes" - "github.com/ergochat/ergo/irc/utils" +) + +const ( + // metadata key + value need to be relayable on a single IRC RPL_KEYVALUE line + maxCombinedMetadataLenBytes = 350 ) var ( @@ -16,42 +21,39 @@ var ( ) type MetadataHaver = interface { - SetMetadata(key string, value string) + SetMetadata(key string, value string, limit int) (updated bool, err error) GetMetadata(key string) (string, bool) - DeleteMetadata(key string) + DeleteMetadata(key string) (updated bool) ListMetadata() map[string]string ClearMetadata() map[string]string CountMetadata() int } -func notifySubscribers(server *Server, session *Session, target string, key string, value string) { - var notify utils.HashSet[*Session] = make(utils.HashSet[*Session]) - targetChannel := server.channels.Get(target) - targetClient := server.clients.Get(target) +func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver, targetName, key, value string) { + var recipientSessions iter.Seq[*Session] - if targetClient != nil { - notify = targetClient.FriendsMonitors(caps.Metadata) - // notify clients about changes regarding themselves - for _, s := range targetClient.Sessions() { - notify.Add(s) - } - } - if targetChannel != nil { - members := targetChannel.Members() - for _, m := range members { - for _, s := range m.Sessions() { - if s.capabilities.Has(caps.Metadata) { - notify.Add(s) - } - } + switch target := targetObj.(type) { + case *Client: + // TODO this case is expensive and might warrant rate-limiting + friends := target.FriendsMonitors(caps.Metadata) + // broadcast metadata update to other connected sessions + for _, s := range target.Sessions() { + friends.Add(s) } + recipientSessions = maps.Keys(friends) + case *Channel: + recipientSessions = target.sessionsWithCaps(caps.Metadata) + default: + return // impossible } - // don't notify the session that made the change - notify.Remove(session) + broadcastMetadataUpdate(server, recipientSessions, session, targetName, key, value) +} - for s := range notify { - if !s.isSubscribedTo(key) { +func broadcastMetadataUpdate(server *Server, sessions iter.Seq[*Session], originator *Session, target, key, value string) { + for s := range sessions { + // don't notify the session that made the change + if s == originator || !s.isSubscribedTo(key) { continue } @@ -64,42 +66,38 @@ func notifySubscribers(server *Server, session *Session, target string, key stri } func syncClientMetadata(server *Server, rb *ResponseBuffer, target *Client) { - if len(rb.session.MetadataSubscriptions()) == 0 { - return - } - batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) + subs := rb.session.MetadataSubscriptions() values := target.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" rb.Add(nil, server.name, "METADATA", target.Nick(), k, visibility, v) } } } -func syncChannelMetadata(server *Server, rb *ResponseBuffer, target *Channel) { - if len(rb.session.MetadataSubscriptions()) == 0 { - return - } - +func syncChannelMetadata(server *Server, rb *ResponseBuffer, channel *Channel) { batchId := rb.StartNestedBatch("metadata") defer rb.EndNestedBatch(batchId) - values := target.ListMetadata() + subs := rb.session.MetadataSubscriptions() + chname := channel.Name() + + values := channel.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" - rb.Add(nil, server.name, "METADATA", target.Name(), k, visibility, v) + rb.Add(nil, server.name, "METADATA", chname, k, visibility, v) } } - for _, client := range target.Members() { + for _, client := range channel.Members() { values := client.ListMetadata() for k, v := range values { - if rb.session.isSubscribedTo(k) { + if subs.Has(k) { visibility := "*" rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v) } @@ -110,55 +108,34 @@ func syncChannelMetadata(server *Server, rb *ResponseBuffer, target *Channel) { var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+") func metadataKeyIsEvil(key string) bool { - key = strings.TrimSpace(key) // just in case - return len(key) == 0 || // key needs to contain stuff key[0] == ':' || // key can't start with a colon metadataEvilCharsRegexp.MatchString(key) // key can't contain the stuff it can't contain } -func metadataCanIEditThisKey(client *Client, target string, _ string) bool { - if !metadataCanIEditThisTarget(client, target) { // you can't edit keys on targets you can't edit. - return false - } - - // todo: we don't actually do anything regarding visibility yet so there's not much to do here - - return true +func metadataCanIEditThisKey(client *Client, targetObj MetadataHaver, key string) bool { + // no key-specific logic as yet + return metadataCanIEditThisTarget(client, targetObj) } -func metadataCanIEditThisTarget(client *Client, target string) bool { - if !metadataCanISeeThisTarget(client, target) { // you can't edit what you can't see. a wise man told me this once - return false +func metadataCanIEditThisTarget(client *Client, targetObj MetadataHaver) bool { + switch target := targetObj.(type) { + case *Client: + return client == target || client.HasRoleCapabs("metadata") + case *Channel: + return target.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("metadata") + default: + return false // impossible } - - if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want - return true - } - - if target == client.Nick() { // your right to swing your fist ends where my nose begins - return true - } - - // if you're a channel operator, knock yourself out - channel := client.server.channels.Get(target) - if channel != nil && channel.ClientIsAtLeast(client, modes.Operator) { - return true - } - - return false } -func metadataCanISeeThisTarget(client *Client, target string) bool { - if client.HasRoleCapabs("sajoin") { // sajoin opers can do whatever they want +func metadataCanISeeThisTarget(client *Client, targetObj MetadataHaver) bool { + switch target := targetObj.(type) { + case *Client: return true + case *Channel: + return target.hasClient(client) || client.HasRoleCapabs("metadata") + default: + return false // impossible } - - // check if the user is in the channel - channel := client.server.channels.Get(target) - if channel != nil && !channel.hasClient(client) { - return false - } - - return true } diff --git a/irc/server.go b/irc/server.go index 5974f633..4b23fe8a 100644 --- a/irc/server.go +++ b/irc/server.go @@ -496,6 +496,9 @@ func (server *Server) playRegistrationBurst(session *Session) { if !(rb.session.capabilities.Has(caps.ExtendedISupport) && rb.session.isupportSentPrereg) { server.RplISupport(c, rb) } + if session.capabilities.Has(caps.Metadata) && c.AlwaysOn() { + playMetadataList(rb, d.nick, d.nick, c.ListMetadata()) + } if d.account != "" && session.capabilities.Has(caps.Persistence) { reportPersistenceStatus(c, rb, false) } diff --git a/irctest b/irctest index 15c847eb..a3c2110b 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 15c847ebb19b1c434347236296cbc4d34ba7c4b2 +Subproject commit a3c2110be24ad53ec4babe0facdc25aa2151d3e5 diff --git a/traditional.yaml b/traditional.yaml index 55f16ea7..4b29ec34 100644 --- a/traditional.yaml +++ b/traditional.yaml @@ -695,6 +695,7 @@ oper-classes: - "history" # modify or delete history messages - "defcon" # use the DEFCON command (restrict server capabilities) - "massmessage" # message all users on the server + - "metadata" # modify arbitrary metadata on channels and users # ircd operators opers: @@ -1058,12 +1059,14 @@ history: # e.g., ERGO__SERVER__MAX_SENDQ=128k. see the manual for more details. allow-environment-overrides: true -# experimental IRC metadata support for setting key/value data on channels and nicknames. +# metadata support for setting key/value data on channels and nicknames. metadata: - # can clients use the metadata command? + # can clients store metadata? enabled: true # how many keys can a client subscribe to? - max-subs: 1000 + max-subs: 100 + # how many keys can be stored per entity? + max-keys: 100 # experimental support for mobile push notifications # see the manual for potential security, privacy, and performance implications.