Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@thatcher-gaming
Copy link
Contributor

adds support for most of the draft/metadata-2 spec

fixes #1745

what's new:

  • a new draft/metadata-2 cap
  • the metadata command, with SET, GET, CLEAR, LIST, SUB, UNSUB, SUBS & SYNC
  • a metadata section in the config

what's missing:

  • before-connect
  • visibility controls. as of right now everything is *
  • any attempt at rate limiting
  • postponed synchronisation

this is a draft because i still need to write some tests but i thought i'd open a pr so someone can tell me if i've done something egregiously wrong (this is my first time doing anything particularly substantial in go (!!!!))

@slingamn
Copy link
Member

Thank you so much, this is delightful!

I'll let @progval weigh in on correctness w.r.t. the specification.

I have a brief implementation note: don't write any new code referencing buntdb. Store everything (the metadata itself, the subscription list) as members of the Client (not Session) and Channel structs, protected by the stateMutex locks in those structs. Use those members as the source of truth for all runtime reads and writes.

I am in the process of refactoring the account system to have asynchronous persistence, but the corresponding refactor for channels is already done (#2028), so if you want to persist channel metadata, here's how you do it:

  • Add a Metadata member to RegisteredChannel (this is the struct that gets serialized to the database for registered channels). Add fields that copy the data to (*Channel).applyRegInfo and (*Channel).ExportRegistration (the first copies from RegisteredChannel to Channel, the second in the reverse direction).
  • After updating channel metadata, call channel.MarkDirty(IncludeAllAttrs).

As for clients/accounts, don't write any persistence code yet, I will add it as part of the accounts refactor :-)

@slingamn
Copy link
Member

Ah, I think I was wrong earlier: if the expectation is that metadata subscriptions have to be refreshed by the client on every reconnection, then the correct place to store them is indeed the Session object (and they don't persist).

Copy link
Contributor

@progval progval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, thanks.

First wave of reviews from me, needed so I can run the rest of my tests.

Copy link
Contributor

@progval progval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another round of reviews before I go to bed

noKeyPerms(key)
return
}

Copy link
Contributor

@progval progval Jun 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@slingamn Should we check the key is small enough that it's not going to be truncated when sent to other clients? (METADATA * SET display-name :abc is shorter than :irc.example.com METADATA user1 display-name * :abc)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this addressed in the spec at all?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I support adding a preemptive sanity check here, but we probably shouldn't do arithmetic with the server name length, I think it should be more like "key + value are less than 350 bytes"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of course not, this is IRC :)

@thatcher-gaming
Copy link
Contributor Author

thatcher-gaming commented Jun 13, 2025

After updating channel metadata, call channel.MarkDirty(IncludeAllAttrs).

@slingamn This is now in, but there's a problem: calling this during set seems to make the function hang forever. I do not know why this is.

@thatcher-gaming
Copy link
Contributor Author

Oh and @progval, I'm finding this bit of the spec somewhat confusing:

If the target is a channel, it also syncs the metadata for all other users in that channel

My sleep deprived brain interpreted it as "send information about the channel to each user in the channel", but I now realise it's much more likely to mean "send the information about all the channel's participants to the client". I am not a smart person and have no idea if this would actually be a common interpretation, so feel free to disregard.

Copy link
Member

@slingamn slingamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice. @progval can hook you up with the unreleased integration tests for this

irc/metadata.go Outdated

type MetadataStore = map[string]string

type MetadataThing = struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dead code now....I don't think we're going to implement visibility as part of the MVP for this so we can just use map[string]string for now

irc/metadata.go Outdated
members := targetChannel.Members()
for _, m := range members {
for _, s := range m.Sessions() {
notify.Add(s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the cap check up here...

If you want to get adventurous you can actually use iter.Seq here, so the client case will still allocate a hashset of friend sessions (because we need to deduplicate), but the channel case can just iterate over Members() and then Sessions() per member and return applicable sessions without allocating, and then iter.Seq is a common interface for ranging over both of these.

@progval
Copy link
Contributor

progval commented Jun 13, 2025

Oh and @progval, I'm finding this bit of the spec somewhat confusing:

If the target is a channel, it also syncs the metadata for all other users in that channel

My sleep deprived brain interpreted it as "send information about the channel to each user in the channel", but I now realise it's much more likely to mean "send the information about all the channel's participants to the client". I am not a smart person and have no idea if this would actually be a common interpretation, so feel free to disregard.

that sounds right

@thatcher-gaming thatcher-gaming marked this pull request as ready for review June 13, 2025 17:12
@thatcher-gaming
Copy link
Contributor Author

I'm 75% sure this is ready for primetime now. Let me know if something goes wrong!!

@thatcher-gaming thatcher-gaming changed the title WIP: metadata-2 metadata-2 Jun 13, 2025
@slingamn slingamn mentioned this pull request Jun 13, 2025
12 tasks
Copy link
Member

@slingamn slingamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is getting very close to passing Val's tests

@slingamn
Copy link
Member

@thatcher-gaming can you add this functionality?

On joining a channel, users will get the channel’s current metadata sent to them with METADATA messages, and get the same information for all users who are in the channel. Specifically, they get that information for the keys they are subscribed to. The server may also tell them to request that information at a later time.

@thatcher-gaming thatcher-gaming force-pushed the master branch 2 times, most recently from d025e39 to 0cf66d7 Compare June 13, 2025 19:03
Copy link
Member

@slingamn slingamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! I'll leave this open while we do more testing.

irc/getters.go Outdated
Comment on lines 836 to 838
if session.metadataSubscriptions == nil {
return false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can delete this (reads and deletes from the nil map in Go are no-ops like an empty map --- stores will panic)

irc/getters.go Outdated
Comment on lines 874 to 876
if session.metadataSubscriptions == nil {
return []string{}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete as per above

Comment on lines +909 to +911
if channel.metadata == nil {
channel.metadata = make(map[string]string)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete as per above

irc/getters.go Outdated
Comment on lines 948 to 950
if channel.metadata == nil {
return 0
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete

irc/getters.go Outdated
channel.stateMutex.Lock()

oldMap := channel.metadata
channel.metadata = make(map[string]string)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

channel.metadata = nil is OK here

irc/getters.go Outdated
channel.metadata = make(map[string]string)

channel.stateMutex.Unlock()
channel.MarkDirty(IncludeAllAttrs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have PTSD from #2149 so my preferred style for all locking is to unlock in a defer, something like this:

defer channel.MarkDirty(IncludeAllAttrs)
channel.stateMutex.Lock()
defer channel.stateMutex.Unlock()
// do the write operation

irc/getters.go Outdated
delete(channel.metadata, key)

channel.stateMutex.Unlock()
channel.MarkDirty(IncludeAllAttrs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see note below about defer

irc/getters.go Outdated
return maps.Clone(session.metadataSubscriptions)
}

func (channel *Channel) GetMetadata(key string) (string, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would return (string, bool) here (the value and ok) --- makes it clearer how to exhaustively handle the success and failure conditions

Copy link
Member

@slingamn slingamn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great from an implementation standpoint! I'd like to get another look from @progval on correctness w.r.t. the spec.

This also passes our updated test suite at https://github.com/progval/irctest/tree/metadata2 as mentioned.

},
"metadata": {
text: `METADATA <target> <subcommand> [<everything else>...]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extra whitespace on this line, should be blank

@slingamn
Copy link
Member

I'm going to merge this so I can work on some follow-up tasks. Thanks for the great PR, @thatcher-gaming !

@slingamn slingamn merged commit 4dcbc48 into ergochat:master Jun 15, 2025
1 check passed
@thatcher-gaming
Copy link
Contributor Author

thanks for all the feedback! sorry about my slightly amateur go knowledge haha

@slingamn slingamn added this to the v2.17 milestone Jun 16, 2025
@slingamn slingamn added IRCv3 new cap New client cap labels Jun 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

IRCv3 new cap New client cap

Projects

None yet

Development

Successfully merging this pull request may close these issues.

METADATA / property / taxonomy

3 participants