mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-10 22:19:31 +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("Account: %s"), casefoldedAccount) + "\r\n",
|
||||||
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
fmt.Sprintf(client.t("Verification code: %s"), code) + "\r\n",
|
||||||
"\r\n",
|
"\r\n",
|
||||||
client.t("To verify your account, issue one of these commands:") + "\r\n",
|
client.t("To verify your account, issue the following command:") + "\r\n",
|
||||||
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
fmt.Sprintf("/MSG NickServ VERIFY %s %s", casefoldedAccount, code) + "\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
irc/channel.go
119
irc/channel.go
@ -37,6 +37,7 @@ type Channel struct {
|
|||||||
createdTime time.Time
|
createdTime time.Time
|
||||||
registeredFounder string
|
registeredFounder string
|
||||||
registeredTime time.Time
|
registeredTime time.Time
|
||||||
|
transferPendingTo string
|
||||||
topic string
|
topic string
|
||||||
topicSetBy string
|
topicSetBy string
|
||||||
topicSetTime time.Time
|
topicSetTime time.Time
|
||||||
@ -59,22 +60,17 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config := s.Config()
|
||||||
|
|
||||||
channel := &Channel{
|
channel := &Channel{
|
||||||
createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo
|
createdTime: time.Now().UTC(), // may be overwritten by applyRegInfo
|
||||||
lists: map[modes.Mode]*UserMaskSet{
|
|
||||||
modes.BanMask: NewUserMaskSet(),
|
|
||||||
modes.ExceptMask: NewUserMaskSet(),
|
|
||||||
modes.InviteMask: NewUserMaskSet(),
|
|
||||||
},
|
|
||||||
members: make(MemberSet),
|
members: make(MemberSet),
|
||||||
name: name,
|
name: name,
|
||||||
nameCasefolded: casefoldedName,
|
nameCasefolded: casefoldedName,
|
||||||
server: s,
|
server: s,
|
||||||
accountToUMode: make(map[string]modes.Mode),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config := s.Config()
|
channel.initializeLists()
|
||||||
|
|
||||||
channel.writerSemaphore.Initialize(1)
|
channel.writerSemaphore.Initialize(1)
|
||||||
channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
|
channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
|
||||||
|
|
||||||
@ -89,6 +85,15 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
|
|||||||
return channel
|
return channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) initializeLists() {
|
||||||
|
channel.lists = map[modes.Mode]*UserMaskSet{
|
||||||
|
modes.BanMask: NewUserMaskSet(),
|
||||||
|
modes.ExceptMask: NewUserMaskSet(),
|
||||||
|
modes.InviteMask: NewUserMaskSet(),
|
||||||
|
}
|
||||||
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureLoaded blocks until the channel's registration info has been loaded
|
// EnsureLoaded blocks until the channel's registration info has been loaded
|
||||||
// from the database.
|
// from the database.
|
||||||
func (channel *Channel) EnsureLoaded() {
|
func (channel *Channel) EnsureLoaded() {
|
||||||
@ -303,6 +308,18 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
|
|||||||
channel.accountToUMode = make(map[string]modes.Mode)
|
channel.accountToUMode = make(map[string]modes.Mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
|
||||||
|
func (channel *Channel) resetAccess() {
|
||||||
|
defer channel.MarkDirty(IncludeLists)
|
||||||
|
|
||||||
|
channel.stateMutex.Lock()
|
||||||
|
defer channel.stateMutex.Unlock()
|
||||||
|
channel.initializeLists()
|
||||||
|
if channel.registeredFounder != "" {
|
||||||
|
channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// IsRegistered returns whether the channel is registered.
|
// IsRegistered returns whether the channel is registered.
|
||||||
func (channel *Channel) IsRegistered() bool {
|
func (channel *Channel) IsRegistered() bool {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
@ -310,6 +327,78 @@ func (channel *Channel) IsRegistered() bool {
|
|||||||
return channel.registeredFounder != ""
|
return channel.registeredFounder != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type channelTransferStatus uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
channelTransferComplete channelTransferStatus = iota
|
||||||
|
channelTransferPending
|
||||||
|
channelTransferCancelled
|
||||||
|
channelTransferFailed
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transfer transfers ownership of a registered channel to a different account
|
||||||
|
func (channel *Channel) Transfer(client *Client, target string, hasPrivs bool) (status channelTransferStatus, err error) {
|
||||||
|
status = channelTransferFailed
|
||||||
|
defer func() {
|
||||||
|
if status == channelTransferComplete && err == nil {
|
||||||
|
channel.Store(IncludeAllChannelAttrs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cftarget, err := CasefoldName(target)
|
||||||
|
if err != nil {
|
||||||
|
err = errAccountDoesNotExist
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel.stateMutex.Lock()
|
||||||
|
defer channel.stateMutex.Unlock()
|
||||||
|
if channel.registeredFounder == "" {
|
||||||
|
err = errChannelNotOwnedByAccount
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if hasPrivs {
|
||||||
|
channel.transferOwnership(cftarget)
|
||||||
|
return channelTransferComplete, nil
|
||||||
|
} else {
|
||||||
|
if channel.registeredFounder == cftarget {
|
||||||
|
// transferring back to yourself cancels a pending transfer
|
||||||
|
channel.transferPendingTo = ""
|
||||||
|
return channelTransferCancelled, nil
|
||||||
|
} else {
|
||||||
|
channel.transferPendingTo = cftarget
|
||||||
|
return channelTransferPending, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) transferOwnership(newOwner string) {
|
||||||
|
delete(channel.accountToUMode, channel.registeredFounder)
|
||||||
|
channel.registeredFounder = newOwner
|
||||||
|
channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder
|
||||||
|
channel.transferPendingTo = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcceptTransfer implements `CS TRANSFER #chan ACCEPT`
|
||||||
|
func (channel *Channel) AcceptTransfer(client *Client) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
channel.Store(IncludeAllChannelAttrs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
account := client.Account()
|
||||||
|
if account == "" {
|
||||||
|
return errAccountNotLoggedIn
|
||||||
|
}
|
||||||
|
channel.stateMutex.Lock()
|
||||||
|
defer channel.stateMutex.Unlock()
|
||||||
|
if account != channel.transferPendingTo {
|
||||||
|
return errChannelTransferNotOffered
|
||||||
|
}
|
||||||
|
channel.transferOwnership(account)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) regenerateMembersCache() {
|
func (channel *Channel) regenerateMembersCache() {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
result := make([]*Client, len(channel.members))
|
result := make([]*Client, len(channel.members))
|
||||||
@ -1117,21 +1206,23 @@ func (channel *Channel) Quit(client *Client) {
|
|||||||
client.removeChannel(channel)
|
client.removeChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) {
|
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
|
||||||
|
if !hasPrivs {
|
||||||
if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
|
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"))
|
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel"))
|
||||||
return
|
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) {
|
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"))
|
rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges"))
|
||||||
return
|
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 {
|
if len(comment) > kicklimit {
|
||||||
comment = comment[:kicklimit]
|
comment = comment[:kicklimit]
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ type ChannelManager struct {
|
|||||||
sync.RWMutex // tier 2
|
sync.RWMutex // tier 2
|
||||||
chans map[string]*channelManagerEntry
|
chans map[string]*channelManagerEntry
|
||||||
registeredChannels map[string]bool
|
registeredChannels map[string]bool
|
||||||
|
purgedChannels map[string]empty
|
||||||
server *Server
|
server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,9 +38,11 @@ func (cm *ChannelManager) Initialize(server *Server) {
|
|||||||
|
|
||||||
func (cm *ChannelManager) loadRegisteredChannels() {
|
func (cm *ChannelManager) loadRegisteredChannels() {
|
||||||
registeredChannels := cm.server.channelRegistry.AllChannels()
|
registeredChannels := cm.server.channelRegistry.AllChannels()
|
||||||
|
purgedChannels := cm.server.channelRegistry.PurgedChannels()
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
cm.registeredChannels = registeredChannels
|
cm.registeredChannels = registeredChannels
|
||||||
|
cm.purgedChannels = purgedChannels
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||||
@ -69,6 +72,10 @@ func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin
|
|||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
|
_, purged := cm.purgedChannels[casefoldedName]
|
||||||
|
if purged {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
entry := cm.chans[casefoldedName]
|
entry := cm.chans[casefoldedName]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
registered := cm.registeredChannels[casefoldedName]
|
registered := cm.registeredChannels[casefoldedName]
|
||||||
@ -267,3 +274,50 @@ func (cm *ChannelManager) Channels() (result []*Channel) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purge marks a channel as purged.
|
||||||
|
func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err error) {
|
||||||
|
chname, err = CasefoldChannel(chname)
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidChannelName
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
cm.purgedChannels[chname] = empty{}
|
||||||
|
cm.Unlock()
|
||||||
|
|
||||||
|
cm.server.channelRegistry.PurgeChannel(chname, record)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPurged queries whether a channel is purged.
|
||||||
|
func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
||||||
|
chname, err := CasefoldChannel(chname)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
_, result = cm.purgedChannels[chname]
|
||||||
|
cm.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpurge deletes a channel's purged status.
|
||||||
|
func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
||||||
|
chname, err = CasefoldChannel(chname)
|
||||||
|
if err != nil {
|
||||||
|
return errNoSuchChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
_, found := cm.purgedChannels[chname]
|
||||||
|
delete(cm.purgedChannels, chname)
|
||||||
|
cm.Unlock()
|
||||||
|
|
||||||
|
cm.server.channelRegistry.UnpurgeChannel(chname)
|
||||||
|
if !found {
|
||||||
|
return errNoSuchChannel
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -32,6 +32,8 @@ const (
|
|||||||
keyChannelPassword = "channel.key %s"
|
keyChannelPassword = "channel.key %s"
|
||||||
keyChannelModes = "channel.modes %s"
|
keyChannelModes = "channel.modes %s"
|
||||||
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
keyChannelAccountToUMode = "channel.accounttoumode %s"
|
||||||
|
|
||||||
|
keyChannelPurged = "channel.purged %s"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -96,6 +98,12 @@ type RegisteredChannel struct {
|
|||||||
Invites map[string]MaskInfo
|
Invites map[string]MaskInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChannelPurgeRecord struct {
|
||||||
|
Oper string
|
||||||
|
PurgedAt time.Time
|
||||||
|
Reason string
|
||||||
|
}
|
||||||
|
|
||||||
// ChannelRegistry manages registered channels.
|
// ChannelRegistry manages registered channels.
|
||||||
type ChannelRegistry struct {
|
type ChannelRegistry struct {
|
||||||
server *Server
|
server *Server
|
||||||
@ -124,6 +132,24 @@ func (reg *ChannelRegistry) AllChannels() (result map[string]bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurgedChannels returns the set of all channel names that have been purged
|
||||||
|
func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) {
|
||||||
|
result = make(map[string]empty)
|
||||||
|
|
||||||
|
prefix := fmt.Sprintf(keyChannelPurged, "")
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
channel := strings.TrimPrefix(key, prefix)
|
||||||
|
result[channel] = empty{}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
// StoreChannel obtains a consistent view of a channel, then persists it to the store.
|
||||||
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
|
func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) {
|
||||||
if !reg.server.ChannelRegistrationEnabled() {
|
if !reg.server.ChannelRegistrationEnabled() {
|
||||||
@ -191,11 +217,11 @@ func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredC
|
|||||||
|
|
||||||
info = RegisteredChannel{
|
info = RegisteredChannel{
|
||||||
Name: name,
|
Name: name,
|
||||||
RegisteredAt: time.Unix(regTimeInt, 0),
|
RegisteredAt: time.Unix(regTimeInt, 0).UTC(),
|
||||||
Founder: founder,
|
Founder: founder,
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
TopicSetBy: topicSetBy,
|
TopicSetBy: topicSetBy,
|
||||||
TopicSetTime: time.Unix(topicSetTimeInt, 0),
|
TopicSetTime: time.Unix(topicSetTimeInt, 0).UTC(),
|
||||||
Key: password,
|
Key: password,
|
||||||
Modes: modeSlice,
|
Modes: modeSlice,
|
||||||
Bans: banlist,
|
Bans: banlist,
|
||||||
@ -256,14 +282,12 @@ func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info Regist
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// saveChannel saves a channel to the store.
|
func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) {
|
||||||
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
|
|
||||||
channelKey := channelInfo.NameCasefolded
|
channelKey := channelInfo.NameCasefolded
|
||||||
// maintain the mapping of account -> registered channels
|
chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey)
|
||||||
chanExistsKey := fmt.Sprintf(keyChannelExists, channelKey)
|
founder, existsErr := tx.Get(chanFounderKey)
|
||||||
_, existsErr := tx.Get(chanExistsKey)
|
if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder {
|
||||||
if existsErr == buntdb.ErrNotFound {
|
// add to new founder's list
|
||||||
// this is a new registration, need to update account-to-channels
|
|
||||||
accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
|
accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder)
|
||||||
alreadyChannels, _ := tx.Get(accountChannelsKey)
|
alreadyChannels, _ := tx.Get(accountChannelsKey)
|
||||||
newChannels := channelKey // this is the casefolded channel name
|
newChannels := channelKey // this is the casefolded channel name
|
||||||
@ -272,9 +296,30 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
|
|||||||
}
|
}
|
||||||
tx.Set(accountChannelsKey, newChannels, nil)
|
tx.Set(accountChannelsKey, newChannels, nil)
|
||||||
}
|
}
|
||||||
|
if existsErr == nil && founder != channelInfo.Founder {
|
||||||
|
// remove from old founder's list
|
||||||
|
accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder)
|
||||||
|
alreadyChannelsRaw, _ := tx.Get(accountChannelsKey)
|
||||||
|
var newChannels []string
|
||||||
|
if alreadyChannelsRaw != "" {
|
||||||
|
for _, chname := range strings.Split(alreadyChannelsRaw, ",") {
|
||||||
|
if chname != channelInfo.NameCasefolded {
|
||||||
|
newChannels = append(newChannels, chname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveChannel saves a channel to the store.
|
||||||
|
func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) {
|
||||||
|
channelKey := channelInfo.NameCasefolded
|
||||||
|
// maintain the mapping of account -> registered channels
|
||||||
|
reg.updateAccountToChannelMapping(tx, channelInfo)
|
||||||
|
|
||||||
if includeFlags&IncludeInitial != 0 {
|
if includeFlags&IncludeInitial != 0 {
|
||||||
tx.Set(chanExistsKey, "1", nil)
|
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
||||||
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
|
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
|
||||||
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
|
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
|
||||||
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
||||||
@ -306,3 +351,48 @@ func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredCha
|
|||||||
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurgeChannel records a channel purge.
|
||||||
|
func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) {
|
||||||
|
serialized, err := json.Marshal(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
serializedStr := string(serialized)
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
|
||||||
|
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
tx.Set(key, serializedStr, nil)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPurgeRecord retrieves information about whether and how a channel was purged.
|
||||||
|
func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) {
|
||||||
|
var rawRecord string
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
rawRecord, _ = tx.Get(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if rawRecord == "" {
|
||||||
|
err = errNoSuchChannel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal([]byte(rawRecord), &record)
|
||||||
|
if err != nil {
|
||||||
|
reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error())
|
||||||
|
err = errNoSuchChannel
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnpurgeChannel deletes the record of a channel purge.
|
||||||
|
func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) {
|
||||||
|
key := fmt.Sprintf(keyChannelPurged, chname)
|
||||||
|
return reg.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
|
tx.Delete(key)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
280
irc/chanserv.go
280
irc/chanserv.go
@ -18,6 +18,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const chanservHelp = `ChanServ lets you register and manage channels.`
|
const chanservHelp = `ChanServ lets you register and manage channels.`
|
||||||
|
const chanservMask = "ChanServ!ChanServ@localhost"
|
||||||
|
|
||||||
func chanregEnabled(config *Config) bool {
|
func chanregEnabled(config *Config) bool {
|
||||||
return config.Channels.Registration.Enabled
|
return config.Channels.Registration.Enabled
|
||||||
@ -75,12 +76,71 @@ referenced by their registered account names, not their nicknames.`,
|
|||||||
enabled: chanregEnabled,
|
enabled: chanregEnabled,
|
||||||
minParams: 1,
|
minParams: 1,
|
||||||
},
|
},
|
||||||
|
"clear": {
|
||||||
|
handler: csClearHandler,
|
||||||
|
help: `Syntax: $bCLEAR #channel target$b
|
||||||
|
|
||||||
|
CLEAR removes users or settings from a channel. Specifically:
|
||||||
|
|
||||||
|
$bCLEAR #channel users$b kicks all users except for you.
|
||||||
|
$bCLEAR #channel access$b resets all stored bans, invites, ban exceptions,
|
||||||
|
and persistent user-mode grants made with CS AMODE.`,
|
||||||
|
helpShort: `$bCLEAR$b removes users or settings from a channel.`,
|
||||||
|
enabled: chanregEnabled,
|
||||||
|
minParams: 2,
|
||||||
|
},
|
||||||
|
"transfer": {
|
||||||
|
handler: csTransferHandler,
|
||||||
|
help: `Syntax: $bTRANSFER [accept] #channel user [code]$b
|
||||||
|
|
||||||
|
TRANSFER transfers ownership of a channel from one user to another.
|
||||||
|
To prevent accidental transfers, a verification code is required. For
|
||||||
|
example, $bTRANSFER #channel alice$b displays the required confirmation
|
||||||
|
code, then $bTRANSFER #channel alice 2930242125$b initiates the transfer.
|
||||||
|
Unless you are an IRC operator with the correct permissions, alice must
|
||||||
|
then accept the transfer, which she can do with $bTRANSFER accept #channel$b.`,
|
||||||
|
helpShort: `$bTRANSFER$b transfers ownership of a channel to another user.`,
|
||||||
|
enabled: chanregEnabled,
|
||||||
|
minParams: 2,
|
||||||
|
},
|
||||||
|
"purge": {
|
||||||
|
handler: csPurgeHandler,
|
||||||
|
help: `Syntax: $bPURGE #channel [reason]$b
|
||||||
|
|
||||||
|
PURGE blacklists a channel from the server, making it impossible to join
|
||||||
|
or otherwise interact with the channel. If the channel currently has members,
|
||||||
|
they will be kicked from it. PURGE may also be applied preemptively to
|
||||||
|
channels that do not currently have members.`,
|
||||||
|
helpShort: `$bPURGE$b blacklists a channel from the server.`,
|
||||||
|
capabs: []string{"chanreg"},
|
||||||
|
minParams: 1,
|
||||||
|
maxParams: 2,
|
||||||
|
unsplitFinalParam: true,
|
||||||
|
},
|
||||||
|
"unpurge": {
|
||||||
|
handler: csUnpurgeHandler,
|
||||||
|
help: `Syntax: $bUNPURGE #channel$b
|
||||||
|
|
||||||
|
UNPURGE removes any blacklisting of a channel that was previously
|
||||||
|
set using PURGE.`,
|
||||||
|
helpShort: `$bUNPURGE$b undoes a previous PURGE command.`,
|
||||||
|
capabs: []string{"chanreg"},
|
||||||
|
minParams: 1,
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
handler: csInfoHandler,
|
||||||
|
help: `Syntax: $INFO #channel$b
|
||||||
|
|
||||||
|
INFO displays info about a registered channel.`,
|
||||||
|
helpShort: `$bINFO$b displays info about a registered channel.`,
|
||||||
|
minParams: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// csNotice sends the client a notice from ChanServ
|
// csNotice sends the client a notice from ChanServ
|
||||||
func csNotice(rb *ResponseBuffer, text string) {
|
func csNotice(rb *ResponseBuffer, text string) {
|
||||||
rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text)
|
rb.Add(nil, chanservMask, "NOTICE", rb.target.Nick(), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
@ -219,9 +279,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
|||||||
}
|
}
|
||||||
|
|
||||||
account := client.Account()
|
account := client.Account()
|
||||||
channelsAlreadyRegistered := server.accounts.ChannelsForAccount(account)
|
if !checkChanLimit(client, rb) {
|
||||||
if server.Config().Channels.Registration.MaxChannelsPerAccount <= len(channelsAlreadyRegistered) {
|
|
||||||
csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,7 +292,7 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
|||||||
|
|
||||||
csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName))
|
||||||
|
|
||||||
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName))
|
server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.Nick(), channelName))
|
||||||
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
|
server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString))
|
||||||
|
|
||||||
// give them founder privs
|
// give them founder privs
|
||||||
@ -249,6 +307,17 @@ func csRegisterHandler(server *Server, client *Client, command string, params []
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check whether a client has already registered too many channels
|
||||||
|
func checkChanLimit(client *Client, rb *ResponseBuffer) (ok bool) {
|
||||||
|
account := client.Account()
|
||||||
|
channelsAlreadyRegistered := client.server.accounts.ChannelsForAccount(account)
|
||||||
|
ok = len(channelsAlreadyRegistered) < client.server.Config().Channels.Registration.MaxChannelsPerAccount || client.HasRoleCapabs("chanreg")
|
||||||
|
if !ok {
|
||||||
|
csNotice(rb, client.t("You have already registered the maximum number of channels; try dropping some with /CS UNREGISTER"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
channelName := params[0]
|
channelName := params[0]
|
||||||
var verificationCode string
|
var verificationCode string
|
||||||
@ -299,3 +368,204 @@ func unregisterConfirmationCode(name string, registeredAt time.Time) (code strin
|
|||||||
codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16))
|
codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16))
|
||||||
return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes())))
|
return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
channel := server.channels.Get(params[0])
|
||||||
|
if channel == nil {
|
||||||
|
csNotice(rb, client.t("Channel does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account := client.Account()
|
||||||
|
if !(client.HasRoleCapabs("chanreg") || (account != "" && account == channel.Founder())) {
|
||||||
|
csNotice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(params[1]) {
|
||||||
|
case "access":
|
||||||
|
channel.resetAccess()
|
||||||
|
csNotice(rb, client.t("Successfully reset channel access"))
|
||||||
|
case "users":
|
||||||
|
for _, target := range channel.Members() {
|
||||||
|
if target != client {
|
||||||
|
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
csNotice(rb, client.t("Invalid parameters"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func csTransferHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
if strings.ToLower(params[0]) == "accept" {
|
||||||
|
processTransferAccept(client, params[1], rb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chname := params[0]
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
csNotice(rb, client.t("Channel does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
regInfo := channel.ExportRegistration(0)
|
||||||
|
chname = regInfo.Name
|
||||||
|
account := client.Account()
|
||||||
|
isFounder := account != "" && account == regInfo.Founder
|
||||||
|
hasPrivs := client.HasRoleCapabs("chanreg")
|
||||||
|
if !(isFounder || hasPrivs) {
|
||||||
|
csNotice(rb, client.t("Insufficient privileges"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
target := params[1]
|
||||||
|
_, err := server.accounts.LoadAccount(params[1])
|
||||||
|
if err != nil {
|
||||||
|
csNotice(rb, client.t("Account does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
expectedCode := unregisterConfirmationCode(regInfo.Name, regInfo.RegisteredAt)
|
||||||
|
codeValidated := 2 < len(params) && params[2] == expectedCode
|
||||||
|
if !codeValidated {
|
||||||
|
csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b")))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("To confirm your channel transfer, type: /CS TRANSFER %[1]s %[2]s %[3]s"), chname, target, expectedCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
status, err := channel.Transfer(client, target, hasPrivs)
|
||||||
|
if err == nil {
|
||||||
|
switch status {
|
||||||
|
case channelTransferComplete:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Successfully transferred channel %[1]s to account %[2]s"), chname, target))
|
||||||
|
case channelTransferPending:
|
||||||
|
sendTransferPendingNotice(server, target, chname)
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Transfer of channel %[1]s to account %[2]s succeeded, pending acceptance"), chname, target))
|
||||||
|
case channelTransferCancelled:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Cancelled pending transfer of channel %s"), chname))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
csNotice(rb, client.t("Could not transfer channel"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTransferPendingNotice(server *Server, account, chname string) {
|
||||||
|
clients := server.accounts.AccountToClients(account)
|
||||||
|
if len(clients) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var client *Client
|
||||||
|
for _, candidate := range clients {
|
||||||
|
client = candidate
|
||||||
|
if candidate.NickCasefolded() == candidate.Account() {
|
||||||
|
break // prefer the login where the nick is the account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.Send(nil, chanservMask, "NOTICE", client.Nick(), fmt.Sprintf(client.t("You have been offered ownership of channel %s. To accept, /CS TRANSFER ACCEPT %s"), chname, chname))
|
||||||
|
}
|
||||||
|
|
||||||
|
func processTransferAccept(client *Client, chname string, rb *ResponseBuffer) {
|
||||||
|
channel := client.server.channels.Get(chname)
|
||||||
|
if channel == nil {
|
||||||
|
csNotice(rb, client.t("Channel does not exist"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !checkChanLimit(client, rb) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch channel.AcceptTransfer(client) {
|
||||||
|
case nil:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Successfully accepted ownership of channel %s"), channel.Name()))
|
||||||
|
case errChannelTransferNotOffered:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("You weren't offered ownership of channel %s"), channel.Name()))
|
||||||
|
default:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Could not accept ownership of channel %s"), channel.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func csPurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
oper := client.Oper()
|
||||||
|
if oper == nil {
|
||||||
|
return // should be impossible because you need oper capabs for this
|
||||||
|
}
|
||||||
|
|
||||||
|
chname := params[0]
|
||||||
|
var reason string
|
||||||
|
if 1 < len(params) {
|
||||||
|
reason = params[1]
|
||||||
|
}
|
||||||
|
purgeRecord := ChannelPurgeRecord{
|
||||||
|
Oper: oper.Name,
|
||||||
|
PurgedAt: time.Now().UTC(),
|
||||||
|
Reason: reason,
|
||||||
|
}
|
||||||
|
switch server.channels.Purge(chname, purgeRecord) {
|
||||||
|
case nil:
|
||||||
|
channel := server.channels.Get(chname)
|
||||||
|
if channel != nil { // channel need not exist to be purged
|
||||||
|
for _, target := range channel.Members() {
|
||||||
|
channel.Kick(client, target, "Cleared by ChanServ", rb, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Successfully purged channel %s from the server"), chname))
|
||||||
|
case errInvalidChannelName:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Can't purge invalid channel %s"), chname))
|
||||||
|
default:
|
||||||
|
csNotice(rb, client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func csUnpurgeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
chname := params[0]
|
||||||
|
switch server.channels.Unpurge(chname) {
|
||||||
|
case nil:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Successfully unpurged channel %s from the server"), chname))
|
||||||
|
case errNoSuchChannel:
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s wasn't previously purged from the server"), chname))
|
||||||
|
default:
|
||||||
|
csNotice(rb, client.t("An error occurred"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func csInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||||
|
chname, err := CasefoldChannel(params[0])
|
||||||
|
if err != nil {
|
||||||
|
csNotice(rb, client.t("Invalid channel name"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// purge status
|
||||||
|
if client.HasRoleCapabs("chanreg") {
|
||||||
|
purgeRecord, err := server.channelRegistry.LoadPurgeRecord(chname)
|
||||||
|
if err == nil {
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Purged by operator: %s"), purgeRecord.Oper))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Purged at: %s"), purgeRecord.PurgedAt.Format(time.RFC1123)))
|
||||||
|
if purgeRecord.Reason != "" {
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Purge reason: %s"), purgeRecord.Reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if server.channels.IsPurged(chname) {
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s was purged by the server operators and cannot be used"), chname))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var chinfo RegisteredChannel
|
||||||
|
channel := server.channels.Get(params[0])
|
||||||
|
if channel != nil {
|
||||||
|
chinfo = channel.ExportRegistration(0)
|
||||||
|
} else {
|
||||||
|
chinfo, err = server.channelRegistry.LoadChannel(chname)
|
||||||
|
if err != nil && !(err == errNoSuchChannel || err == errFeatureDisabled) {
|
||||||
|
csNotice(rb, client.t("An error occurred"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel exists but is unregistered, or doesn't exist:
|
||||||
|
if chinfo.Founder == "" {
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s is not registered"), chname))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Channel %s is registered"), chinfo.Name))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Founder: %s"), chinfo.Founder))
|
||||||
|
csNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), chinfo.RegisteredAt.Format(time.RFC1123)))
|
||||||
|
}
|
||||||
|
@ -637,11 +637,11 @@ func (session *Session) playResume() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !session.resumeDetails.HistoryIncomplete {
|
if !session.resumeDetails.HistoryIncomplete {
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
|
fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected"))
|
||||||
} else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
} else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds))
|
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds))
|
||||||
} else {
|
} else {
|
||||||
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (message history may have been lost)")))
|
fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected (message history may have been lost)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ var (
|
|||||||
errCallbackFailed = errors.New("Account verification could not be sent")
|
errCallbackFailed = errors.New("Account verification could not be sent")
|
||||||
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
errCertfpAlreadyExists = errors.New(`An account already exists for your certificate fingerprint`)
|
||||||
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
errChannelNotOwnedByAccount = errors.New("Channel not owned by the specified account")
|
||||||
|
errChannelTransferNotOffered = errors.New(`You weren't offered ownership of that channel`)
|
||||||
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
errChannelAlreadyRegistered = errors.New("Channel is already registered")
|
||||||
errChannelNameInUse = errors.New(`Channel name in use`)
|
errChannelNameInUse = errors.New(`Channel name in use`)
|
||||||
errInvalidChannelName = errors.New(`Invalid channel name`)
|
errInvalidChannelName = errors.New(`Invalid channel name`)
|
||||||
|
@ -1370,7 +1370,7 @@ func kickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
if comment == "" {
|
if comment == "" {
|
||||||
comment = kick.nick
|
comment = kick.nick
|
||||||
}
|
}
|
||||||
channel.Kick(client, target, comment, rb)
|
channel.Kick(client, target, comment, rb, false)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1725,15 +1725,14 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
|||||||
prefix := client.NickMaskString()
|
prefix := client.NickMaskString()
|
||||||
//TODO(dan): we should change the name of String and make it return a slice here
|
//TODO(dan): we should change the name of String and make it return a slice here
|
||||||
args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
|
args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
|
||||||
for _, member := range channel.Members() {
|
|
||||||
if member == client {
|
|
||||||
rb.Add(nil, prefix, "MODE", args...)
|
rb.Add(nil, prefix, "MODE", args...)
|
||||||
for _, session := range client.Sessions() {
|
for _, session := range client.Sessions() {
|
||||||
if session != rb.session {
|
if session != rb.session {
|
||||||
session.Send(nil, prefix, "MODE", args...)
|
session.Send(nil, prefix, "MODE", args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
for _, member := range channel.Members() {
|
||||||
|
if member != client {
|
||||||
member.Send(nil, prefix, "MODE", args...)
|
member.Send(nil, prefix, "MODE", args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2359,7 +2358,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
if reason != "" {
|
if reason != "" {
|
||||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
|
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
|
||||||
} else {
|
} else {
|
||||||
targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
|
targetRb.Add(nil, targetPrefix, "PART", oldName, mcl.t("Channel renamed"))
|
||||||
}
|
}
|
||||||
if mSession.capabilities.Has(caps.ExtendedJoin) {
|
if mSession.capabilities.Has(caps.ExtendedJoin) {
|
||||||
targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname)
|
targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname)
|
||||||
|
@ -186,7 +186,7 @@ func hsRequestHandler(server *Server, client *Client, command string, params []s
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
hsNotice(rb, client.t("An error occurred"))
|
hsNotice(rb, client.t("An error occurred"))
|
||||||
} else {
|
} else {
|
||||||
hsNotice(rb, fmt.Sprintf(client.t("Your vhost request will be reviewed by an administrator")))
|
hsNotice(rb, client.t("Your vhost request will be reviewed by an administrator"))
|
||||||
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
|
chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
|
||||||
hsNotifyChannel(server, chanMsg)
|
hsNotifyChannel(server, chanMsg)
|
||||||
// TODO send admins a snomask of some kind
|
// TODO send admins a snomask of some kind
|
||||||
@ -221,7 +221,7 @@ func hsStatusHandler(server *Server, client *Client, command string, params []st
|
|||||||
if account.VHost.ApprovedVHost != "" {
|
if account.VHost.ApprovedVHost != "" {
|
||||||
hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
|
hsNotice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
|
||||||
if !account.VHost.Enabled {
|
if !account.VHost.Enabled {
|
||||||
hsNotice(rb, fmt.Sprintf(client.t("This vhost is currently disabled, but can be enabled with /HS ON")))
|
hsNotice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
|
hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
|
||||||
|
10
irc/modes.go
10
irc/modes.go
@ -119,6 +119,9 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
|||||||
if isSamode {
|
if isSamode {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if details.account != "" && details.account == channel.Founder() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
switch change.Mode {
|
switch change.Mode {
|
||||||
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
|
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
|
||||||
// List on these modes is a no-op anyway
|
// List on these modes is a no-op anyway
|
||||||
@ -289,15 +292,16 @@ func (channel *Channel) ProcessAccountToUmodeChange(client *Client, change modes
|
|||||||
targetModeAfter = change.Mode
|
targetModeAfter = change.Mode
|
||||||
}
|
}
|
||||||
|
|
||||||
// operators and founders can do anything
|
// server operators and founders can do anything:
|
||||||
hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder)
|
hasPrivs := isOperChange || (account != "" && account == channel.registeredFounder)
|
||||||
// halfop and up can list, and do add/removes at levels <= their own
|
// halfop and up can list:
|
||||||
if change.Op == modes.List && (clientMode == modes.Halfop || umodeGreaterThan(clientMode, modes.Halfop)) {
|
if change.Op == modes.List && (clientMode == modes.Halfop || umodeGreaterThan(clientMode, modes.Halfop)) {
|
||||||
hasPrivs = true
|
hasPrivs = true
|
||||||
|
// you can do adds or removes at levels you have "privileges over":
|
||||||
} else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) {
|
} else if channelUserModeHasPrivsOver(clientMode, targetModeNow) && channelUserModeHasPrivsOver(clientMode, targetModeAfter) {
|
||||||
hasPrivs = true
|
hasPrivs = true
|
||||||
|
// and you can always de-op yourself:
|
||||||
} else if change.Op == modes.Remove && account == change.Arg {
|
} else if change.Op == modes.Remove && account == change.Arg {
|
||||||
// you can always de-op yourself
|
|
||||||
hasPrivs = true
|
hasPrivs = true
|
||||||
}
|
}
|
||||||
if !hasPrivs {
|
if !hasPrivs {
|
||||||
|
@ -310,19 +310,19 @@ func displaySetting(settingName string, settings AccountSettings, client *Client
|
|||||||
}
|
}
|
||||||
case "bouncer":
|
case "bouncer":
|
||||||
if !config.Accounts.Bouncer.Enabled {
|
if !config.Accounts.Bouncer.Enabled {
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators")))
|
nsNotice(rb, client.t("This feature has been disabled by the server administrators"))
|
||||||
} else {
|
} else {
|
||||||
switch settings.AllowBouncer {
|
switch settings.AllowBouncer {
|
||||||
case BouncerAllowedServerDefault:
|
case BouncerAllowedServerDefault:
|
||||||
if config.Accounts.Bouncer.AllowedByDefault {
|
if config.Accounts.Bouncer.AllowedByDefault {
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out")))
|
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account, but you can opt out"))
|
||||||
} else {
|
} else {
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in")))
|
nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account, but you can opt in"))
|
||||||
}
|
}
|
||||||
case BouncerDisallowedByUser:
|
case BouncerDisallowedByUser:
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account")))
|
nsNotice(rb, client.t("Bouncer functionality is currently disabled for your account"))
|
||||||
case BouncerAllowedByUser:
|
case BouncerAllowedByUser:
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account")))
|
nsNotice(rb, client.t("Bouncer functionality is currently enabled for your account"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -569,7 +569,7 @@ func nsInfoHandler(server *Server, client *Client, command string, params []stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Account: %s"), account.Name))
|
nsNotice(rb, fmt.Sprintf(client.t("Account: %s"), account.Name))
|
||||||
registeredAt := account.RegisteredAt.Format("Jan 02, 2006 15:04:05Z")
|
registeredAt := account.RegisteredAt.Format(time.RFC1123)
|
||||||
nsNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
|
nsNotice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
|
||||||
// TODO nicer formatting for this
|
// TODO nicer formatting for this
|
||||||
for _, nick := range account.AdditionalNicks {
|
for _, nick := range account.AdditionalNicks {
|
||||||
|
Loading…
Reference in New Issue
Block a user