mirror of
https://github.com/ergochat/ergo.git
synced 2025-06-22 06:37:30 +02:00
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
This commit is contained in:
parent
4dcbc48159
commit
3b7db7fff7
10
default.yaml
10
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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
141
irc/handlers.go
141
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 <target> <subcommand> [<and so on>...]
|
||||
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()
|
||||
|
@ -342,7 +342,7 @@ specification.`,
|
||||
},
|
||||
"metadata": {
|
||||
text: `METADATA <target> <subcommand> [<everything else>...]
|
||||
|
||||
|
||||
Retrieve and meddle with metadata for the given target.
|
||||
Have a look at https://ircv3.net/specs/extensions/metadata for interesting technical information.`,
|
||||
},
|
||||
|
139
irc/metadata.go
139
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
2
irctest
2
irctest
@ -1 +1 @@
|
||||
Subproject commit 15c847ebb19b1c434347236296cbc4d34ba7c4b2
|
||||
Subproject commit a3c2110be24ad53ec4babe0facdc25aa2151d3e5
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user