diff --git a/irc/channel.go b/irc/channel.go index a4510557..dd7f7eb1 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -8,7 +8,7 @@ import ( type Channel struct { flags ChannelModeSet - lists map[ChannelMode]UserMaskSet + lists map[ChannelMode]*UserMaskSet key string members MemberSet name string @@ -26,10 +26,10 @@ func IsChannel(target string) bool { func NewChannel(s *Server, name string) *Channel { channel := &Channel{ flags: make(ChannelModeSet), - lists: map[ChannelMode]UserMaskSet{ - BanMask: make(UserMaskSet), - ExceptMask: make(UserMaskSet), - InviteMask: make(UserMaskSet), + lists: map[ChannelMode]*UserMaskSet{ + BanMask: NewUserMaskSet(), + ExceptMask: NewUserMaskSet(), + InviteMask: NewUserMaskSet(), }, members: make(MemberSet), name: strings.ToLower(name), @@ -151,6 +151,19 @@ func (channel *Channel) Join(client *Client, key string) { return } + isInvited := channel.lists[InviteMask].Match(client.UserHost()) + if channel.flags[InviteOnly] && !isInvited { + client.ErrInviteOnlyChan(channel) + return + } + + if channel.lists[BanMask].Match(client.UserHost()) && + !isInvited && + !channel.lists[ExceptMask].Match(client.UserHost()) { + client.ErrBannedFromChan(channel) + return + } + client.channels.Add(channel) channel.members.Add(client) if !channel.flags[Persistent] && (len(channel.members) == 1) { @@ -213,7 +226,7 @@ func (channel *Channel) SetTopic(client *Client, topic string) { } if err := channel.Persist(); err != nil { - log.Println(err) + log.Println("Channel.Persist:", channel, err) } } @@ -310,15 +323,35 @@ func (channel *Channel) applyModeMember(client *Client, mode ChannelMode, return false } +func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeOp, mask string) bool { + if !channel.ClientIsOperator(client) { + client.ErrChanOPrivIsNeeded(channel) + return false + } + + list := channel.lists[mode] + if list == nil { + // This should never happen, but better safe than panicky. + return false + } + + if op == Add { + list.Add(mask) + } else if op == Remove { + list.Remove(mask) + } + + for lmask := range channel.lists[mode].masks { + client.RplMaskList(mode, channel, lmask) + } + client.RplEndOfMaskList(mode, channel) + return true +} + func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) bool { switch change.mode { case BanMask, ExceptMask, InviteMask: - // TODO add/remove - - for mask := range channel.lists[change.mode] { - client.RplMaskList(change.mode, channel, mask) - } - client.RplEndOfMaskList(change.mode, channel) + return channel.applyModeMask(client, change.mode, change.op, change.arg) case Moderated, NoOutside, OpOnlyTopic, Persistent, Private: return channel.applyModeFlag(client, change.mode, change.op) @@ -390,7 +423,7 @@ func (channel *Channel) Mode(client *Client, changes ChannelModeChanges) { } if err := channel.Persist(); err != nil { - log.Println(err) + log.Println("Channel.Persist:", channel, err) } } } @@ -464,6 +497,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) { return } + if channel.flags[InviteOnly] { + channel.lists[InviteMask].Add(invitee.UserHost()) + if err := channel.Persist(); err != nil { + log.Println("Channel.Persist:", channel, err) + } + } + inviter.RplInviting(invitee, channel.name) invitee.Reply(RplInviteMsg(inviter, invitee, channel.name)) if invitee.flags[Away] { diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 2661c2ef..6c66d1da 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -178,3 +178,60 @@ func (db *ClientDB) Remove(client *Client) { } } } + +// +// usermask to regexp +// + +type UserMaskSet struct { + masks map[string]bool + regexp *regexp.Regexp +} + +func NewUserMaskSet() *UserMaskSet { + return &UserMaskSet{ + masks: make(map[string]bool), + } +} + +func (set *UserMaskSet) Add(mask string) { + set.masks[mask] = true + set.setRegexp() +} + +func (set *UserMaskSet) Remove(mask string) { + delete(set.masks, mask) + set.setRegexp() +} + +func (set *UserMaskSet) Match(userhost string) bool { + if set.regexp == nil { + return false + } + return set.regexp.MatchString(userhost) +} + +func (set *UserMaskSet) setRegexp() { + if len(set.masks) == 0 { + set.regexp = nil + return + } + + maskExprs := make([]string, len(set.masks)) + index := 0 + for mask := range set.masks { + manyParts := strings.Split(mask, "*") + manyExprs := make([]string, len(manyParts)) + for mindex, manyPart := range manyParts { + oneParts := strings.Split(manyPart, "?") + oneExprs := make([]string, len(oneParts)) + for oindex, onePart := range oneParts { + oneExprs[oindex] = regexp.QuoteMeta(onePart) + } + manyExprs[mindex] = strings.Join(oneExprs, ".") + } + maskExprs[index] = strings.Join(manyExprs, ".*") + } + expr := "^" + strings.Join(maskExprs, "|") + "$" + set.regexp, _ = regexp.Compile(expr) +} diff --git a/irc/reply.go b/irc/reply.go index 00d24143..7adb63fe 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -541,3 +541,13 @@ func (target *Client) ErrInvalidCapCmd(subCommand CapSubCommand) { target.NumericReply(ERR_INVALIDCAPCMD, "%s :Invalid CAP subcommand", subCommand) } + +func (target *Client) ErrBannedFromChan(channel *Channel) { + target.NumericReply(ERR_BANNEDFROMCHAN, + "%s :Cannot join channel (+b)", channel) +} + +func (target *Client) ErrInviteOnlyChan(channel *Channel) { + target.NumericReply(ERR_INVITEONLYCHAN, + "%s :Cannot join channel (+i)", channel) +} diff --git a/irc/types.go b/irc/types.go index 60557e8f..e9e7a98b 100644 --- a/irc/types.go +++ b/irc/types.go @@ -9,8 +9,6 @@ import ( // simple types // -type UserMaskSet map[string]bool - type CapSubCommand string type Capability string