Merge pull request #692 from slingamn/issue683_channelpurge.1

chanserv enhancements and miscellaneous fixes
This commit is contained in:
Shivaram Lingamneni 2019-12-22 10:41:24 -05:00 committed by GitHub
commit bf5a02f077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 680 additions and 97 deletions

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

@ -37,6 +37,7 @@ type Channel struct {
createdTime time.Time
registeredFounder string
registeredTime time.Time
transferPendingTo string
topic string
topicSetBy string
topicSetTime time.Time
@ -52,29 +53,18 @@ 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{
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 +79,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 +302,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 +321,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))
@ -1128,21 +1211,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]
}

@ -13,33 +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
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()
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.registeredSkeletons = registeredSkeletons
}
// Get returns an existing channel with name equivalent to `name`, or nil
@ -61,33 +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()
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()
@ -102,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
}
@ -112,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)
}
}
}
@ -170,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
}
@ -204,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
}
@ -229,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
}
@ -267,3 +325,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.Add(chname)
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.Has(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.Has(chname)
delete(cm.purgedChannels, chname)
cm.Unlock()
cm.server.channelRegistry.UnpurgeChannel(chname)
if !found {
return errNoSuchChannel
}
return nil
}

@ -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
@ -106,21 +114,37 @@ func (reg *ChannelRegistry) Initialize(server *Server) {
reg.server = server
}
func (reg *ChannelRegistry) AllChannels() (result map[string]bool) {
result = make(map[string]bool)
// 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
}
result = append(result, value)
return true
})
})
prefix := fmt.Sprintf(keyChannelExists, "")
return
}
// 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)
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] = true
result[channel] = empty{}
return true
})
})
return
}
@ -191,11 +215,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 +280,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 +294,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 +349,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
})
}

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

@ -694,11 +694,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)"))
}
}
}

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

@ -1292,13 +1292,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
@ -1308,7 +1323,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])
@ -1322,7 +1337,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
}
@ -1372,7 +1390,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
}
@ -1727,15 +1745,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...)
}
}
@ -2380,7 +2397,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)

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

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

@ -316,19 +316,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:
@ -575,7 +575,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 {

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