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",
),
CapDef(
identifier="MetadataTwoJudgementDay",
identifier="Metadata",
name="draft/metadata-2",
url="https://ircv3.net/specs/extensions/metadata",
standard="draft IRCv3",

View file

@ -65,9 +65,9 @@ const (
// https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md
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
MetadataTwoJudgementDay Capability = iota
Metadata Capability = iota
// Multiline is the proposed IRCv3 capability named "draft/multiline":
// https://github.com/ircv3/ircv3-specifications/pull/398

View file

@ -55,7 +55,7 @@ type Channel struct {
dirtyBits uint
settings ChannelSettings
uuid utils.UUID
metadata MetadataStore
metadata map[string]string
// these caches are paired to allow iteration over channel members without holding the lock
membersCache []*Client
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))
}
if rb.session.capabilities.Has(caps.Metadata) {
syncChannelMetadata(client.server, rb, channel)
}
if rb.session.client == client {
// don't send topic and names for a SAJOIN of a different client
channel.SendTopic(client, rb, false)

View file

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

View file

@ -131,7 +131,7 @@ type Client struct {
clearablePushMessages map[string]time.Time
pushSubscriptionsExist atomic.Uint32 // this is a cache on len(pushSubscriptions) != 0
pushQueue pushQueue
metadata MetadataStore
metadata map[string]string
}
type saslStatus struct {
@ -216,7 +216,7 @@ type Session struct {
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.

View file

@ -1646,7 +1646,7 @@ func LoadConfig(filename string) (config *Config, err error) {
}
if !config.Metadata.Enabled {
config.Server.supportedCaps.Disable(caps.MetadataTwoJudgementDay)
config.Server.supportedCaps.Disable(caps.Metadata)
} else {
var metadataValues []string
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))
}
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()
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) {
session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock()
if session.metadataSubscriptions == nil {
session.metadataSubscriptions = make(utils.HashSet[string])
}
var added []string
maxSubs := session.client.server.Config().Metadata.MaxSubs
for _, k := range keys {
if !slices.Contains(session.metadataSubscriptions, k) {
if !session.metadataSubscriptions.Has(k) {
if len(session.metadataSubscriptions) > maxSubs {
return added, errMetadataTooManySubs
}
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
new := slices.DeleteFunc(session.metadataSubscriptions,
func(keyName string) bool {
if slices.Contains(keys, keyName) {
removed = append(removed, keyName)
return true
} else {
return false
}
},
)
if session.metadataSubscriptions == nil {
return []string{}
}
session.metadataSubscriptions = new
for k := range session.metadataSubscriptions {
if slices.Contains(keys, k) {
removed = append(removed, k)
session.metadataSubscriptions.Remove(k)
}
}
return removed
}
func (session *Session) MetadataSubscriptions() []string {
func (session *Session) MetadataSubscriptions() utils.HashSet[string] {
session.client.stateMutex.Lock()
defer session.client.stateMutex.Unlock()
return slices.Clone(session.metadataSubscriptions)
return maps.Clone(session.metadataSubscriptions)
}
func (channel *Channel) GetMetadata(key string) (string, error) {
@ -901,7 +907,7 @@ func (channel *Channel) SetMetadata(key string, value string) {
channel.stateMutex.Lock()
if channel.metadata == nil {
channel.metadata = make(MetadataStore)
channel.metadata = make(map[string]string)
}
channel.metadata[key] = value
@ -909,7 +915,7 @@ func (channel *Channel) SetMetadata(key string, value string) {
channel.MarkDirty(IncludeAllAttrs)
}
func (channel *Channel) ListMetadata() MetadataStore {
func (channel *Channel) ListMetadata() map[string]string {
channel.stateMutex.RLock()
defer channel.stateMutex.RUnlock()
@ -924,11 +930,11 @@ func (channel *Channel) DeleteMetadata(key string) {
channel.MarkDirty(IncludeAllAttrs)
}
func (channel *Channel) ClearMetadata() MetadataStore {
func (channel *Channel) ClearMetadata() map[string]string {
channel.stateMutex.Lock()
oldMap := channel.metadata
channel.metadata = make(MetadataStore)
channel.metadata = make(map[string]string)
channel.stateMutex.Unlock()
channel.MarkDirty(IncludeAllAttrs)
@ -962,15 +968,13 @@ func (client *Client) SetMetadata(key string, value string) {
defer client.stateMutex.Unlock()
if client.metadata == nil {
client.metadata = make(MetadataStore)
client.metadata = make(map[string]string)
}
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()
defer client.stateMutex.RUnlock()
@ -984,12 +988,12 @@ func (client *Client) DeleteMetadata(key string) {
delete(client.metadata, key)
}
func (client *Client) ClearMetadata() MetadataStore {
func (client *Client) ClearMetadata() map[string]string {
client.stateMutex.Lock()
defer client.stateMutex.Unlock()
oldMap := client.metadata
client.metadata = make(MetadataStore)
client.metadata = make(map[string]string)
return oldMap
}

View file

@ -9,11 +9,13 @@ package irc
import (
"bytes"
"fmt"
"maps"
"net"
"os"
"runtime"
"runtime/debug"
"runtime/pprof"
"slices"
"sort"
"strconv"
"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
chunked := utils.ChunkifyParams(added, lineLength)
chunked := utils.ChunkifyParams(slices.Values(added), lineLength)
for _, line := range chunked {
params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...)
rb.Add(nil, server.name, RPL_METADATASUBOK, params...)
}
case "unsub":
@ -3260,42 +3262,29 @@ func metadataHandler(server *Server, client *Client, msg ircmsg.Message, rb *Res
removed := rb.session.UnsubscribeFrom(keys...)
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 {
params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...)
rb.Add(nil, server.name, RPL_METADATAUNSUBOK, params...)
}
case "subs":
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 {
params := append([]string{client.Nick()}, line...)
rb.Add(nil, server.name, RPL_METADATASUBS, params...)
}
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 {
for _, client := range targetChannel.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)
}
}
}
syncChannelMetadata(server, rb, targetChannel)
}
if targetClient != nil {
syncClientMetadata(server, rb, targetClient)
}
default:

View file

@ -15,14 +15,12 @@ var (
errMetadataNotFound = errors.New("key not found")
)
type MetadataStore = map[string]string
type MetadataHaver = interface {
SetMetadata(key string, value string)
GetMetadata(key string) (string, error)
DeleteMetadata(key string)
ListMetadata() MetadataStore
ClearMetadata() MetadataStore
ListMetadata() map[string]string
ClearMetadata() map[string]string
CountMetadata() int
}
@ -32,7 +30,7 @@ func notifySubscribers(server *Server, session *Session, target string, key stri
targetClient := server.clients.Get(target)
if targetClient != nil {
notify = targetClient.FriendsMonitors(caps.MetadataTwoJudgementDay)
notify = targetClient.FriendsMonitors(caps.Metadata)
// notify clients about changes regarding themselves
for _, s := range targetClient.Sessions() {
notify.Add(s)
@ -42,7 +40,7 @@ func notifySubscribers(server *Server, session *Session, target string, key stri
members := targetChannel.Members()
for _, m := range members {
for _, s := range m.Sessions() {
if s.capabilities.Has(caps.MetadataTwoJudgementDay) {
if s.capabilities.Has(caps.Metadata) {
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_./:-]+")
func metadataKeyIsEvil(key string) bool {

View file

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