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
This commit is contained in:
Shivaram Lingamneni 2019-12-16 19:50:15 -05:00
parent 62473468f0
commit 07865b8f63
11 changed files with 566 additions and 57 deletions

View File

@ -490,7 +490,7 @@ func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccou
fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n", fmt.Sprintf(client.t("Account: %s"), casefoldedAccount) + "\r\n",
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n", fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
"\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", fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
} }

View File

@ -37,6 +37,7 @@ type Channel struct {
createdTime time.Time createdTime time.Time
registeredFounder string registeredFounder string
registeredTime time.Time registeredTime time.Time
transferPendingTo string
topic string topic string
topicSetBy string topicSetBy string
topicSetTime time.Time topicSetTime time.Time
@ -59,22 +60,17 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
return nil return nil
} }
config := s.Config()
channel := &Channel{ channel := &Channel{
createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo
lists: map[modes.Mode]*UserMaskSet{
modes.BanMask: NewUserMaskSet(),
modes.ExceptMask: NewUserMaskSet(),
modes.InviteMask: NewUserMaskSet(),
},
members: make(MemberSet), members: make(MemberSet),
name: name, name: name,
nameCasefolded: casefoldedName, nameCasefolded: casefoldedName,
server: s, server: s,
accountToUMode: make(map[string]modes.Mode),
} }
config := s.Config() channel.initializeLists()
channel.writerSemaphore.Initialize(1) channel.writerSemaphore.Initialize(1)
channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow) channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
@ -89,6 +85,15 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
return 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 // EnsureLoaded blocks until the channel's registration info has been loaded
// from the database. // from the database.
func (channel *Channel) EnsureLoaded() { func (channel *Channel) EnsureLoaded() {
@ -303,6 +308,18 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
channel.accountToUMode = make(map[string]modes.Mode) 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. // IsRegistered returns whether the channel is registered.
func (channel *Channel) IsRegistered() bool { func (channel *Channel) IsRegistered() bool {
channel.stateMutex.RLock() channel.stateMutex.RLock()
@ -310,6 +327,78 @@ func (channel *Channel) IsRegistered() bool {
return channel.registeredFounder != "" 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() { func (channel *Channel) regenerateMembersCache() {
channel.stateMutex.RLock() channel.stateMutex.RLock()
result := make([]*Client, len(channel.members)) result := make([]*Client, len(channel.members))
@ -1117,21 +1206,23 @@ func (channel *Channel) Quit(client *Client) {
client.removeChannel(channel) client.removeChannel(channel)
} }
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) { func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { if !hasPrivs {
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
return 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) { if !channel.hasClient(target) {
rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel")) rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel"))
return 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 { if len(comment) > kicklimit {
comment = comment[:kicklimit] comment = comment[:kicklimit]
} }

View File

@ -22,6 +22,7 @@ type ChannelManager struct {
sync.RWMutex // tier 2 sync.RWMutex // tier 2
chans map[string]*channelManagerEntry chans map[string]*channelManagerEntry
registeredChannels map[string]bool registeredChannels map[string]bool
purgedChannels map[string]empty
server *Server server *Server
} }
@ -37,9 +38,11 @@ func (cm *ChannelManager) Initialize(server *Server) {
func (cm *ChannelManager) loadRegisteredChannels() { func (cm *ChannelManager) loadRegisteredChannels() {
registeredChannels := cm.server.channelRegistry.AllChannels() registeredChannels := cm.server.channelRegistry.AllChannels()
purgedChannels := cm.server.channelRegistry.PurgedChannels()
cm.Lock() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
cm.registeredChannels = registeredChannels cm.registeredChannels = registeredChannels
cm.purgedChannels = purgedChannels
} }
// Get returns an existing channel with name equivalent to `name`, or nil // 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() cm.Lock()
defer cm.Unlock() defer cm.Unlock()
_, purged := cm.purgedChannels[casefoldedName]
if purged {
return nil
}
entry := cm.chans[casefoldedName] entry := cm.chans[casefoldedName]
if entry == nil { if entry == nil {
registered := cm.registeredChannels[casefoldedName] registered := cm.registeredChannels[casefoldedName]
@ -267,3 +274,50 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
} }
return 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
}

View File

@ -32,6 +32,8 @@ const (
keyChannelPassword = "channel.key %s" keyChannelPassword = "channel.key %s"
keyChannelModes = "channel.modes %s" keyChannelModes = "channel.modes %s"
keyChannelAccountToUMode = "channel.accounttoumode %s" keyChannelAccountToUMode = "channel.accounttoumode %s"
keyChannelPurged = "channel.purged %s"
) )
var ( var (
@ -96,6 +98,12 @@ type RegisteredChannel struct {
Invites map[string]MaskInfo Invites map[string]MaskInfo
} }
type ChannelPurgeRecord struct {
Oper string
PurgedAt time.Time
Reason string
}
// ChannelRegistry manages registered channels. // ChannelRegistry manages registered channels.
type ChannelRegistry struct { type ChannelRegistry struct {
server *Server server *Server
@ -124,6 +132,24 @@ func (reg *ChannelRegistry) AllChannels() (result map[string]bool) {
return 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. // StoreChannel obtains a consistent view of a channel, then persists it to the store.
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) { func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
if !reg.server.ChannelRegistrationEnabled() { if !reg.server.ChannelRegistrationEnabled() {
@ -191,11 +217,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
info = RegisteredChannel{ info = RegisteredChannel{
Name: name, Name: name,
RegisteredAt: time.Unix(regTimeInt, 0), RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
Founder: founder, Founder: founder,
Topic: topic, Topic: topic,
TopicSetBy: topicSetBy, TopicSetBy: topicSetBy,
TopicSetTime: time.Unix(topicSetTimeInt, 0), TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(),
Key: password, Key: password,
Modes: modeSlice, Modes: modeSlice,
Bans: banlist, 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) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
channelKey := channelInfo.NameCasefolded channelKey := channelInfo.NameCasefolded
// maintain the mapping of account -> registered channels chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
chanExistsKey := fmt.Sprintf(keyChannelExists, channelKey) founder, existsErr := tx.Get(chanFounderKey)
_, existsErr := tx.Get(chanExistsKey) if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
if existsErr == buntdb.ErrNotFound { // add to new founder's list
// this is a new registration, need to update account-to-channels
accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
alreadyChannels, _ := tx.Get(accountChannelsKey) alreadyChannels, _ := tx.Get(accountChannelsKey)
newChannels := channelKey // this is the casefolded channel name 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) 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 { 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(keyChannelName, channelKey), channelInfo.Name, nil)
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, 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) 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
})
}

View File

@ -18,6 +18,7 @@ import (
) )
const chanservHelp = `ChanServ lets you register and manage channels.` const chanservHelp = `ChanServ lets you register and manage channels.`
const chanservMask = "ChanServ!ChanServ@localhost"
func chanregEnabled(config *Config) bool { func chanregEnabled(config *Config) bool {
return config.Channels.Registration.Enabled return config.Channels.Registration.Enabled
@ -75,12 +76,71 @@ referenced by their registered account names, not their nicknames.`,
enabled: chanregEnabled, enabled: chanregEnabled,
minParams: 1, 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 // csNotice sends the client a notice from ChanServ
func csNotice(rb *ResponseBuffer, text string) { 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) { 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() account := client.Account()
channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account) if !checkChanLimit(client, rb) {
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"))
return 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)) 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)) 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 // 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) { func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
channelName := params[0] channelName := params[0]
var verificationCode string var verificationCode string
@ -299,3 +368,204 @@ func unregisterConfirmationCode(name string, registeredAt time.Time) (code strin
codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16)) codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16))
return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes()))) 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)))
}

View File

@ -637,11 +637,11 @@ func (session *Session) playResume() {
} }
} else { } else {
if !session.resumeDetails.HistoryIncomplete { 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() { } 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)) fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds))
} else { } 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)"))
} }
} }
} }

View File

@ -30,6 +30,7 @@ var (
errCallbackFailed = errors.New("Account verification could not be sent") errCallbackFailed = errors.New("Account verification could not be sent")
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`) errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account") 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") errChannelAlreadyRegistered = errors.New("Channel is already registered")
errChannelNameInUse = errors.New(`Channel name in use`) errChannelNameInUse = errors.New(`Channel name in use`)
errInvalidChannelName = errors.New(`Invalid channel name`) errInvalidChannelName = errors.New(`Invalid channel name`)

View File

@ -1370,7 +1370,7 @@ func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if comment == "" { if comment == "" {
comment = kick.nick comment = kick.nick
} }
channel.Kick(client, target, comment, rb) channel.Kick(client, target, comment, rb, false)
} }
return false return false
} }
@ -1725,15 +1725,14 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
prefix := client.NickMaskString() 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(), " ")...)
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() { for _, member := range channel.Members() {
if member == client { 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 {
member.Send(nil, prefix, "MODE", args...) member.Send(nil, prefix, "MODE", args...)
} }
} }
@ -2359,7 +2358,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
if reason != "" { if reason != "" {
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason)) targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
} else { } 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) { if mSession.capabilities.Has(caps.ExtendedJoin) {
targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname) targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname)

View File

@ -186,7 +186,7 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s
if err != nil { if err != nil {
hsNotice(rb, client.t("An error occurred")) hsNotice(rb, client.t("An error occurred"))
} else { } 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) chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
hsNotifyChannel(server, chanMsg) hsNotifyChannel(server, chanMsg)
// TODO send admins a snomask of some kind // 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 != "" { if account.VHost.ApprovedVHost != "" {
hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost)) hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
if !account.VHost.Enabled { 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 { } else {
hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName)) hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))

View File

@ -119,6 +119,9 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
if isSamode { if isSamode {
return true return true
} }
if details.account != "" && details.account == channel.Founder() {
return true
}
switch change.Mode { switch change.Mode {
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
// List on these modes is a no-op anyway // List on these modes is a no-op anyway
@ -289,15 +292,16 @@ func (channel *Channel) ProcessAccountToUmodeChange(client *Client, change modes
targetModeAfter = change.Mode targetModeAfter = change.Mode
} }
// operators and founders can do anything // server operators and founders can do anything:
hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder) 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)) { if change.Op == modes.List && (clientMode == modes.Halfop || umodeGreaterThan(clientMode, modes.Halfop)) {
hasPrivs = true hasPrivs = true
// you can do adds or removes at levels you have "privileges over":
} else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) { } else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) {
hasPrivs = true hasPrivs = true
// and you can always de-op yourself:
} else if change.Op == modes.Remove && account == change.Arg { } else if change.Op == modes.Remove && account == change.Arg {
// you can always de-op yourself
hasPrivs = true hasPrivs = true
} }
if !hasPrivs { if !hasPrivs {

View File

@ -310,19 +310,19 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
} }
case "bouncer": case "bouncer":
if !config.Accounts.Bouncer.Enabled { 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 { } else {
switch settings.AllowBouncer { switch settings.AllowBouncer {
case BouncerAllowedServerDefault: case BouncerAllowedServerDefault:
if config.Accounts.Bouncer.AllowedByDefault { 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 { } 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: 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: 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: 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)) 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)) nsNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
// TODO nicer formatting for this // TODO nicer formatting for this
for _, nick := range account.AdditionalNicks { for _, nick := range account.AdditionalNicks {