diff --git a/irc/client.go b/irc/client.go index 42a6dc95..8953f04c 100644 --- a/irc/client.go +++ b/irc/client.go @@ -823,7 +823,7 @@ func (client *Client) applyPreregMetadata(session *Session) { return } - // TODO this is expensive + // note: this is expensive but it's comparable to destroy(), OK to do once per session friends := client.FriendsMonitors(caps.Metadata) for _, s := range client.Sessions() { if s != session { diff --git a/irc/getters.go b/irc/getters.go index fc85bce3..a0eaeb5c 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -895,6 +895,23 @@ func (channel *Channel) GetMetadata(key string) (string, bool) { return val, ok } +type MetadataPair struct { + Key string + Value string +} + +func (channel *Channel) GetMetadataBulk(keys []string) (result []MetadataPair) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + for _, k := range keys { + if val, ok := channel.metadata[k]; ok { + result = append(result, MetadataPair{Key: k, Value: val}) + } + } + return result +} + func (channel *Channel) SetMetadata(key string, value string, limit int) (updated bool, err error) { defer channel.MarkDirty(IncludeAllAttrs) @@ -962,6 +979,18 @@ func (client *Client) GetMetadata(key string) (string, bool) { return val, ok } +func (client *Client) GetMetadataBulk(keys []string) (result []MetadataPair) { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + for _, k := range keys { + if val, ok := client.metadata[k]; ok { + result = append(result, MetadataPair{Key: k, Value: val}) + } + } + return result +} + func (client *Client) SetMetadata(key string, value string, limit int) (updated bool, err error) { var alwaysOn bool defer func() { diff --git a/irc/handlers.go b/irc/handlers.go index b74800f0..a8fc1dde 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3375,6 +3375,12 @@ func metadataSubsHandler(client *Client, subcommand string, params []string, rb rb.Add(nil, server.name, RPL_METADATASUBOK, params...) } + if client.registered && len(added) != 0 { + if throttled, _ := client.checkMetadataThrottle(); !throttled { + processMetadataNewSubscriptions(client, rb, added) + } + } + case "unsub": keys := params[2:] removed := rb.session.UnsubscribeFrom(keys...) diff --git a/irc/metadata.go b/irc/metadata.go index 80b0b063..400430dd 100644 --- a/irc/metadata.go +++ b/irc/metadata.go @@ -9,6 +9,7 @@ import ( "github.com/ergochat/ergo/irc/caps" "github.com/ergochat/ergo/irc/modes" + "github.com/ergochat/ergo/irc/utils" ) const ( @@ -35,7 +36,6 @@ func notifySubscribers(server *Server, session *Session, targetObj MetadataHaver 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() { @@ -126,6 +126,40 @@ func playMetadataVerbBatch(rb *ResponseBuffer, target string, values map[string] } } +func processMetadataNewSubscriptions(client *Client, rb *ResponseBuffer, subs []string) { + // "When subscribing to a key, clients SHOULD receive the current value + // of that key for channels/users they are receiving updates for." + // note that this is expensive because we need to compute the friends + visibility := "*" + friendsSeen := make(utils.HashSet[*Client]) + for _, channel := range client.Channels() { + chname := channel.Name() + for _, pair := range channel.GetMetadataBulk(subs) { + rb.Add(nil, client.server.name, "METADATA", chname, pair.Key, visibility, pair.Value) + } + for _, friend := range channel.Members() { + if friendsSeen.Has(friend) { + continue + } + friendsSeen.Add(friend) + for _, pair := range friend.GetMetadataBulk(subs) { + rb.Add(nil, client.server.name, "METADATA", friend.Nick(), pair.Key, visibility, pair.Value) + } + } + } + + for _, friendNick := range client.server.monitorManager.List(rb.session) { + friend := client.server.clients.Get(friendNick) + if friend == nil || friendsSeen.Has(friend) { + continue + } + friendsSeen.Add(friend) + for _, pair := range friend.GetMetadataBulk(subs) { + rb.Add(nil, client.server.name, "METADATA", friend.Nick(), pair.Key, visibility, pair.Value) + } + } +} + var validMetadataKeyRegexp = regexp.MustCompile("^[a-z0-9_./-]+$") func metadataKeyIsEvil(key string) bool {