mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 10:42:52 +01:00
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:
parent
62473468f0
commit
07865b8f63
@ -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",
|
||||
}
|
||||
|
||||
|
119
irc/channel.go
119
irc/channel.go
@ -37,6 +37,7 @@ type Channel struct {
|
||||
createdTime time.Time
|
||||
registeredFounder string
|
||||
registeredTime time.Time
|
||||
transferPendingTo string
|
||||
topic string
|
||||
topicSetBy string
|
||||
topicSetTime time.Time
|
||||
@ -59,22 +60,17 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
|
||||
return nil
|
||||
}
|
||||
|
||||
config := s.Config()
|
||||
|
||||
channel := &Channel{
|
||||
createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo
|
||||
lists: map[modes.Mode]*UserMaskSet{
|
||||
modes.BanMask: NewUserMaskSet(),
|
||||
modes.ExceptMask: NewUserMaskSet(),
|
||||
modes.InviteMask: NewUserMaskSet(),
|
||||
},
|
||||
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) {
|
||||
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.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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
kicklimit := client.server.Config().Limits.KickLen
|
||||
kicklimit := channel.server.Config().Limits.KickLen
|
||||
if len(comment) > kicklimit {
|
||||
comment = comment[:kicklimit]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
280
irc/chanserv.go
280
irc/chanserv.go
@ -18,6 +18,7 @@ import (
|
||||
)
|
||||
|
||||
const chanservHelp = `ChanServ lets you register and manage channels.`
|
||||
const chanservMask = "ChanServ!ChanServ@localhost"
|
||||
|
||||
func chanregEnabled(config *Config) bool {
|
||||
return config.Channels.Registration.Enabled
|
||||
@ -75,12 +76,71 @@ referenced by their registered account names, not their nicknames.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 1,
|
||||
},
|
||||
"clear": {
|
||||
handler: csClearHandler,
|
||||
help: `Syntax: $bCLEAR #channel target$b
|
||||
|
||||
CLEAR removes users or settings from a channel. Specifically:
|
||||
|
||||
$bCLEAR #channel users$b kicks all users except for you.
|
||||
$bCLEAR #channel access$b resets all stored bans, invites, ban exceptions,
|
||||
and persistent user-mode grants made with CS AMODE.`,
|
||||
helpShort: `$bCLEAR$b removes users or settings from a channel.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"transfer": {
|
||||
handler: csTransferHandler,
|
||||
help: `Syntax: $bTRANSFER [accept] #channel user [code]$b
|
||||
|
||||
TRANSFER transfers ownership of a channel from one user to another.
|
||||
To prevent accidental transfers, a verification code is required. For
|
||||
example, $bTRANSFER #channel alice$b displays the required confirmation
|
||||
code, then $bTRANSFER #channel alice 2930242125$b initiates the transfer.
|
||||
Unless you are an IRC operator with the correct permissions, alice must
|
||||
then accept the transfer, which she can do with $bTRANSFER accept #channel$b.`,
|
||||
helpShort: `$bTRANSFER$b transfers ownership of a channel to another user.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
"purge": {
|
||||
handler: csPurgeHandler,
|
||||
help: `Syntax: $bPURGE #channel [reason]$b
|
||||
|
||||
PURGE blacklists a channel from the server, making it impossible to join
|
||||
or otherwise interact with the channel. If the channel currently has members,
|
||||
they will be kicked from it. PURGE may also be applied preemptively to
|
||||
channels that do not currently have members.`,
|
||||
helpShort: `$bPURGE$b blacklists a channel from the server.`,
|
||||
capabs: []string{"chanreg"},
|
||||
minParams: 1,
|
||||
maxParams: 2,
|
||||
unsplitFinalParam: true,
|
||||
},
|
||||
"unpurge": {
|
||||
handler: csUnpurgeHandler,
|
||||
help: `Syntax: $bUNPURGE #channel$b
|
||||
|
||||
UNPURGE removes any blacklisting of a channel that was previously
|
||||
set using PURGE.`,
|
||||
helpShort: `$bUNPURGE$b undoes a previous PURGE command.`,
|
||||
capabs: []string{"chanreg"},
|
||||
minParams: 1,
|
||||
},
|
||||
"info": {
|
||||
handler: csInfoHandler,
|
||||
help: `Syntax: $INFO #channel$b
|
||||
|
||||
INFO displays info about a registered channel.`,
|
||||
helpShort: `$bINFO$b displays info about a registered channel.`,
|
||||
minParams: 1,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// csNotice sends the client a notice from ChanServ
|
||||
func csNotice(rb *ResponseBuffer, text string) {
|
||||
rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text)
|
||||
rb.Add(nil, chanservMask, "NOTICE", rb.target.Nick(), text)
|
||||
}
|
||||
|
||||
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
@ -219,9 +279,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
||||
}
|
||||
|
||||
account := client.Account()
|
||||
channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account)
|
||||
if server.Config().Channels.Registration.MaxChannelsPerAccount <= len(channelsAlreadyRegistered) {
|
||||
csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||
if !checkChanLimit(client, rb) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -234,7 +292,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
||||
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
|
||||
|
||||
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName))
|
||||
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName))
|
||||
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
|
||||
|
||||
// give them founder privs
|
||||
@ -249,6 +307,17 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
||||
}
|
||||
}
|
||||
|
||||
// check whether a client has already registered too many channels
|
||||
func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
|
||||
account := client.Account()
|
||||
channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
|
||||
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||
if !ok {
|
||||
csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channelName := params[0]
|
||||
var verificationCode string
|
||||
@ -299,3 +368,204 @@ func unregisterConfirmationCode(name string, registeredAt time.Time) (code strin
|
||||
codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16))
|
||||
return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes())))
|
||||
}
|
||||
|
||||
func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel == nil {
|
||||
csNotice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
account := client.Account()
|
||||
if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
|
||||
csNotice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(params[1]) {
|
||||
case "access":
|
||||
channel.resetAccess()
|
||||
csNotice(rb, client.t("Successfully reset channel access"))
|
||||
case "users":
|
||||
for _, target := range channel.Members() {
|
||||
if target != client {
|
||||
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||
}
|
||||
}
|
||||
default:
|
||||
csNotice(rb, client.t("Invalid parameters"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func csTransferHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
if strings.ToLower(params[0]) == "accept" {
|
||||
processTransferAccept(client, params[1], rb)
|
||||
return
|
||||
}
|
||||
chname := params[0]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
csNotice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
regInfo := channel.ExportRegistration(0)
|
||||
chname = regInfo.Name
|
||||
account := client.Account()
|
||||
isFounder := account != "" && account == regInfo.Founder
|
||||
hasPrivs := client.HasRoleCapabs("chanreg")
|
||||
if !(isFounder || hasPrivs) {
|
||||
csNotice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
target := params[1]
|
||||
_, err := server.accounts.LoadAccount(params[1])
|
||||
if err != nil {
|
||||
csNotice(rb, client.t("Account does not exist"))
|
||||
return
|
||||
}
|
||||
expectedCode := unregisterConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
|
||||
codeValidated := 2 < len(params) && params[2] == expectedCode
|
||||
if !codeValidated {
|
||||
csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
|
||||
csNotice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode))
|
||||
return
|
||||
}
|
||||
status, err := channel.Transfer(client, target, hasPrivs)
|
||||
if err == nil {
|
||||
switch status {
|
||||
case channelTransferComplete:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target))
|
||||
case channelTransferPending:
|
||||
sendTransferPendingNotice(server, target, chname)
|
||||
csNotice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target))
|
||||
case channelTransferCancelled:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
|
||||
}
|
||||
} else {
|
||||
csNotice(rb, client.t("Could not transfer channel"))
|
||||
}
|
||||
}
|
||||
|
||||
func sendTransferPendingNotice(server *Server, account, chname string) {
|
||||
clients := server.accounts.AccountToClients(account)
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
var client *Client
|
||||
for _, candidate := range clients {
|
||||
client = candidate
|
||||
if candidate.NickCasefolded() == candidate.Account() {
|
||||
break // prefer the login where the nick is the account
|
||||
}
|
||||
}
|
||||
client.Send(nil, chanservMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %s. To accept, /CS TRANSFER ACCEPT %s"), chname, chname))
|
||||
}
|
||||
|
||||
func processTransferAccept(client *Client, chname string, rb *ResponseBuffer) {
|
||||
channel := client.server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
csNotice(rb, client.t("Channel does not exist"))
|
||||
return
|
||||
}
|
||||
if !checkChanLimit(client, rb) {
|
||||
return
|
||||
}
|
||||
switch channel.AcceptTransfer(client) {
|
||||
case nil:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name()))
|
||||
case errChannelTransferNotOffered:
|
||||
csNotice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name()))
|
||||
default:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
func csPurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
oper := client.Oper()
|
||||
if oper == nil {
|
||||
return // should be impossible because you need oper capabs for this
|
||||
}
|
||||
|
||||
chname := params[0]
|
||||
var reason string
|
||||
if 1 < len(params) {
|
||||
reason = params[1]
|
||||
}
|
||||
purgeRecord := ChannelPurgeRecord{
|
||||
Oper: oper.Name,
|
||||
PurgedAt: time.Now().UTC(),
|
||||
Reason: reason,
|
||||
}
|
||||
switch server.channels.Purge(chname, purgeRecord) {
|
||||
case nil:
|
||||
channel := server.channels.Get(chname)
|
||||
if channel != nil { // channel need not exist to be purged
|
||||
for _, target := range channel.Members() {
|
||||
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||
}
|
||||
}
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
|
||||
case errInvalidChannelName:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
|
||||
default:
|
||||
csNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csUnpurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
chname := params[0]
|
||||
switch server.channels.Unpurge(chname) {
|
||||
case nil:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
|
||||
case errNoSuchChannel:
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
|
||||
default:
|
||||
csNotice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
chname, err := CasefoldChannel(params[0])
|
||||
if err != nil {
|
||||
csNotice(rb, client.t("Invalid channel name"))
|
||||
return
|
||||
}
|
||||
|
||||
// purge status
|
||||
if client.HasRoleCapabs("chanreg") {
|
||||
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
||||
if err == nil {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
|
||||
if purgeRecord.Reason != "" {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if server.channels.IsPurged(chname) {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||
}
|
||||
}
|
||||
|
||||
var chinfo RegisteredChannel
|
||||
channel := server.channels.Get(params[0])
|
||||
if channel != nil {
|
||||
chinfo = channel.ExportRegistration(0)
|
||||
} else {
|
||||
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
||||
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
||||
csNotice(rb, client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// channel exists but is unregistered, or doesn't exist:
|
||||
if chinfo.Founder == "" {
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
|
||||
return
|
||||
}
|
||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
||||
csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
||||
}
|
||||
|
@ -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)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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`)
|
||||
|
@ -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(), " ")...)
|
||||
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 {
|
||||
for _, member := range channel.Members() {
|
||||
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)
|
||||
|
@ -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))
|
||||
|
10
irc/modes.go
10
irc/modes.go
@ -119,6 +119,9 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||
if isSamode {
|
||||
return true
|
||||
}
|
||||
if details.account != "" && details.account == channel.Founder() {
|
||||
return true
|
||||
}
|
||||
switch change.Mode {
|
||||
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
|
||||
// List on these modes is a no-op anyway
|
||||
@ -289,15 +292,16 @@ func (channel *Channel) ProcessAccountToUmodeChange(client *Client, change modes
|
||||
targetModeAfter = change.Mode
|
||||
}
|
||||
|
||||
// operators and founders can do anything
|
||||
// server operators and founders can do anything:
|
||||
hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder)
|
||||
// halfop and up can list, and do add/removes at levels <= their own
|
||||
// halfop and up can list:
|
||||
if change.Op == modes.List && (clientMode == modes.Halfop || umodeGreaterThan(clientMode, modes.Halfop)) {
|
||||
hasPrivs = true
|
||||
// you can do adds or removes at levels you have "privileges over":
|
||||
} else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) {
|
||||
hasPrivs = true
|
||||
// and you can always de-op yourself:
|
||||
} else if change.Op == modes.Remove && account == change.Arg {
|
||||
// you can always de-op yourself
|
||||
hasPrivs = true
|
||||
}
|
||||
if !hasPrivs {
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user