3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-13 07:29:30 +01:00

Merge pull request #650 from slingamn/issue644_bans.5

two mode-related fixes
This commit is contained in:
Shivaram Lingamneni 2019-10-22 23:14:13 -07:00 committed by GitHub
commit 607da61bf9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 228 additions and 128 deletions

View File

@ -124,18 +124,12 @@ func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) {
for _, mode := range chanReg.Modes { for _, mode := range chanReg.Modes {
channel.flags.SetMode(mode, true) channel.flags.SetMode(mode, true)
} }
for _, mask := range chanReg.Banlist {
channel.lists[modes.BanMask].Add(mask)
}
for _, mask := range chanReg.Exceptlist {
channel.lists[modes.ExceptMask].Add(mask)
}
for _, mask := range chanReg.Invitelist {
channel.lists[modes.InviteMask].Add(mask)
}
for account, mode := range chanReg.AccountToUMode { for account, mode := range chanReg.AccountToUMode {
channel.accountToUMode[account] = mode channel.accountToUMode[account] = mode
} }
channel.lists[modes.BanMask].SetMasks(chanReg.Bans)
channel.lists[modes.InviteMask].SetMasks(chanReg.Invites)
channel.lists[modes.ExceptMask].SetMasks(chanReg.Excepts)
} }
// obtain a consistent snapshot of the channel state that can be persisted to the DB // obtain a consistent snapshot of the channel state that can be persisted to the DB
@ -160,15 +154,9 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
} }
if includeFlags&IncludeLists != 0 { if includeFlags&IncludeLists != 0 {
for mask := range channel.lists[modes.BanMask].masks { info.Bans = channel.lists[modes.BanMask].Masks()
info.Banlist = append(info.Banlist, mask) info.Invites = channel.lists[modes.InviteMask].Masks()
} info.Excepts = channel.lists[modes.ExceptMask].Masks()
for mask := range channel.lists[modes.ExceptMask].masks {
info.Exceptlist = append(info.Exceptlist, mask)
}
for mask := range channel.lists[modes.InviteMask].masks {
info.Invitelist = append(info.Invitelist, mask)
}
info.AccountToUMode = make(map[string]modes.Mode) info.AccountToUMode = make(map[string]modes.Mode)
for account, mode := range channel.accountToUMode { for account, mode := range channel.accountToUMode {
info.AccountToUMode[account] = mode info.AccountToUMode[account] = mode
@ -1097,14 +1085,12 @@ func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode, rb *Respon
} }
nick := client.Nick() nick := client.Nick()
channel.stateMutex.RLock() chname := channel.Name()
// XXX don't acquire any new locks in this section, besides Socket.Write for mask, info := range channel.lists[mode].Masks() {
for mask := range channel.lists[mode].masks { rb.Add(nil, client.server.name, rpllist, nick, chname, mask, info.CreatorNickmask, strconv.FormatInt(info.TimeCreated.Unix(), 10))
rb.Add(nil, client.server.name, rpllist, nick, channel.name, mask)
} }
channel.stateMutex.RUnlock()
rb.Add(nil, client.server.name, rplendoflist, nick, channel.name, client.t("End of list")) rb.Add(nil, client.server.name, rplendoflist, nick, chname, client.t("End of list"))
} }
// Quit removes the given client from the channel // Quit removes the given client from the channel

View File

@ -88,12 +88,12 @@ type RegisteredChannel struct {
Key string Key string
// AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h)
AccountToUMode map[string]modes.Mode AccountToUMode map[string]modes.Mode
// Banlist represents the bans set on the channel. // Bans represents the bans set on the channel.
Banlist []string Bans map[string]MaskInfo
// Exceptlist represents the exceptions set on the channel. // Excepts represents the exceptions set on the channel.
Exceptlist []string Excepts map[string]MaskInfo
// Invitelist represents the invite exceptions set on the channel. // Invites represents the invite exceptions set on the channel.
Invitelist []string Invites map[string]MaskInfo
} }
// ChannelRegistry manages registered channels. // ChannelRegistry manages registered channels.
@ -180,11 +180,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
modeSlice[i] = modes.Mode(mode) modeSlice[i] = modes.Mode(mode)
} }
var banlist []string var banlist map[string]MaskInfo
_ = json.Unmarshal([]byte(banlistString), &banlist) _ = json.Unmarshal([]byte(banlistString), &banlist)
var exceptlist []string var exceptlist map[string]MaskInfo
_ = json.Unmarshal([]byte(exceptlistString), &exceptlist) _ = json.Unmarshal([]byte(exceptlistString), &exceptlist)
var invitelist []string var invitelist map[string]MaskInfo
_ = json.Unmarshal([]byte(invitelistString), &invitelist) _ = json.Unmarshal([]byte(invitelistString), &invitelist)
accountToUMode := make(map[string]modes.Mode) accountToUMode := make(map[string]modes.Mode)
_ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode) _ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode)
@ -198,9 +198,9 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
TopicSetTime: time.Unix(topicSetTimeInt, 0), TopicSetTime: time.Unix(topicSetTimeInt, 0),
Key: password, Key: password,
Modes: modeSlice, Modes: modeSlice,
Banlist: banlist, Bans: banlist,
Exceptlist: exceptlist, Excepts: exceptlist,
Invitelist: invitelist, Invites: invitelist,
AccountToUMode: accountToUMode, AccountToUMode: accountToUMode,
} }
return nil return nil
@ -296,11 +296,11 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
} }
if includeFlags&IncludeLists != 0 { if includeFlags&IncludeLists != 0 {
banlistString, _ := json.Marshal(channelInfo.Banlist) banlistString, _ := json.Marshal(channelInfo.Bans)
tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil)
exceptlistString, _ := json.Marshal(channelInfo.Exceptlist) exceptlistString, _ := json.Marshal(channelInfo.Excepts)
tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil)
invitelistString, _ := json.Marshal(channelInfo.Invitelist) invitelistString, _ := json.Marshal(channelInfo.Invites)
tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil)
accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode) accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode)
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil) tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)

View File

@ -5,10 +5,9 @@
package irc package irc
import ( import (
"fmt"
"log"
"regexp" "regexp"
"strings" "strings"
"time"
"github.com/goshuirc/irc-go/ircmatch" "github.com/goshuirc/irc-go/ircmatch"
@ -251,63 +250,62 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
//TODO(dan): move this over to generally using glob syntax instead? //TODO(dan): move this over to generally using glob syntax instead?
// kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?) // kinda more expected in normal ban/etc masks, though regex is useful (probably as an extban?)
type MaskInfo struct {
TimeCreated time.Time
CreatorNickmask string
CreatorAccount string
}
// UserMaskSet holds a set of client masks and lets you match hostnames to them. // UserMaskSet holds a set of client masks and lets you match hostnames to them.
type UserMaskSet struct { type UserMaskSet struct {
sync.RWMutex sync.RWMutex
masks map[string]bool masks map[string]MaskInfo
regexp *regexp.Regexp regexp *regexp.Regexp
} }
// NewUserMaskSet returns a new UserMaskSet.
func NewUserMaskSet() *UserMaskSet { func NewUserMaskSet() *UserMaskSet {
return &UserMaskSet{ return new(UserMaskSet)
masks: make(map[string]bool),
}
} }
// Add adds the given mask to this set. // Add adds the given mask to this set.
func (set *UserMaskSet) Add(mask string) (added bool) { func (set *UserMaskSet) Add(mask, creatorNickmask, creatorAccount string) (maskAdded string, err error) {
casefoldedMask, err := Casefold(mask) casefoldedMask, err := CanonicalizeMaskWildcard(mask)
if err != nil { if err != nil {
log.Println(fmt.Sprintf("ERROR: Could not add mask to usermaskset: [%s]", mask)) return
return false
} }
set.Lock() set.Lock()
added = !set.masks[casefoldedMask] if set.masks == nil {
if added { set.masks = make(map[string]MaskInfo)
set.masks[casefoldedMask] = true }
_, present := set.masks[casefoldedMask]
if !present {
maskAdded = casefoldedMask
set.masks[casefoldedMask] = MaskInfo{
TimeCreated: time.Now().UTC(),
CreatorNickmask: creatorNickmask,
CreatorAccount: creatorAccount,
}
} }
set.Unlock() set.Unlock()
if added { if !present {
set.setRegexp()
}
return
}
// AddAll adds the given masks to this set.
func (set *UserMaskSet) AddAll(masks []string) (added bool) {
set.Lock()
defer set.Unlock()
for _, mask := range masks {
if !added && !set.masks[mask] {
added = true
}
set.masks[mask] = true
}
if added {
set.setRegexp() set.setRegexp()
} }
return return
} }
// Remove removes the given mask from this set. // Remove removes the given mask from this set.
func (set *UserMaskSet) Remove(mask string) (removed bool) { func (set *UserMaskSet) Remove(mask string) (maskRemoved string, err error) {
mask, err = CanonicalizeMaskWildcard(mask)
if err != nil {
return
}
set.Lock() set.Lock()
removed = set.masks[mask] _, removed := set.masks[mask]
if removed { if removed {
maskRemoved = mask
delete(set.masks, mask) delete(set.masks, mask)
} }
set.Unlock() set.Unlock()
@ -318,6 +316,24 @@ func (set *UserMaskSet) Remove(mask string) (removed bool) {
return return
} }
func (set *UserMaskSet) SetMasks(masks map[string]MaskInfo) {
set.Lock()
set.masks = masks
set.Unlock()
set.setRegexp()
}
func (set *UserMaskSet) Masks() (result map[string]MaskInfo) {
set.RLock()
defer set.RUnlock()
result = make(map[string]MaskInfo, len(set.masks))
for mask, info := range set.masks {
result[mask] = info
}
return
}
// Match matches the given n!u@h. // Match matches the given n!u@h.
func (set *UserMaskSet) Match(userhost string) bool { func (set *UserMaskSet) Match(userhost string) bool {
set.RLock() set.RLock()
@ -330,19 +346,6 @@ func (set *UserMaskSet) Match(userhost string) bool {
return regexp.MatchString(userhost) return regexp.MatchString(userhost)
} }
// String returns the masks in this set.
func (set *UserMaskSet) String() string {
set.RLock()
masks := make([]string, len(set.masks))
index := 0
for mask := range set.masks {
masks[index] = mask
index++
}
set.RUnlock()
return strings.Join(masks, " ")
}
func (set *UserMaskSet) Length() int { func (set *UserMaskSet) Length() int {
set.RLock() set.RLock()
defer set.RUnlock() defer set.RUnlock()

View File

@ -22,7 +22,7 @@ const (
// 'version' of the database schema // 'version' of the database schema
keySchemaVersion = "db.version" keySchemaVersion = "db.version"
// latest schema of the db // latest schema of the db
latestDbSchema = "6" latestDbSchema = "7"
) )
type SchemaChanger func(*Config, *buntdb.Tx) error type SchemaChanger func(*Config, *buntdb.Tx) error
@ -440,6 +440,66 @@ func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
return nil return nil
} }
type maskInfoV7 struct {
TimeCreated time.Time
CreatorNickmask string
CreatorAccount string
}
func schemaChangeV6ToV7(config *Config, tx *buntdb.Tx) error {
now := time.Now().UTC()
var channels []string
prefix := "channel.exists "
tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
if !strings.HasPrefix(key, prefix) {
return false
}
channels = append(channels, strings.TrimPrefix(key, prefix))
return true
})
converter := func(key string) {
oldRawValue, err := tx.Get(key)
if err != nil {
return
}
var masks []string
err = json.Unmarshal([]byte(oldRawValue), &masks)
if err != nil {
return
}
newCookedValue := make(map[string]maskInfoV7)
for _, mask := range masks {
normalizedMask, err := CanonicalizeMaskWildcard(mask)
if err != nil {
continue
}
newCookedValue[normalizedMask] = maskInfoV7{
TimeCreated: now,
CreatorNickmask: "*",
CreatorAccount: "*",
}
}
newRawValue, err := json.Marshal(newCookedValue)
if err != nil {
return
}
tx.Set(key, string(newRawValue), nil)
}
prefixes := []string{
"channel.banlist %s",
"channel.exceptlist %s",
"channel.invitelist %s",
}
for _, channel := range channels {
for _, prefix := range prefixes {
converter(fmt.Sprintf(prefix, channel))
}
}
return nil
}
func init() { func init() {
allChanges := []SchemaChange{ allChanges := []SchemaChange{
{ {
@ -467,6 +527,11 @@ func init() {
TargetVersion: "6", TargetVersion: "6",
Changer: schemaChangeV5ToV6, Changer: schemaChangeV5ToV6,
}, },
{
InitialVersion: "6",
TargetVersion: "7",
Changer: schemaChangeV6ToV7,
},
} }
// build the index // build the index

View File

@ -1685,13 +1685,12 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
return false return false
} }
// applied mode changes var changes modes.ModeChanges
applied := make(modes.ModeChanges, 0)
if 1 < len(msg.Params) { if 1 < len(msg.Params) {
// parse out real mode changes // parse out real mode changes
params := msg.Params[1:] params := msg.Params[1:]
changes, unknown := modes.ParseChannelModeChanges(params...) var unknown map[rune]bool
changes, unknown = modes.ParseChannelModeChanges(params...)
// alert for unknown mode changes // alert for unknown mode changes
for char := range unknown { for char := range unknown {
@ -1700,10 +1699,9 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
if len(unknown) == 1 && len(changes) == 0 { if len(unknown) == 1 && len(changes) == 0 {
return false return false
} }
// apply mode changes
applied = channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb)
} }
// process mode changes, include list operations (an empty set of changes does a list)
applied := channel.ApplyChannelModeChanges(client, msg.Command == "SAMODE", changes, rb)
// save changes // save changes
var includeFlags uint var includeFlags uint
@ -1719,8 +1717,8 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
} }
// send out changes // send out changes
prefix := client.NickMaskString()
if len(applied) > 0 { if len(applied) > 0 {
prefix := client.NickMaskString()
//TODO(dan): we should change the name of String and make it return a slice here //TODO(dan): we should change the name of String and make it return a slice here
args := append([]string{channel.name}, strings.Split(applied.String(), " ")...) args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
for _, member := range channel.Members() { for _, member := range channel.Members() {
@ -1735,10 +1733,6 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
member.Send(nil, prefix, "MODE", args...) member.Send(nil, prefix, "MODE", args...)
} }
} }
} else {
args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...)
rb.Add(nil, prefix, RPL_CHANNELMODEIS, args...)
rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10))
} }
return false return false
} }

View File

@ -6,6 +6,7 @@
package irc package irc
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
@ -104,17 +105,15 @@ func ParseDefaultChannelModes(rawModes *string) modes.Modes {
} }
// ApplyChannelModeChanges applies a given set of mode changes. // ApplyChannelModeChanges applies a given set of mode changes.
func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes modes.ModeChanges, rb *ResponseBuffer) modes.ModeChanges { func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes modes.ModeChanges, rb *ResponseBuffer) (applied modes.ModeChanges) {
// so we only output one warning for each list type when full // so we only output one warning for each list type when full
listFullWarned := make(map[modes.Mode]bool) listFullWarned := make(map[modes.Mode]bool)
var alreadySentPrivError bool var alreadySentPrivError bool
applied := make(modes.ModeChanges, 0) maskOpCount := 0
chname := channel.Name()
isListOp := func(change modes.ModeChange) bool { details := client.Details()
return (change.Op == modes.List) || (change.Arg == "")
}
hasPrivs := func(change modes.ModeChange) bool { hasPrivs := func(change modes.ModeChange) bool {
if isSamode { if isSamode {
@ -127,18 +126,19 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
return true return true
} }
cfarg, _ := CasefoldName(change.Arg) cfarg, _ := CasefoldName(change.Arg)
isSelfChange := cfarg == client.NickCasefolded() isSelfChange := cfarg == details.nickCasefolded
if change.Op == modes.Remove && isSelfChange { if change.Op == modes.Remove && isSelfChange {
// "There is no restriction, however, on anyone `deopping' themselves" // "There is no restriction, however, on anyone `deopping' themselves"
// <https://tools.ietf.org/html/rfc2812#section-3.1.5> // <https://tools.ietf.org/html/rfc2812#section-3.1.5>
return true return true
} }
return channelUserModeHasPrivsOver(channel.HighestUserMode(client), change.Mode) return channelUserModeHasPrivsOver(channel.HighestUserMode(client), change.Mode)
case modes.BanMask: case modes.InviteMask, modes.ExceptMask:
// #163: allow unprivileged users to list ban masks // listing these requires privileges
return isListOp(change) || channel.ClientIsAtLeast(client, modes.ChannelOperator)
default:
return channel.ClientIsAtLeast(client, modes.ChannelOperator) return channel.ClientIsAtLeast(client, modes.ChannelOperator)
default:
// #163: allow unprivileged users to list ban masks, and any other modes
return change.Op == modes.List || channel.ClientIsAtLeast(client, modes.ChannelOperator)
} }
} }
@ -146,40 +146,52 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
if !hasPrivs(change) { if !hasPrivs(change) {
if !alreadySentPrivError { if !alreadySentPrivError {
alreadySentPrivError = true alreadySentPrivError = true
rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.name, client.t("You're not a channel operator")) rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, details.nick, channel.name, client.t("You're not a channel operator"))
} }
continue continue
} }
switch change.Mode { switch change.Mode {
case modes.BanMask, modes.ExceptMask, modes.InviteMask: case modes.BanMask, modes.ExceptMask, modes.InviteMask:
if isListOp(change) { maskOpCount += 1
if change.Op == modes.List {
channel.ShowMaskList(client, change.Mode, rb) channel.ShowMaskList(client, change.Mode, rb)
continue continue
} }
// confirm mask looks valid mask := change.Arg
mask, err := Casefold(change.Arg)
if err != nil {
continue
}
switch change.Op { switch change.Op {
case modes.Add: case modes.Add:
if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes { if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes {
if !listFullWarned[change.Mode] { if !listFullWarned[change.Mode] {
rb.Add(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full")) rb.Add(nil, client.server.name, ERR_BANLISTFULL, details.nick, chname, change.Mode.String(), client.t("Channel list is full"))
listFullWarned[change.Mode] = true listFullWarned[change.Mode] = true
} }
continue continue
} }
channel.lists[change.Mode].Add(mask) maskAdded, err := channel.lists[change.Mode].Add(mask, details.nickMask, details.accountName)
applied = append(applied, change) if maskAdded != "" {
appliedChange := change
appliedChange.Arg = maskAdded
applied = append(applied, appliedChange)
} else if err != nil {
rb.Add(nil, client.server.name, ERR_INVALIDMODEPARAM, details.nick, mask, fmt.Sprintf(client.t("Invalid mode %s parameter: %s"), string(change.Mode), mask))
} else {
rb.Add(nil, client.server.name, ERR_LISTMODEALREADYSET, chname, mask, string(change.Mode), fmt.Sprintf(client.t("Channel %s list already contains %s"), chname, mask))
}
case modes.Remove: case modes.Remove:
channel.lists[change.Mode].Remove(mask) maskRemoved, err := channel.lists[change.Mode].Remove(mask)
applied = append(applied, change) if maskRemoved != "" {
appliedChange := change
appliedChange.Arg = maskRemoved
applied = append(applied, appliedChange)
} else if err != nil {
rb.Add(nil, client.server.name, ERR_INVALIDMODEPARAM, details.nick, mask, fmt.Sprintf(client.t("Invalid mode %s parameter: %s"), string(change.Mode), mask))
} else {
rb.Add(nil, client.server.name, ERR_LISTMODENOTSET, chname, mask, string(change.Mode), fmt.Sprintf(client.t("Channel %s list does not contain %s"), chname, mask))
}
} }
case modes.UserLimit: case modes.UserLimit:
@ -223,7 +235,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
nick := change.Arg nick := change.Arg
if nick == "" { if nick == "" {
rb.Add(nil, client.server.name, ERR_NEEDMOREPARAMS, client.Nick(), "MODE", client.t("Not enough parameters")) rb.Add(nil, client.server.name, ERR_NEEDMOREPARAMS, client.Nick(), "MODE", client.t("Not enough parameters"))
return nil continue
} }
change := channel.applyModeToMember(client, change.Mode, change.Op, nick, rb) change := channel.applyModeToMember(client, change.Mode, change.Op, nick, rb)
@ -233,6 +245,13 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
} }
} }
// #649: don't send 324 RPL_CHANNELMODEIS if we were only working with mask lists
if len(applied) == 0 && !alreadySentPrivError && (maskOpCount == 0 || maskOpCount < len(changes)) {
args := append([]string{details.nick, chname}, channel.modeStrings(client)...)
rb.Add(nil, client.server.name, RPL_CHANNELMODEIS, args...)
rb.Add(nil, client.server.name, RPL_CREATIONTIME, details.nick, chname, strconv.FormatInt(channel.createdTime.Unix(), 10))
}
return applied return applied
} }

View File

@ -47,6 +47,19 @@ func TestParseChannelModeChanges(t *testing.T) {
if len(modes) != 1 || modes[0] != expected { if len(modes) != 1 || modes[0] != expected {
t.Errorf("unexpected mode change: %v", modes) t.Errorf("unexpected mode change: %v", modes)
} }
modes, unknown = ParseChannelModeChanges("+b")
if len(unknown) > 0 {
t.Errorf("unexpected unknown mode change: %v", unknown)
}
// +b with no argument becomes a list operation
expectedChanges := ModeChanges{{
Op: List,
Mode: BanMask,
}}
if !reflect.DeepEqual(modes, expectedChanges) {
t.Errorf("unexpected mode change: %v instead of %v", modes, expectedChanges)
}
} }
func TestSetMode(t *testing.T) { func TestSetMode(t *testing.T) {

View File

@ -70,7 +70,7 @@ const (
RPL_LISTEND = "323" RPL_LISTEND = "323"
RPL_CHANNELMODEIS = "324" RPL_CHANNELMODEIS = "324"
RPL_UNIQOPIS = "325" RPL_UNIQOPIS = "325"
RPL_CHANNELCREATED = "329" RPL_CREATIONTIME = "329"
RPL_WHOISACCOUNT = "330" RPL_WHOISACCOUNT = "330"
RPL_NOTOPIC = "331" RPL_NOTOPIC = "331"
RPL_TOPIC = "332" RPL_TOPIC = "332"
@ -169,6 +169,9 @@ const (
RPL_YOURLANGUAGESARE = "687" RPL_YOURLANGUAGESARE = "687"
ERR_CHANNAMEINUSE = "692" ERR_CHANNAMEINUSE = "692"
ERR_CANNOTRENAME = "693" ERR_CANNOTRENAME = "693"
ERR_INVALIDMODEPARAM = "696"
ERR_LISTMODEALREADYSET = "697"
ERR_LISTMODENOTSET = "698"
RPL_HELPSTART = "704" RPL_HELPSTART = "704"
RPL_HELPTXT = "705" RPL_HELPTXT = "705"
RPL_ENDOFHELP = "706" RPL_ENDOFHELP = "706"

View File

@ -8,6 +8,7 @@ package irc
import ( import (
"fmt" "fmt"
"strings" "strings"
"unicode"
"github.com/oragono/confusables" "github.com/oragono/confusables"
"golang.org/x/text/cases" "golang.org/x/text/cases"
@ -191,9 +192,15 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) {
nick = "*" nick = "*"
} }
if nick != "*" { if nick != "*" {
nick, err = Casefold(nick) // XXX allow nick wildcards in pure ASCII, but not in unicode,
if err != nil { // because the * character breaks casefolding
return "", err if IsPureASCII(nick) {
nick = strings.ToLower(nick)
} else {
nick, err = Casefold(nick)
if err != nil {
return "", err
}
} }
} }
if user == "" { if user == "" {
@ -210,3 +217,12 @@ func CanonicalizeMaskWildcard(userhost string) (expanded string, err error) {
} }
return fmt.Sprintf("%s!%s@%s", nick, user, host), nil return fmt.Sprintf("%s!%s@%s", nick, user, host), nil
} }
func IsPureASCII(str string) bool {
for i := 0; i < len(str); i++ {
if unicode.MaxASCII < str[i] {
return false
}
}
return true
}

View File

@ -211,4 +211,5 @@ func TestCanonicalizeMaskWildcard(t *testing.T) {
tester("slingamn!shivaram*", "slingamn!shivaram*@*", nil) tester("slingamn!shivaram*", "slingamn!shivaram*@*", nil)
tester("slingamn!", "slingamn!*@*", nil) tester("slingamn!", "slingamn!*@*", nil)
tester("shivaram*@good-fortune", "*!shivaram*@good-fortune", nil) tester("shivaram*@good-fortune", "*!shivaram*@good-fortune", nil)
tester("shivaram*", "shivaram*!*@*", nil)
} }