mirror of
				https://github.com/ergochat/ergo.git
				synced 2025-11-03 23:37:22 +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",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										127
									
								
								irc/channel.go
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								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(),
 | 
			
		||||
		},
 | 
			
		||||
		createdTime:    time.Now().UTC(), // may be overwritten by applyRegInfo
 | 
			
		||||
		members:        make(MemberSet),
 | 
			
		||||
		name:           name,
 | 
			
		||||
		nameCasefolded: casefoldedName,
 | 
			
		||||
		server:         s,
 | 
			
		||||
		accountToUMode: make(map[string]modes.Mode),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := s.Config()
 | 
			
		||||
 | 
			
		||||
	channel.initializeLists()
 | 
			
		||||
	channel.writerSemaphore.Initialize(1)
 | 
			
		||||
	channel.history.Initialize(config.History.ChannelLength, config.History.AutoresizeWindow)
 | 
			
		||||
 | 
			
		||||
@ -89,6 +85,15 @@ func NewChannel(s *Server, name string, registered bool) *Channel {
 | 
			
		||||
	return channel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (channel *Channel) initializeLists() {
 | 
			
		||||
	channel.lists = map[modes.Mode]*UserMaskSet{
 | 
			
		||||
		modes.BanMask:    NewUserMaskSet(),
 | 
			
		||||
		modes.ExceptMask: NewUserMaskSet(),
 | 
			
		||||
		modes.InviteMask: NewUserMaskSet(),
 | 
			
		||||
	}
 | 
			
		||||
	channel.accountToUMode = make(map[string]modes.Mode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EnsureLoaded blocks until the channel's registration info has been loaded
 | 
			
		||||
// from the database.
 | 
			
		||||
func (channel *Channel) EnsureLoaded() {
 | 
			
		||||
@ -303,6 +308,18 @@ func (channel *Channel) SetUnregistered(expectedFounder string) {
 | 
			
		||||
	channel.accountToUMode = make(map[string]modes.Mode)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// implements `CHANSERV CLEAR #chan ACCESS` (resets bans, invites, excepts, and amodes)
 | 
			
		||||
func (channel *Channel) resetAccess() {
 | 
			
		||||
	defer channel.MarkDirty(IncludeLists)
 | 
			
		||||
 | 
			
		||||
	channel.stateMutex.Lock()
 | 
			
		||||
	defer channel.stateMutex.Unlock()
 | 
			
		||||
	channel.initializeLists()
 | 
			
		||||
	if channel.registeredFounder != "" {
 | 
			
		||||
		channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsRegistered returns whether the channel is registered.
 | 
			
		||||
func (channel *Channel) IsRegistered() bool {
 | 
			
		||||
	channel.stateMutex.RLock()
 | 
			
		||||
@ -310,6 +327,78 @@ func (channel *Channel) IsRegistered() bool {
 | 
			
		||||
	return channel.registeredFounder != ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type channelTransferStatus uint
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	channelTransferComplete channelTransferStatus = iota
 | 
			
		||||
	channelTransferPending
 | 
			
		||||
	channelTransferCancelled
 | 
			
		||||
	channelTransferFailed
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Transfer transfers ownership of a registered channel to a different account
 | 
			
		||||
func (channel *Channel) Transfer(client *Client, target string, hasPrivs bool) (status channelTransferStatus, err error) {
 | 
			
		||||
	status = channelTransferFailed
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if status == channelTransferComplete && err == nil {
 | 
			
		||||
			channel.Store(IncludeAllChannelAttrs)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	cftarget, err := CasefoldName(target)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		err = errAccountDoesNotExist
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	channel.stateMutex.Lock()
 | 
			
		||||
	defer channel.stateMutex.Unlock()
 | 
			
		||||
	if channel.registeredFounder == "" {
 | 
			
		||||
		err = errChannelNotOwnedByAccount
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if hasPrivs {
 | 
			
		||||
		channel.transferOwnership(cftarget)
 | 
			
		||||
		return channelTransferComplete, nil
 | 
			
		||||
	} else {
 | 
			
		||||
		if channel.registeredFounder == cftarget {
 | 
			
		||||
			// transferring back to yourself cancels a pending transfer
 | 
			
		||||
			channel.transferPendingTo = ""
 | 
			
		||||
			return channelTransferCancelled, nil
 | 
			
		||||
		} else {
 | 
			
		||||
			channel.transferPendingTo = cftarget
 | 
			
		||||
			return channelTransferPending, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (channel *Channel) transferOwnership(newOwner string) {
 | 
			
		||||
	delete(channel.accountToUMode, channel.registeredFounder)
 | 
			
		||||
	channel.registeredFounder = newOwner
 | 
			
		||||
	channel.accountToUMode[channel.registeredFounder] = modes.ChannelFounder
 | 
			
		||||
	channel.transferPendingTo = ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// AcceptTransfer implements `CS TRANSFER #chan ACCEPT`
 | 
			
		||||
func (channel *Channel) AcceptTransfer(client *Client) (err error) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			channel.Store(IncludeAllChannelAttrs)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	account := client.Account()
 | 
			
		||||
	if account == "" {
 | 
			
		||||
		return errAccountNotLoggedIn
 | 
			
		||||
	}
 | 
			
		||||
	channel.stateMutex.Lock()
 | 
			
		||||
	defer channel.stateMutex.Unlock()
 | 
			
		||||
	if account != channel.transferPendingTo {
 | 
			
		||||
		return errChannelTransferNotOffered
 | 
			
		||||
	}
 | 
			
		||||
	channel.transferOwnership(account)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (channel *Channel) regenerateMembersCache() {
 | 
			
		||||
	channel.stateMutex.RLock()
 | 
			
		||||
	result := make([]*Client, len(channel.members))
 | 
			
		||||
@ -1117,21 +1206,23 @@ func (channel *Channel) Quit(client *Client) {
 | 
			
		||||
	client.removeChannel(channel)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) {
 | 
			
		||||
	if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
 | 
			
		||||
		rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel"))
 | 
			
		||||
		return
 | 
			
		||||
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer, hasPrivs bool) {
 | 
			
		||||
	if !hasPrivs {
 | 
			
		||||
		if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
 | 
			
		||||
			rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel"))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !channel.ClientHasPrivsOver(client, target) {
 | 
			
		||||
			rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges"))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if !channel.hasClient(target) {
 | 
			
		||||
		rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if !channel.ClientHasPrivsOver(client, target) {
 | 
			
		||||
		rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges"))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	kicklimit := client.server.Config().Limits.KickLen
 | 
			
		||||
	kicklimit := channel.server.Config().Limits.KickLen
 | 
			
		||||
	if len(comment) > kicklimit {
 | 
			
		||||
		comment = comment[:kicklimit]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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(), " ")...)
 | 
			
		||||
		rb.Add(nil, prefix, "MODE", args...)
 | 
			
		||||
		for _, session := range client.Sessions() {
 | 
			
		||||
			if session != rb.session {
 | 
			
		||||
				session.Send(nil, prefix, "MODE", args...)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		for _, member := range channel.Members() {
 | 
			
		||||
			if member == client {
 | 
			
		||||
				rb.Add(nil, prefix, "MODE", args...)
 | 
			
		||||
				for _, session := range client.Sessions() {
 | 
			
		||||
					if session != rb.session {
 | 
			
		||||
						session.Send(nil, prefix, "MODE", args...)
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
			if member != client {
 | 
			
		||||
				member.Send(nil, prefix, "MODE", args...)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
@ -2359,7 +2358,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
 | 
			
		||||
				if reason != "" {
 | 
			
		||||
					targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
 | 
			
		||||
				} else {
 | 
			
		||||
					targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
 | 
			
		||||
					targetRb.Add(nil, targetPrefix, "PART", oldName, mcl.t("Channel renamed"))
 | 
			
		||||
				}
 | 
			
		||||
				if mSession.capabilities.Has(caps.ExtendedJoin) {
 | 
			
		||||
					targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname)
 | 
			
		||||
 | 
			
		||||
@ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user