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("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",
}

View File

@ -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]
}

View File

@ -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
}

View File

@ -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
})
}

View File

@ -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)))
}

View File

@ -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)"))
}
}
}

View File

@ -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`)

View File

@ -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)

View File

@ -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))

View File

@ -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 {

View File

@ -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 {