tidy up a bit

This commit is contained in:
leah 2025-06-13 20:06:11 +01:00
parent db4b23bb48
commit 6d94aa1591
10 changed files with 112 additions and 67 deletions

View file

@ -238,7 +238,7 @@ CAPDEFS = [
standard="Soju/Goguma vendor", standard="Soju/Goguma vendor",
), ),
CapDef( CapDef(
identifier="MetadataTwoJudgementDay", identifier="Metadata",
name="draft/metadata-2", name="draft/metadata-2",
url="https://ircv3.net/specs/extensions/metadata", url="https://ircv3.net/specs/extensions/metadata",
standard="draft IRCv3", standard="draft IRCv3",

View file

@ -65,9 +65,9 @@ const (
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md // https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
MessageRedaction Capability = iota MessageRedaction Capability = iota
// MetadataTwoJudgementDay is the draft IRCv3 capability named "draft/metadata-2": // Metadata is the draft IRCv3 capability named "draft/metadata-2":
// https://ircv3.net/specs/extensions/metadata // https://ircv3.net/specs/extensions/metadata
MetadataTwoJudgementDay Capability = iota Metadata Capability = iota
// Multiline is the proposed IRCv3 capability named "draft/multiline": // Multiline is the proposed IRCv3 capability named "draft/multiline":
// https://github.com/ircv3/ircv3-specifications/pull/398 // https://github.com/ircv3/ircv3-specifications/pull/398

View file

@ -55,7 +55,7 @@ type Channel struct {
dirtyBits uint dirtyBits uint
settings ChannelSettings settings ChannelSettings
uuid utils.UUID uuid utils.UUID
metadata MetadataStore metadata map[string]string
// these caches are paired to allow iteration over channel members without holding the lock // these caches are paired to allow iteration over channel members without holding the lock
membersCache []*Client membersCache []*Client
memberDataCache []*memberData memberDataCache []*memberData
@ -895,6 +895,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname)) rb.Add(nil, client.server.name, "MARKREAD", chname, client.GetReadMarker(chcfname))
} }
if rb.session.capabilities.Has(caps.Metadata) {
syncChannelMetadata(client.server, rb, channel)
}
if rb.session.client == client { if rb.session.client == client {
// don't send topic and names for a SAJOIN of a different client // don't send topic and names for a SAJOIN of a different client
channel.SendTopic(client, rb, false) channel.SendTopic(client, rb, false)

View file

@ -64,7 +64,7 @@ type RegisteredChannel struct {
// Settings are the chanserv-modifiable settings // Settings are the chanserv-modifiable settings
Settings ChannelSettings Settings ChannelSettings
// Metadata set using the METADATA command // Metadata set using the METADATA command
Metadata MetadataStore Metadata map[string]string
} }
func (r *RegisteredChannel) Serialize() ([]byte, error) { func (r *RegisteredChannel) Serialize() ([]byte, error) {

View file

@ -131,7 +131,7 @@ type Client struct {
clearablePushMessages map[string]time.Time clearablePushMessages map[string]time.Time
pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0 pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0
pushQueue pushQueue pushQueue pushQueue
metadata MetadataStore metadata map[string]string
} }
type saslStatus struct { type saslStatus struct {
@ -216,7 +216,7 @@ type Session struct {
webPushEndpoint string // goroutine-local: web push endpoint registered by the current session webPushEndpoint string // goroutine-local: web push endpoint registered by the current session
metadataSubscriptions []string metadataSubscriptions utils.HashSet[string]
} }
// MultilineBatch tracks the state of a client-to-server multiline batch. // MultilineBatch tracks the state of a client-to-server multiline batch.

View file

@ -1646,7 +1646,7 @@ func LoadConfig(filename string) (config *Config, err error) {
} }
if !config.Metadata.Enabled { if !config.Metadata.Enabled {
config.Server.supportedCaps.Disable(caps.MetadataTwoJudgementDay) config.Server.supportedCaps.Disable(caps.Metadata)
} else { } else {
var metadataValues []string var metadataValues []string
if config.Metadata.MaxSubs >= 0 { if config.Metadata.MaxSubs >= 0 {
@ -1659,7 +1659,7 @@ func LoadConfig(filename string) (config *Config, err error) {
metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes)) metadataValues = append(metadataValues, fmt.Sprintf("max-value-bytes=%d", config.Metadata.MaxValueBytes))
} }
if len(metadataValues) != 0 { if len(metadataValues) != 0 {
config.Server.capValues[caps.MetadataTwoJudgementDay] = strings.Join(metadataValues, ",") config.Server.capValues[caps.Metadata] = strings.Join(metadataValues, ",")
} }
} }

View file

@ -833,24 +833,32 @@ func (session *Session) isSubscribedTo(key string) bool {
session.client.stateMutex.RLock() session.client.stateMutex.RLock()
defer session.client.stateMutex.RUnlock() defer session.client.stateMutex.RUnlock()
return slices.Contains(session.metadataSubscriptions, key) if session.metadataSubscriptions == nil {
return false
}
return session.metadataSubscriptions.Has(key)
} }
func (session *Session) SubscribeTo(keys ...string) ([]string, error) { func (session *Session) SubscribeTo(keys ...string) ([]string, error) {
session.client.stateMutex.Lock() session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock() defer session.client.stateMutex.Unlock()
if session.metadataSubscriptions == nil {
session.metadataSubscriptions = make(utils.HashSet[string])
}
var added []string var added []string
maxSubs := session.client.server.Config().Metadata.MaxSubs maxSubs := session.client.server.Config().Metadata.MaxSubs
for _, k := range keys { for _, k := range keys {
if !slices.Contains(session.metadataSubscriptions, k) { if !session.metadataSubscriptions.Has(k) {
if len(session.metadataSubscriptions) > maxSubs { if len(session.metadataSubscriptions) > maxSubs {
return added, errMetadataTooManySubs return added, errMetadataTooManySubs
} }
added = append(added, k) added = append(added, k)
session.metadataSubscriptions = append(session.metadataSubscriptions, k) session.metadataSubscriptions.Add(k)
} }
} }
@ -863,27 +871,25 @@ func (session *Session) UnsubscribeFrom(keys ...string) []string {
var removed []string var removed []string
new := slices.DeleteFunc(session.metadataSubscriptions, if session.metadataSubscriptions == nil {
func(keyName string) bool { return []string{}
if slices.Contains(keys, keyName) { }
removed = append(removed, keyName)
return true
} else {
return false
}
},
)
session.metadataSubscriptions = new for k := range session.metadataSubscriptions {
if slices.Contains(keys, k) {
removed = append(removed, k)
session.metadataSubscriptions.Remove(k)
}
}
return removed return removed
} }
func (session *Session) MetadataSubscriptions() []string { func (session *Session) MetadataSubscriptions() utils.HashSet[string] {
session.client.stateMutex.Lock() session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock() defer session.client.stateMutex.Unlock()
return slices.Clone(session.metadataSubscriptions) return maps.Clone(session.metadataSubscriptions)
} }
func (channel *Channel) GetMetadata(key string) (string, error) { func (channel *Channel) GetMetadata(key string) (string, error) {
@ -901,7 +907,7 @@ func (channel *Channel) SetMetadata(key string, value string) {
channel.stateMutex.Lock() channel.stateMutex.Lock()
if channel.metadata == nil { if channel.metadata == nil {
channel.metadata = make(MetadataStore) channel.metadata = make(map[string]string)
} }
channel.metadata[key] = value channel.metadata[key] = value
@ -909,7 +915,7 @@ func (channel *Channel) SetMetadata(key string, value string) {
channel.MarkDirty(IncludeAllAttrs) channel.MarkDirty(IncludeAllAttrs)
} }
func (channel *Channel) ListMetadata() MetadataStore { func (channel *Channel) ListMetadata() map[string]string {
channel.stateMutex.RLock() channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock() defer channel.stateMutex.RUnlock()
@ -924,11 +930,11 @@ func (channel *Channel) DeleteMetadata(key string) {
channel.MarkDirty(IncludeAllAttrs) channel.MarkDirty(IncludeAllAttrs)
} }
func (channel *Channel) ClearMetadata() MetadataStore { func (channel *Channel) ClearMetadata() map[string]string {
channel.stateMutex.Lock() channel.stateMutex.Lock()
oldMap := channel.metadata oldMap := channel.metadata
channel.metadata = make(MetadataStore) channel.metadata = make(map[string]string)
channel.stateMutex.Unlock() channel.stateMutex.Unlock()
channel.MarkDirty(IncludeAllAttrs) channel.MarkDirty(IncludeAllAttrs)
@ -962,15 +968,13 @@ func (client *Client) SetMetadata(key string, value string) {
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
if client.metadata == nil { if client.metadata == nil {
client.metadata = make(MetadataStore) client.metadata = make(map[string]string)
} }
client.metadata[key] = value client.metadata[key] = value
// coming soon: https://www.youtube.com/watch?v=K14JkFfWUzc
} }
func (client *Client) ListMetadata() MetadataStore { func (client *Client) ListMetadata() map[string]string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -984,12 +988,12 @@ func (client *Client) DeleteMetadata(key string) {
delete(client.metadata, key) delete(client.metadata, key)
} }
func (client *Client) ClearMetadata() MetadataStore { func (client *Client) ClearMetadata() map[string]string {
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
oldMap := client.metadata oldMap := client.metadata
client.metadata = make(MetadataStore) client.metadata = make(map[string]string)
return oldMap return oldMap
} }

View file

@ -9,11 +9,13 @@ package irc
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"maps"
"net" "net"
"os" "os"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"runtime/pprof" "runtime/pprof"
"slices"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -3248,10 +3250,10 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10 lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBOK) - len(client.Nick()) - 10
chunked := utils.ChunkifyParams(added, lineLength) chunked := utils.ChunkifyParams(slices.Values(added), lineLength)
for _, line := range chunked { for _, line := range chunked {
params := append([]string{client.Nick()}, line...) params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...) rb.Add(nil, server.name, RPL_METADATASUBOK, params...)
} }
case "unsub": case "unsub":
@ -3260,42 +3262,29 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
removed := rb.session.UnsubscribeFrom(keys...) removed := rb.session.UnsubscribeFrom(keys...)
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10 lineLength := MaxLineLen - len(server.name) - len(RPL_METADATAUNSUBOK) - len(client.Nick()) - 10
chunked := utils.ChunkifyParams(removed, lineLength) chunked := utils.ChunkifyParams(slices.Values(removed), lineLength)
for _, line := range chunked { for _, line := range chunked {
params := append([]string{client.Nick()}, line...) params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...) rb.Add(nil, server.name, RPL_METADATAUNSUBOK, params...)
} }
case "subs": case "subs":
lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10 // for safety lineLength := MaxLineLen - len(server.name) - len(RPL_METADATASUBS) - len(client.Nick()) - 10 // for safety
chunked := utils.ChunkifyParams(rb.session.MetadataSubscriptions(), lineLength) subs := rb.session.MetadataSubscriptions()
chunked := utils.ChunkifyParams(maps.Keys(subs), lineLength)
for _, line := range chunked { for _, line := range chunked {
params := append([]string{client.Nick()}, line...) params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...) rb.Add(nil, server.name, RPL_METADATASUBS, params...)
} }
case "sync": case "sync":
batchId := rb.StartNestedBatch("metadata")
defer rb.EndNestedBatch(batchId)
values := t.ListMetadata()
for k, v := range values {
if rb.session.isSubscribedTo(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", target, k, visibility, v)
}
}
if targetChannel != nil { if targetChannel != nil {
for _, client := range targetChannel.Members() { syncChannelMetadata(server, rb, targetChannel)
values := client.ListMetadata() }
for k, v := range values { if targetClient != nil {
if rb.session.isSubscribedTo(k) { syncClientMetadata(server, rb, targetClient)
visibility := "*"
rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v)
}
}
}
} }
default: default:

View file

@ -15,14 +15,12 @@ var (
errMetadataNotFound = errors.New("key not found") errMetadataNotFound = errors.New("key not found")
) )
type MetadataStore = map[string]string
type MetadataHaver = interface { type MetadataHaver = interface {
SetMetadata(key string, value string) SetMetadata(key string, value string)
GetMetadata(key string) (string, error) GetMetadata(key string) (string, error)
DeleteMetadata(key string) DeleteMetadata(key string)
ListMetadata() MetadataStore ListMetadata() map[string]string
ClearMetadata() MetadataStore ClearMetadata() map[string]string
CountMetadata() int CountMetadata() int
} }
@ -32,7 +30,7 @@ func notifySubscribers(server *Server, session *Session, target string, key stri
targetClient := server.clients.Get(target) targetClient := server.clients.Get(target)
if targetClient != nil { if targetClient != nil {
notify = targetClient.FriendsMonitors(caps.MetadataTwoJudgementDay) notify = targetClient.FriendsMonitors(caps.Metadata)
// notify clients about changes regarding themselves // notify clients about changes regarding themselves
for _, s := range targetClient.Sessions() { for _, s := range targetClient.Sessions() {
notify.Add(s) notify.Add(s)
@ -42,7 +40,7 @@ func notifySubscribers(server *Server, session *Session, target string, key stri
members := targetChannel.Members() members := targetChannel.Members()
for _, m := range members { for _, m := range members {
for _, s := range m.Sessions() { for _, s := range m.Sessions() {
if s.capabilities.Has(caps.MetadataTwoJudgementDay) { if s.capabilities.Has(caps.Metadata) {
notify.Add(s) notify.Add(s)
} }
} }
@ -65,6 +63,50 @@ 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)
values := target.ListMetadata()
for k, v := range values {
if rb.session.isSubscribedTo(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
}
batchId := rb.StartNestedBatch("metadata")
defer rb.EndNestedBatch(batchId)
values := target.ListMetadata()
for k, v := range values {
if rb.session.isSubscribedTo(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", target.Name(), k, visibility, v)
}
}
for _, client := range target.Members() {
values := client.ListMetadata()
for k, v := range values {
if rb.session.isSubscribedTo(k) {
visibility := "*"
rb.Add(nil, server.name, "METADATA", client.Nick(), k, visibility, v)
}
}
}
}
var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+") var metadataEvilCharsRegexp = regexp.MustCompile("[^A-Za-z0-9_./:-]+")
func metadataKeyIsEvil(key string) bool { func metadataKeyIsEvil(key string) bool {

View file

@ -1,12 +1,14 @@
package utils package utils
func ChunkifyParams(params []string, maxChars int) [][]string { import "iter"
func ChunkifyParams(params iter.Seq[string], maxChars int) [][]string {
var chunked [][]string var chunked [][]string
var acc []string var acc []string
var length = 0 var length = 0
for _, p := range params { for p := range params {
length = length + len(p) + 1 // (accounting for the space) length = length + len(p) + 1 // (accounting for the space)
if length > maxChars { if length > maxChars {
@ -18,5 +20,9 @@ func ChunkifyParams(params []string, maxChars int) [][]string {
acc = append(acc, p) acc = append(acc, p)
} }
if len(acc) != 0 {
chunked = append(chunked, acc)
}
return chunked return chunked
} }