Add METADATA to the response for new subscriptions; generation of this
response is throttled, even though subscription itself is not, so if
you're throttled the subscription will succeed but you won't get
METADATA lines.
This commit is contained in:
Shivaram Lingamneni 2025-12-15 17:19:17 +00:00
parent d5fb189a55
commit 9484334e4f
4 changed files with 71 additions and 2 deletions

View file

@ -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 {

View file

@ -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() {

View file

@ -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...)

View file

@ -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 {