Enforce channel keys, invites, +s and +H on the IRC listener (closes #122)
check / check (push) Waiting to run

The IRC listener's JOIN now passes the channel key on, INVITE records
the invite, LIST and WHOIS hide +s channels from non-members with the
queries the HTTP API uses, and PRIVMSG to a +H channel gets 404.

The INVITE checks and the channel hashcash check move from the HTTP
handlers into the service, so both transports run the same code. An IRC
client cannot attach a stamp, so its PRIVMSG to a +H channel always
fails that check. The unfiltered channel list query, now unused, is
removed.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #139.
This commit is contained in:
2026-10-20 16:02:14 +02:00
committed by clawbot
parent 1308769a25
commit 3c725a9ca8
7 changed files with 494 additions and 450 deletions
+28 -39
View File
@@ -150,7 +150,9 @@ func (c *Conn) handlePrivmsg(
}
// handleJoin joins one or more channels via the shared
// service layer.
// service layer. The optional second parameter holds the
// channel keys, comma-separated in the order of the
// channels.
func (c *Conn) handleJoin(
ctx context.Context,
msg *Message,
@@ -164,14 +166,24 @@ func (c *Conn) handleJoin(
return
}
for chanName := range strings.SplitSeq(msg.Params[0], ",") {
var keys []string
if len(msg.Params) > 1 {
keys = strings.Split(msg.Params[1], ",")
}
for idx, chanName := range strings.Split(msg.Params[0], ",") {
chanName = strings.TrimSpace(chanName)
if !strings.HasPrefix(chanName, "#") {
chanName = "#" + chanName
}
c.joinChannel(ctx, chanName)
key := ""
if idx < len(keys) {
key = keys[idx]
}
c.joinChannel(ctx, chanName, key)
}
}
@@ -179,10 +191,10 @@ func (c *Conn) handleJoin(
// and delivers topic/names on the wire.
func (c *Conn) joinChannel(
ctx context.Context,
channel string,
channel, key string,
) {
result, err := c.svc.JoinChannel(
ctx, c.sessionID, c.nick, channel, "",
ctx, c.sessionID, c.nick, channel, key,
)
if err != nil {
c.sendIRCError(err)
@@ -763,10 +775,11 @@ func (c *Conn) handleNames(
c.deliverNames(ctx, channel, chID)
}
// handleList sends the channel list.
// handleList sends the channel list, leaving out +s
// channels the user is not on.
func (c *Conn) handleList(ctx context.Context) {
channels, err := c.database.ListAllChannelsWithCounts(
ctx,
channels, err := c.database.ListAllChannelsWithCountsFiltered(
ctx, c.sessionID,
)
if err != nil {
c.sendNumeric(
@@ -886,14 +899,15 @@ func (c *Conn) whoisOper(
}
}
// whoisChannels sends 319 RPL_WHOISCHANNELS.
// whoisChannels sends 319 RPL_WHOISCHANNELS, leaving out
// +s channels the querier is not on.
func (c *Conn) whoisChannels(
ctx context.Context,
target string,
targetID int64,
) {
userChannels, _ := c.database.GetSessionChannels(
ctx, targetID,
userChannels, _ := c.database.GetSessionChannelsFiltered(
ctx, targetID, c.sessionID,
)
if len(userChannels) == 0 {
return
@@ -1230,36 +1244,11 @@ func (c *Conn) handleInvite(
targetNick := msg.Params[0]
channel := msg.Params[1]
chID, err := c.database.GetChannelByName(ctx, channel)
if err != nil {
c.sendNumeric(
irc.ErrNoSuchChannel,
channel, "No such channel",
)
return
}
isMember, _ := c.database.IsChannelMember(
ctx, chID, c.sessionID,
)
if !isMember {
c.sendNumeric(
irc.ErrNotOnChannel,
channel, "You're not on that channel",
)
return
}
targetID, err := c.database.GetSessionByNick(
ctx, targetNick,
targetID, err := c.svc.InviteUser(
ctx, c.sessionID, c.nick, targetNick, channel,
)
if err != nil {
c.sendNumeric(
irc.ErrNoSuchNick,
targetNick, "No such nick",
)
c.sendIRCError(err)
return
}