From 07865b8f63e255660dde6e5186f6c00986b52bf8 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 16 Dec 2019 19:50:15 -0500 Subject: [PATCH 1/2] chanserv enhancements and miscellaneous fixes * Fix #684 * Fix #683 * Add `CHANSERV CLEAR` * Allow mode changes from channel founders even when they aren't joined * Operators with the chanreg capability are exempt from max-channels-per-account * Small fixes and cleanup --- irc/accounts.go | 2 +- irc/channel.go | 127 ++++++++++++++++--- irc/channelmanager.go | 54 ++++++++ irc/channelreg.go | 110 +++++++++++++++-- irc/chanserv.go | 280 +++++++++++++++++++++++++++++++++++++++++- irc/client.go | 4 +- irc/errors.go | 1 + irc/handlers.go | 19 ++- irc/hostserv.go | 4 +- irc/modes.go | 10 +- irc/nickserv.go | 12 +- 11 files changed, 566 insertions(+), 57 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index 3b13bcca..4752c62f 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -490,7 +490,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n", fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n", "\r\n", - client.t("To verify your account, issue one of these commands:") + "\r\n", + client.t("To verify your account, issue the following command:") + "\r\n", fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n", } diff --git a/irc/channel.go b/irc/channel.go index 0536e14b..3ae6b9ea 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -37,6 +37,7 @@ type Channel struct { createdTime time.Time registeredFounder string registeredTime time.Time + transferPendingTo string topic string topicSetBy string topicSetTime time.Time @@ -59,22 +60,17 @@ func NewChannel(s *Server, name string, registered bool) *Channel { return nil } + config := s.Config() + channel := &Channel{ - createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo - lists: map[modes.Mode]*UserMaskSet{ - modes.BanMask: NewUserMaskSet(), - modes.ExceptMask: NewUserMaskSet(), - modes.InviteMask: NewUserMaskSet(), - }, + createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo members: make(MemberSet), name: name, nameCasefolded: casefoldedName, server: s, - accountToUMode: make(map[string]modes.Mode), } - config := s.Config() - + channel.initializeLists() channel.writerSemaphore.Initialize(1) channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow) @@ -89,6 +85,15 @@ func NewChannel(s *Server, name string, registered bool) *Channel { return channel } +func (channel *Channel) initializeLists() { + channel.lists = map[modes.Mode]*UserMaskSet{ + modes.BanMask: NewUserMaskSet(), + modes.ExceptMask: NewUserMaskSet(), + modes.InviteMask: NewUserMaskSet(), + } + channel.accountToUMode = make(map[string]modes.Mode) +} + // EnsureLoaded blocks until the channel's registration info has been loaded // from the database. func (channel *Channel) EnsureLoaded() { @@ -303,6 +308,18 @@ func (channel *Channel) SetUnregistered(expectedFounder string) { channel.accountToUMode = make(map[string]modes.Mode) } +// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes) +func (channel *Channel) resetAccess() { + defer channel.MarkDirty(IncludeLists) + + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + channel.initializeLists() + if channel.registeredFounder != "" { + channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder + } +} + // IsRegistered returns whether the channel is registered. func (channel *Channel) IsRegistered() bool { channel.stateMutex.RLock() @@ -310,6 +327,78 @@ func (channel *Channel) IsRegistered() bool { return channel.registeredFounder != "" } +type channelTransferStatus uint + +const ( + channelTransferComplete channelTransferStatus = iota + channelTransferPending + channelTransferCancelled + channelTransferFailed +) + +// Transfer transfers ownership of a registered channel to a different account +func (channel *Channel) Transfer(client *Client, target string, hasPrivs bool) (status channelTransferStatus, err error) { + status = channelTransferFailed + defer func() { + if status == channelTransferComplete && err == nil { + channel.Store(IncludeAllChannelAttrs) + } + }() + + cftarget, err := CasefoldName(target) + if err != nil { + err = errAccountDoesNotExist + return + } + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + if channel.registeredFounder == "" { + err = errChannelNotOwnedByAccount + return + } + if hasPrivs { + channel.transferOwnership(cftarget) + return channelTransferComplete, nil + } else { + if channel.registeredFounder == cftarget { + // transferring back to yourself cancels a pending transfer + channel.transferPendingTo = "" + return channelTransferCancelled, nil + } else { + channel.transferPendingTo = cftarget + return channelTransferPending, nil + } + } +} + +func (channel *Channel) transferOwnership(newOwner string) { + delete(channel.accountToUMode, channel.registeredFounder) + channel.registeredFounder = newOwner + channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder + channel.transferPendingTo = "" +} + +// AcceptTransfer implements `CS TRANSFER #chan ACCEPT` +func (channel *Channel) AcceptTransfer(client *Client) (err error) { + defer func() { + if err == nil { + channel.Store(IncludeAllChannelAttrs) + } + }() + + account := client.Account() + if account == "" { + return errAccountNotLoggedIn + } + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + if account != channel.transferPendingTo { + return errChannelTransferNotOffered + } + channel.transferOwnership(account) + return nil +} + func (channel *Channel) regenerateMembersCache() { channel.stateMutex.RLock() result := make([]*Client, len(channel.members)) @@ -1117,21 +1206,23 @@ func (channel *Channel) Quit(client *Client) { client.removeChannel(channel) } -func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) { - if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { - rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) - return +func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) { + if !hasPrivs { + if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { + rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) + return + } + if !channel.ClientHasPrivsOver(client, target) { + rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges")) + return + } } if !channel.hasClient(target) { rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel")) return } - if !channel.ClientHasPrivsOver(client, target) { - rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges")) - return - } - kicklimit := client.server.Config().Limits.KickLen + kicklimit := channel.server.Config().Limits.KickLen if len(comment) > kicklimit { comment = comment[:kicklimit] } diff --git a/irc/channelmanager.go b/irc/channelmanager.go index 8ceb390b..8a12ed7c 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -22,6 +22,7 @@ type ChannelManager struct { sync.RWMutex // tier 2 chans map[string]*channelManagerEntry registeredChannels map[string]bool + purgedChannels map[string]empty server *Server } @@ -37,9 +38,11 @@ func (cm *ChannelManager) Initialize(server *Server) { func (cm *ChannelManager) loadRegisteredChannels() { registeredChannels := cm.server.channelRegistry.AllChannels() + purgedChannels := cm.server.channelRegistry.PurgedChannels() cm.Lock() defer cm.Unlock() cm.registeredChannels = registeredChannels + cm.purgedChannels = purgedChannels } // Get returns an existing channel with name equivalent to `name`, or nil @@ -69,6 +72,10 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin cm.Lock() defer cm.Unlock() + _, purged := cm.purgedChannels[casefoldedName] + if purged { + return nil + } entry := cm.chans[casefoldedName] if entry == nil { registered := cm.registeredChannels[casefoldedName] @@ -267,3 +274,50 @@ func (cm *ChannelManager) Channels() (result []*Channel) { } return } + +// Purge marks a channel as purged. +func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err error) { + chname, err = CasefoldChannel(chname) + if err != nil { + return errInvalidChannelName + } + + cm.Lock() + cm.purgedChannels[chname] = empty{} + cm.Unlock() + + cm.server.channelRegistry.PurgeChannel(chname, record) + return nil +} + +// IsPurged queries whether a channel is purged. +func (cm *ChannelManager) IsPurged(chname string) (result bool) { + chname, err := CasefoldChannel(chname) + if err != nil { + return false + } + + cm.Lock() + _, result = cm.purgedChannels[chname] + cm.Unlock() + return +} + +// Unpurge deletes a channel's purged status. +func (cm *ChannelManager) Unpurge(chname string) (err error) { + chname, err = CasefoldChannel(chname) + if err != nil { + return errNoSuchChannel + } + + cm.Lock() + _, found := cm.purgedChannels[chname] + delete(cm.purgedChannels, chname) + cm.Unlock() + + cm.server.channelRegistry.UnpurgeChannel(chname) + if !found { + return errNoSuchChannel + } + return nil +} diff --git a/irc/channelreg.go b/irc/channelreg.go index 6f93104f..ad54affd 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -32,6 +32,8 @@ const ( keyChannelPassword = "channel.key %s" keyChannelModes = "channel.modes %s" keyChannelAccountToUMode = "channel.accounttoumode %s" + + keyChannelPurged = "channel.purged %s" ) var ( @@ -96,6 +98,12 @@ type RegisteredChannel struct { Invites map[string]MaskInfo } +type ChannelPurgeRecord struct { + Oper string + PurgedAt time.Time + Reason string +} + // ChannelRegistry manages registered channels. type ChannelRegistry struct { server *Server @@ -124,6 +132,24 @@ func (reg *ChannelRegistry) AllChannels() (result map[string]bool) { return } +// PurgedChannels returns the set of all channel names that have been purged +func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) { + result = make(map[string]empty) + + prefix := fmt.Sprintf(keyChannelPurged, "") + reg.server.store.View(func(tx *buntdb.Tx) error { + return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { + if !strings.HasPrefix(key, prefix) { + return false + } + channel := strings.TrimPrefix(key, prefix) + result[channel] = empty{} + return true + }) + }) + return +} + // StoreChannel obtains a consistent view of a channel, then persists it to the store. func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) { if !reg.server.ChannelRegistrationEnabled() { @@ -191,11 +217,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC info = RegisteredChannel{ Name: name, - RegisteredAt: time.Unix(regTimeInt, 0), + RegisteredAt: time.Unix(regTimeInt, 0).UTC(), Founder: founder, Topic: topic, TopicSetBy: topicSetBy, - TopicSetTime: time.Unix(topicSetTimeInt, 0), + TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(), Key: password, Modes: modeSlice, Bans: banlist, @@ -256,14 +282,12 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist } } -// saveChannel saves a channel to the store. -func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) { +func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) { channelKey := channelInfo.NameCasefolded - // maintain the mapping of account -> registered channels - chanExistsKey := fmt.Sprintf(keyChannelExists, channelKey) - _, existsErr := tx.Get(chanExistsKey) - if existsErr == buntdb.ErrNotFound { - // this is a new registration, need to update account-to-channels + chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey) + founder, existsErr := tx.Get(chanFounderKey) + if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder { + // add to new founder's list accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) alreadyChannels, _ := tx.Get(accountChannelsKey) newChannels := channelKey // this is the casefolded channel name @@ -272,9 +296,30 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha } tx.Set(accountChannelsKey, newChannels, nil) } + if existsErr == nil && founder != channelInfo.Founder { + // remove from old founder's list + accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder) + alreadyChannelsRaw, _ := tx.Get(accountChannelsKey) + var newChannels []string + if alreadyChannelsRaw != "" { + for _, chname := range strings.Split(alreadyChannelsRaw, ",") { + if chname != channelInfo.NameCasefolded { + newChannels = append(newChannels, chname) + } + } + } + tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil) + } +} + +// saveChannel saves a channel to the store. +func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) { + channelKey := channelInfo.NameCasefolded + // maintain the mapping of account -> registered channels + reg.updateAccountToChannelMapping(tx, channelInfo) if includeFlags&IncludeInitial != 0 { - tx.Set(chanExistsKey, "1", nil) + tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) @@ -306,3 +351,48 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil) } } + +// PurgeChannel records a channel purge. +func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) { + serialized, err := json.Marshal(record) + if err != nil { + return err + } + serializedStr := string(serialized) + key := fmt.Sprintf(keyChannelPurged, chname) + + return reg.server.store.Update(func(tx *buntdb.Tx) error { + tx.Set(key, serializedStr, nil) + return nil + }) +} + +// LoadPurgeRecord retrieves information about whether and how a channel was purged. +func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) { + var rawRecord string + key := fmt.Sprintf(keyChannelPurged, chname) + reg.server.store.View(func(tx *buntdb.Tx) error { + rawRecord, _ = tx.Get(key) + return nil + }) + if rawRecord == "" { + err = errNoSuchChannel + return + } + err = json.Unmarshal([]byte(rawRecord), &record) + if err != nil { + reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error()) + err = errNoSuchChannel + return + } + return +} + +// UnpurgeChannel deletes the record of a channel purge. +func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) { + key := fmt.Sprintf(keyChannelPurged, chname) + return reg.server.store.Update(func(tx *buntdb.Tx) error { + tx.Delete(key) + return nil + }) +} diff --git a/irc/chanserv.go b/irc/chanserv.go index 0d6b02ae..f0be2dd5 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -18,6 +18,7 @@ import ( ) const chanservHelp = `ChanServ lets you register and manage channels.` +const chanservMask = "ChanServ!ChanServ@localhost" func chanregEnabled(config *Config) bool { return config.Channels.Registration.Enabled @@ -75,12 +76,71 @@ referenced by their registered account names, not their nicknames.`, enabled: chanregEnabled, minParams: 1, }, + "clear": { + handler: csClearHandler, + help: `Syntax: $bCLEAR #channel target$b + +CLEAR removes users or settings from a channel. Specifically: + +$bCLEAR #channel users$b kicks all users except for you. +$bCLEAR #channel access$b resets all stored bans, invites, ban exceptions, +and persistent user-mode grants made with CS AMODE.`, + helpShort: `$bCLEAR$b removes users or settings from a channel.`, + enabled: chanregEnabled, + minParams: 2, + }, + "transfer": { + handler: csTransferHandler, + help: `Syntax: $bTRANSFER [accept] #channel user [code]$b + +TRANSFER transfers ownership of a channel from one user to another. +To prevent accidental transfers, a verification code is required. For +example, $bTRANSFER #channel alice$b displays the required confirmation +code, then $bTRANSFER #channel alice 2930242125$b initiates the transfer. +Unless you are an IRC operator with the correct permissions, alice must +then accept the transfer, which she can do with $bTRANSFER accept #channel$b.`, + helpShort: `$bTRANSFER$b transfers ownership of a channel to another user.`, + enabled: chanregEnabled, + minParams: 2, + }, + "purge": { + handler: csPurgeHandler, + help: `Syntax: $bPURGE #channel [reason]$b + +PURGE blacklists a channel from the server, making it impossible to join +or otherwise interact with the channel. If the channel currently has members, +they will be kicked from it. PURGE may also be applied preemptively to +channels that do not currently have members.`, + helpShort: `$bPURGE$b blacklists a channel from the server.`, + capabs: []string{"chanreg"}, + minParams: 1, + maxParams: 2, + unsplitFinalParam: true, + }, + "unpurge": { + handler: csUnpurgeHandler, + help: `Syntax: $bUNPURGE #channel$b + +UNPURGE removes any blacklisting of a channel that was previously +set using PURGE.`, + helpShort: `$bUNPURGE$b undoes a previous PURGE command.`, + capabs: []string{"chanreg"}, + minParams: 1, + }, + "info": { + handler: csInfoHandler, + help: `Syntax: $INFO #channel$b + +INFO displays info about a registered channel.`, + helpShort: `$bINFO$b displays info about a registered channel.`, + minParams: 1, + }, } ) // csNotice sends the client a notice from ChanServ func csNotice(rb *ResponseBuffer, text string) { - rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text) + rb.Add(nil, chanservMask, "NOTICE", rb.target.Nick(), text) } func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { @@ -219,9 +279,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] } account := client.Account() - channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account) - if server.Config().Channels.Registration.MaxChannelsPerAccount <= len(channelsAlreadyRegistered) { - csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER")) + if !checkChanLimit(client, rb) { return } @@ -234,7 +292,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName)) - server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) + server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName)) server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString)) // give them founder privs @@ -249,6 +307,17 @@ func csRegisterHandler(server *Server, client *Client, command string, params [] } } +// check whether a client has already registered too many channels +func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) { + account := client.Account() + channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account) + ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg") + if !ok { + csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER")) + } + return +} + func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { channelName := params[0] var verificationCode string @@ -299,3 +368,204 @@ func unregisterConfirmationCode(name string, registeredAt time.Time) (code strin codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16)) return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes()))) } + +func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + channel := server.channels.Get(params[0]) + if channel == nil { + csNotice(rb, client.t("Channel does not exist")) + return + } + account := client.Account() + if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) { + csNotice(rb, client.t("Insufficient privileges")) + return + } + + switch strings.ToLower(params[1]) { + case "access": + channel.resetAccess() + csNotice(rb, client.t("Successfully reset channel access")) + case "users": + for _, target := range channel.Members() { + if target != client { + channel.Kick(client, target, "Cleared by ChanServ", rb, true) + } + } + default: + csNotice(rb, client.t("Invalid parameters")) + } + +} + +func csTransferHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + if strings.ToLower(params[0]) == "accept" { + processTransferAccept(client, params[1], rb) + return + } + chname := params[0] + channel := server.channels.Get(chname) + if channel == nil { + csNotice(rb, client.t("Channel does not exist")) + return + } + regInfo := channel.ExportRegistration(0) + chname = regInfo.Name + account := client.Account() + isFounder := account != "" && account == regInfo.Founder + hasPrivs := client.HasRoleCapabs("chanreg") + if !(isFounder || hasPrivs) { + csNotice(rb, client.t("Insufficient privileges")) + return + } + target := params[1] + _, err := server.accounts.LoadAccount(params[1]) + if err != nil { + csNotice(rb, client.t("Account does not exist")) + return + } + expectedCode := unregisterConfirmationCode(regInfo.Name, regInfo.RegisteredAt) + codeValidated := 2 < len(params) && params[2] == expectedCode + if !codeValidated { + csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b"))) + csNotice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode)) + return + } + status, err := channel.Transfer(client, target, hasPrivs) + if err == nil { + switch status { + case channelTransferComplete: + csNotice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target)) + case channelTransferPending: + sendTransferPendingNotice(server, target, chname) + csNotice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target)) + case channelTransferCancelled: + csNotice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname)) + } + } else { + csNotice(rb, client.t("Could not transfer channel")) + } +} + +func sendTransferPendingNotice(server *Server, account, chname string) { + clients := server.accounts.AccountToClients(account) + if len(clients) == 0 { + return + } + var client *Client + for _, candidate := range clients { + client = candidate + if candidate.NickCasefolded() == candidate.Account() { + break // prefer the login where the nick is the account + } + } + client.Send(nil, chanservMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %s. To accept, /CS TRANSFER ACCEPT %s"), chname, chname)) +} + +func processTransferAccept(client *Client, chname string, rb *ResponseBuffer) { + channel := client.server.channels.Get(chname) + if channel == nil { + csNotice(rb, client.t("Channel does not exist")) + return + } + if !checkChanLimit(client, rb) { + return + } + switch channel.AcceptTransfer(client) { + case nil: + csNotice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name())) + case errChannelTransferNotOffered: + csNotice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name())) + default: + csNotice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name())) + } +} + +func csPurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + oper := client.Oper() + if oper == nil { + return // should be impossible because you need oper capabs for this + } + + chname := params[0] + var reason string + if 1 < len(params) { + reason = params[1] + } + purgeRecord := ChannelPurgeRecord{ + Oper: oper.Name, + PurgedAt: time.Now().UTC(), + Reason: reason, + } + switch server.channels.Purge(chname, purgeRecord) { + case nil: + channel := server.channels.Get(chname) + if channel != nil { // channel need not exist to be purged + for _, target := range channel.Members() { + channel.Kick(client, target, "Cleared by ChanServ", rb, true) + } + } + csNotice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname)) + case errInvalidChannelName: + csNotice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname)) + default: + csNotice(rb, client.t("An error occurred")) + } +} + +func csUnpurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + chname := params[0] + switch server.channels.Unpurge(chname) { + case nil: + csNotice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname)) + case errNoSuchChannel: + csNotice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname)) + default: + csNotice(rb, client.t("An error occurred")) + } +} + +func csInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + chname, err := CasefoldChannel(params[0]) + if err != nil { + csNotice(rb, client.t("Invalid channel name")) + return + } + + // purge status + if client.HasRoleCapabs("chanreg") { + purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname) + if err == nil { + csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname)) + csNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper)) + csNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123))) + if purgeRecord.Reason != "" { + csNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason)) + } + } + } else { + if server.channels.IsPurged(chname) { + csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname)) + } + } + + var chinfo RegisteredChannel + channel := server.channels.Get(params[0]) + if channel != nil { + chinfo = channel.ExportRegistration(0) + } else { + chinfo, err = server.channelRegistry.LoadChannel(chname) + if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) { + csNotice(rb, client.t("An error occurred")) + return + } + } + + // channel exists but is unregistered, or doesn't exist: + if chinfo.Founder == "" { + csNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname)) + return + } + csNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name)) + csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder)) + csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123))) +} diff --git a/irc/client.go b/irc/client.go index b67e78b1..9cb006b2 100644 --- a/irc/client.go +++ b/irc/client.go @@ -637,11 +637,11 @@ func (session *Session) playResume() { } } else { if !session.resumeDetails.HistoryIncomplete { - fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) + fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected")) } else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() { fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds)) } else { - fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (message history may have been lost)"))) + fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected (message history may have been lost)")) } } } diff --git a/irc/errors.go b/irc/errors.go index 5874f690..f3dafd0f 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -30,6 +30,7 @@ var ( errCallbackFailed = errors.New("Account verification could not be sent") errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") + errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`) errChannelAlreadyRegistered = errors.New("Channel is already registered") errChannelNameInUse = errors.New(`Channel name in use`) errInvalidChannelName = errors.New(`Invalid channel name`) diff --git a/irc/handlers.go b/irc/handlers.go index dbcc5cb7..8bab0a04 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1370,7 +1370,7 @@ func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if comment == "" { comment = kick.nick } - channel.Kick(client, target, comment, rb) + channel.Kick(client, target, comment, rb, false) } return false } @@ -1725,15 +1725,14 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res prefix := client.NickMaskString() //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(), " ")...) + rb.Add(nil, prefix, "MODE", args...) + for _, session := range client.Sessions() { + if session != rb.session { + session.Send(nil, prefix, "MODE", args...) + } + } for _, member := range channel.Members() { - if member == client { - rb.Add(nil, prefix, "MODE", args...) - for _, session := range client.Sessions() { - if session != rb.session { - session.Send(nil, prefix, "MODE", args...) - } - } - } else { + if member != client { member.Send(nil, prefix, "MODE", args...) } } @@ -2359,7 +2358,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re if reason != "" { targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason)) } else { - targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed"))) + targetRb.Add(nil, targetPrefix, "PART", oldName, mcl.t("Channel renamed")) } if mSession.capabilities.Has(caps.ExtendedJoin) { targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname) diff --git a/irc/hostserv.go b/irc/hostserv.go index d7eb243b..8b1242e8 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -186,7 +186,7 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s if err != nil { hsNotice(rb, client.t("An error occurred")) } else { - hsNotice(rb, fmt.Sprintf(client.t("Your vhost request will be reviewed by an administrator"))) + hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator")) chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost) hsNotifyChannel(server, chanMsg) // TODO send admins a snomask of some kind @@ -221,7 +221,7 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st if account.VHost.ApprovedVHost != "" { hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost)) if !account.VHost.Enabled { - hsNotice(rb, fmt.Sprintf(client.t("This vhost is currently disabled, but can be enabled with /HS ON"))) + hsNotice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON")) } } else { hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName)) diff --git a/irc/modes.go b/irc/modes.go index 66c17859..4b14869b 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -119,6 +119,9 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c if isSamode { return true } + if details.account != "" && details.account == channel.Founder() { + return true + } switch change.Mode { case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: // List on these modes is a no-op anyway @@ -289,15 +292,16 @@ func (channel *Channel) ProcessAccountToUmodeChange(client *Client, change modes targetModeAfter = change.Mode } - // operators and founders can do anything + // server operators and founders can do anything: hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder) - // halfop and up can list, and do add/removes at levels <= their own + // halfop and up can list: if change.Op == modes.List && (clientMode == modes.Halfop || umodeGreaterThan(clientMode, modes.Halfop)) { hasPrivs = true + // you can do adds or removes at levels you have "privileges over": } else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) { hasPrivs = true + // and you can always de-op yourself: } else if change.Op == modes.Remove && account == change.Arg { - // you can always de-op yourself hasPrivs = true } if !hasPrivs { diff --git a/irc/nickserv.go b/irc/nickserv.go index f0bd0037..ac8ddfdb 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -310,19 +310,19 @@ func displaySetting(settingName string, settings AccountSettings, client *Client } case "bouncer": if !config.Accounts.Bouncer.Enabled { - nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators"))) + nsNotice(rb, client.t("This feature has been disabled by the server administrators")) } else { switch settings.AllowBouncer { case BouncerAllowedServerDefault: if config.Accounts.Bouncer.AllowedByDefault { - nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out"))) + nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account, but you can opt out")) } else { - nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in"))) + nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account, but you can opt in")) } case BouncerDisallowedByUser: - nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account"))) + nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account")) case BouncerAllowedByUser: - nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account"))) + nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account")) } } default: @@ -569,7 +569,7 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri } nsNotice(rb, fmt.Sprintf(client.t("Account: %s"), account.Name)) - registeredAt := account.RegisteredAt.Format("Jan 02, 2006 15:04:05Z") + registeredAt := account.RegisteredAt.Format(time.RFC1123) nsNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt)) // TODO nicer formatting for this for _, nick := range account.AdditionalNicks { From c5a81d59ffa301695805009b232c2c0d0fbbbbab Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 17 Dec 2019 13:21:26 -0500 Subject: [PATCH 2/2] fix #581 --- irc/channel.go | 8 +-- irc/channelmanager.go | 119 ++++++++++++++++++++++++++++++------------ irc/channelreg.go | 12 ++--- irc/errors.go | 2 + irc/handlers.go | 26 +++++++-- irc/types.go | 11 ++++ 6 files changed, 126 insertions(+), 52 deletions(-) diff --git a/irc/channel.go b/irc/channel.go index 3ae6b9ea..0de004ee 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -53,13 +53,7 @@ type Channel struct { // NewChannel creates a new channel from a `Server` and a `name` // string, which must be unique on the server. -func NewChannel(s *Server, name string, registered bool) *Channel { - casefoldedName, err := CasefoldChannel(name) - if err != nil { - s.logger.Error("internal", "Bad channel name", name, err.Error()) - return nil - } - +func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channel { config := s.Config() channel := &Channel{ diff --git a/irc/channelmanager.go b/irc/channelmanager.go index 8a12ed7c..507223f8 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -13,36 +13,54 @@ type channelManagerEntry struct { // think the channel is empty (without holding a lock across the entire Channel.Join() // call) pendingJoins int + skeleton string } // ChannelManager keeps track of all the channels on the server, // providing synchronization for creation of new channels on first join, // cleanup of empty channels on last part, and renames. type ChannelManager struct { - sync.RWMutex // tier 2 - chans map[string]*channelManagerEntry - registeredChannels map[string]bool - purgedChannels map[string]empty - server *Server + sync.RWMutex // tier 2 + // chans is the main data structure, mapping casefolded name -> *Channel + chans map[string]*channelManagerEntry + chansSkeletons StringSet // skeletons of *unregistered* chans + registeredChannels StringSet // casefolds of registered chans + registeredSkeletons StringSet // skeletons of registered chans + purgedChannels StringSet // casefolds of purged chans + server *Server } // NewChannelManager returns a new ChannelManager. func (cm *ChannelManager) Initialize(server *Server) { cm.chans = make(map[string]*channelManagerEntry) + cm.chansSkeletons = make(StringSet) cm.server = server if server.Config().Channels.Registration.Enabled { cm.loadRegisteredChannels() } + // purging should work even if registration is disabled + cm.purgedChannels = cm.server.channelRegistry.PurgedChannels() } func (cm *ChannelManager) loadRegisteredChannels() { - registeredChannels := cm.server.channelRegistry.AllChannels() - purgedChannels := cm.server.channelRegistry.PurgedChannels() + rawNames := cm.server.channelRegistry.AllChannels() + registeredChannels := make(StringSet, len(rawNames)) + registeredSkeletons := make(StringSet, len(rawNames)) + for _, name := range rawNames { + cfname, err := CasefoldChannel(name) + if err == nil { + registeredChannels.Add(cfname) + } + skeleton, err := Skeleton(name) + if err == nil { + registeredSkeletons.Add(skeleton) + } + } cm.Lock() defer cm.Unlock() cm.registeredChannels = registeredChannels - cm.purgedChannels = purgedChannels + cm.registeredSkeletons = registeredSkeletons } // Get returns an existing channel with name equivalent to `name`, or nil @@ -64,37 +82,49 @@ func (cm *ChannelManager) Get(name string) (channel *Channel) { func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error { server := client.server casefoldedName, err := CasefoldChannel(name) - if err != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { + skeleton, skerr := Skeleton(name) + if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { return errNoSuchChannel } - channel := func() *Channel { + channel, err := func() (*Channel, error) { cm.Lock() defer cm.Unlock() - _, purged := cm.purgedChannels[casefoldedName] - if purged { - return nil + if cm.purgedChannels.Has(casefoldedName) { + return nil, errChannelPurged } entry := cm.chans[casefoldedName] if entry == nil { - registered := cm.registeredChannels[casefoldedName] + registered := cm.registeredChannels.Has(casefoldedName) // enforce OpOnlyCreation if !registered && server.Config().Channels.OpOnlyCreation && !client.HasRoleCapabs("chanreg") { - return nil + return nil, errInsufficientPrivs + } + // enforce confusables + if cm.chansSkeletons.Has(skeleton) || (!registered && cm.registeredSkeletons.Has(skeleton)) { + return nil, errConfusableIdentifier } entry = &channelManagerEntry{ - channel: NewChannel(server, name, registered), + channel: NewChannel(server, name, casefoldedName, registered), pendingJoins: 0, } + if !registered { + // for an unregistered channel, we already have the correct unfolded name + // and therefore the final skeleton. for a registered channel, we don't have + // the unfolded name yet (it needs to be loaded from the db), but we already + // have the final skeleton in `registeredSkeletons` so we don't need to track it + cm.chansSkeletons.Add(skeleton) + entry.skeleton = skeleton + } cm.chans[casefoldedName] = entry } entry.pendingJoins += 1 - return entry.channel + return entry.channel, nil }() - if channel == nil { - return errNoSuchChannel + if err != nil { + return err } channel.EnsureLoaded() @@ -109,8 +139,9 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { cm.Lock() defer cm.Unlock() - nameCasefolded := channel.NameCasefolded() - entry := cm.chans[nameCasefolded] + cfname := channel.NameCasefolded() + + entry := cm.chans[cfname] if entry == nil || entry.channel != channel { return } @@ -119,7 +150,10 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) { entry.pendingJoins -= 1 } if entry.pendingJoins == 0 && entry.channel.IsClean() { - delete(cm.chans, nameCasefolded) + delete(cm.chans, cfname) + if entry.skeleton != "" { + delete(cm.chansSkeletons, entry.skeleton) + } } } @@ -177,7 +211,7 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err if err != nil { return err } - cm.registeredChannels[cfname] = true + cm.registeredChannels.Add(cfname) return nil } @@ -211,13 +245,17 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e } // Rename renames a channel (but does not notify the members) -func (cm *ChannelManager) Rename(name string, newname string) (err error) { +func (cm *ChannelManager) Rename(name string, newName string) (err error) { cfname, err := CasefoldChannel(name) if err != nil { return errNoSuchChannel } - cfnewname, err := CasefoldChannel(newname) + newCfname, err := CasefoldChannel(newName) + if err != nil { + return errInvalidChannelName + } + newSkeleton, err := Skeleton(newName) if err != nil { return errInvalidChannelName } @@ -236,22 +274,35 @@ func (cm *ChannelManager) Rename(name string, newname string) (err error) { cm.Lock() defer cm.Unlock() - if cm.chans[cfnewname] != nil || cm.registeredChannels[cfnewname] { + if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) { + return errChannelNameInUse + } + if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) { return errChannelNameInUse } entry := cm.chans[cfname] - if entry == nil { + if entry == nil || !entry.channel.IsLoaded() { return errNoSuchChannel } channel = entry.channel info = channel.ExportRegistration(IncludeInitial) + registered := info.Founder != "" delete(cm.chans, cfname) - cm.chans[cfnewname] = entry - if cm.registeredChannels[cfname] { + cm.chans[newCfname] = entry + if registered { delete(cm.registeredChannels, cfname) - cm.registeredChannels[cfnewname] = true + if oldSkeleton, err := Skeleton(info.Name); err == nil { + delete(cm.registeredSkeletons, oldSkeleton) + } + cm.registeredChannels.Add(newCfname) + cm.registeredSkeletons.Add(newSkeleton) + } else { + delete(cm.chansSkeletons, entry.skeleton) + cm.chansSkeletons.Add(newSkeleton) + entry.skeleton = newSkeleton + cm.chans[cfname] = entry } - entry.channel.Rename(newname, cfnewname) + entry.channel.Rename(newName, newCfname) return nil } @@ -283,7 +334,7 @@ func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err e } cm.Lock() - cm.purgedChannels[chname] = empty{} + cm.purgedChannels.Add(chname) cm.Unlock() cm.server.channelRegistry.PurgeChannel(chname, record) @@ -298,7 +349,7 @@ func (cm *ChannelManager) IsPurged(chname string) (result bool) { } cm.Lock() - _, result = cm.purgedChannels[chname] + result = cm.purgedChannels.Has(chname) cm.Unlock() return } @@ -311,7 +362,7 @@ func (cm *ChannelManager) Unpurge(chname string) (err error) { } cm.Lock() - _, found := cm.purgedChannels[chname] + found := cm.purgedChannels.Has(chname) delete(cm.purgedChannels, chname) cm.Unlock() diff --git a/irc/channelreg.go b/irc/channelreg.go index ad54affd..e0ea9b25 100644 --- a/irc/channelreg.go +++ b/irc/channelreg.go @@ -114,17 +114,15 @@ func (reg *ChannelRegistry) Initialize(server *Server) { reg.server = server } -func (reg *ChannelRegistry) AllChannels() (result map[string]bool) { - result = make(map[string]bool) - - prefix := fmt.Sprintf(keyChannelExists, "") +// AllChannels returns the uncasefolded names of all registered channels. +func (reg *ChannelRegistry) AllChannels() (result []string) { + prefix := fmt.Sprintf(keyChannelName, "") reg.server.store.View(func(tx *buntdb.Tx) error { return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { if !strings.HasPrefix(key, prefix) { return false } - channel := strings.TrimPrefix(key, prefix) - result[channel] = true + result = append(result, value) return true }) }) @@ -132,7 +130,7 @@ func (reg *ChannelRegistry) AllChannels() (result map[string]bool) { return } -// PurgedChannels returns the set of all channel names that have been purged +// PurgedChannels returns the set of all casefolded channel names that have been purged func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) { result = make(map[string]empty) diff --git a/irc/errors.go b/irc/errors.go index f3dafd0f..00d64851 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -41,6 +41,8 @@ var ( errNicknameReserved = errors.New("nickname is reserved") errNoExistingBan = errors.New("Ban does not exist") errNoSuchChannel = errors.New(`No such channel`) + errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`) + errConfusableIdentifier = errors.New("This identifier is confusable with one already in use") errInsufficientPrivs = errors.New("Insufficient privileges") errInvalidUsername = errors.New("Invalid username") errFeatureDisabled = errors.New(`That feature is disabled`) diff --git a/irc/handlers.go b/irc/handlers.go index 8bab0a04..661b23f3 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1290,13 +1290,28 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp key = keys[i] } err := server.channels.Join(client, name, key, false, rb) - if err == errNoSuchChannel { - rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(name), client.t("No such channel")) + if err != nil { + sendJoinError(client, name, rb, err) } } return false } +func sendJoinError(client *Client, name string, rb *ResponseBuffer, err error) { + var errMsg string + switch err { + case errInsufficientPrivs: + errMsg = `Only server operators can create new channels` + case errConfusableIdentifier: + errMsg = `That channel name is too close to the name of another channel` + case errChannelPurged: + errMsg = err.Error() + default: + errMsg = `No such channel` + } + rb.Add(nil, client.server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(name), client.t(errMsg)) +} + // SAJOIN [nick] #channel{,#channel} func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { var target *Client @@ -1306,7 +1321,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re channelString = msg.Params[0] } else { if len(msg.Params) == 1 { - rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "KICK", client.t("Not enough parameters")) + rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "SAJOIN", client.t("Not enough parameters")) return false } else { target = server.clients.Get(msg.Params[0]) @@ -1320,7 +1335,10 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re channels := strings.Split(channelString, ",") for _, chname := range channels { - server.channels.Join(target, chname, "", true, rb) + err := server.channels.Join(target, chname, "", true, rb) + if err != nil { + sendJoinError(client, chname, rb, err) + } } return false } diff --git a/irc/types.go b/irc/types.go index 756f1b61..6e1a1110 100644 --- a/irc/types.go +++ b/irc/types.go @@ -28,6 +28,17 @@ func (clients ClientSet) Has(client *Client) bool { return ok } +type StringSet map[string]empty + +func (s StringSet) Has(str string) bool { + _, ok := s[str] + return ok +} + +func (s StringSet) Add(str string) { + s[str] = empty{} +} + // MemberSet is a set of members with modes. type MemberSet map[*Client]*modes.ModeSet