mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-26 05:49:25 +01:00
Merge pull request #692 from slingamn/issue683_channelpurge.1
chanserv enhancements and miscellaneous fixes
This commit is contained in:
commit
bf5a02f077
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
133
irc/channel.go
133
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
|
||||||
@ -52,29 +53,18 @@ type Channel struct {
|
|||||||
|
|
||||||
// NewChannel creates a new channel from a `Server` and a `name`
|
// NewChannel creates a new channel from a `Server` and a `name`
|
||||||
// string, which must be unique on the server.
|
// string, which must be unique on the server.
|
||||||
func NewChannel(s *Server, name string, registered bool) *Channel {
|
func NewChannel(s *Server, name, casefoldedName string, registered bool) *Channel {
|
||||||
casefoldedName, err := CasefoldChannel(name)
|
config := s.Config()
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("internal", "Bad channel name", name, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +79,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 +302,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 +321,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))
|
||||||
@ -1128,21 +1211,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 !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
|
if !hasPrivs {
|
||||||
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel"))
|
if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
|
||||||
return
|
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) {
|
if !channel.hasClient(target) {
|
||||||
rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel"))
|
rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel"))
|
||||||
return
|
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 {
|
if len(comment) > kicklimit {
|
||||||
comment = comment[:kicklimit]
|
comment = comment[:kicklimit]
|
||||||
}
|
}
|
||||||
|
@ -13,33 +13,54 @@ type channelManagerEntry struct {
|
|||||||
// think the channel is empty (without holding a lock across the entire Channel.Join()
|
// think the channel is empty (without holding a lock across the entire Channel.Join()
|
||||||
// call)
|
// call)
|
||||||
pendingJoins int
|
pendingJoins int
|
||||||
|
skeleton string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelManager keeps track of all the channels on the server,
|
// ChannelManager keeps track of all the channels on the server,
|
||||||
// providing synchronization for creation of new channels on first join,
|
// providing synchronization for creation of new channels on first join,
|
||||||
// cleanup of empty channels on last part, and renames.
|
// cleanup of empty channels on last part, and renames.
|
||||||
type ChannelManager struct {
|
type ChannelManager struct {
|
||||||
sync.RWMutex // tier 2
|
sync.RWMutex // tier 2
|
||||||
chans map[string]*channelManagerEntry
|
// chans is the main data structure, mapping casefolded name -> *Channel
|
||||||
registeredChannels map[string]bool
|
chans map[string]*channelManagerEntry
|
||||||
server *Server
|
chansSkeletons StringSet // skeletons of *unregistered* chans
|
||||||
|
registeredChannels StringSet // casefolds of registered chans
|
||||||
|
registeredSkeletons StringSet // skeletons of registered chans
|
||||||
|
purgedChannels StringSet // casefolds of purged chans
|
||||||
|
server *Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChannelManager returns a new ChannelManager.
|
// NewChannelManager returns a new ChannelManager.
|
||||||
func (cm *ChannelManager) Initialize(server *Server) {
|
func (cm *ChannelManager) Initialize(server *Server) {
|
||||||
cm.chans = make(map[string]*channelManagerEntry)
|
cm.chans = make(map[string]*channelManagerEntry)
|
||||||
|
cm.chansSkeletons = make(StringSet)
|
||||||
cm.server = server
|
cm.server = server
|
||||||
|
|
||||||
if server.Config().Channels.Registration.Enabled {
|
if server.Config().Channels.Registration.Enabled {
|
||||||
cm.loadRegisteredChannels()
|
cm.loadRegisteredChannels()
|
||||||
}
|
}
|
||||||
|
// purging should work even if registration is disabled
|
||||||
|
cm.purgedChannels = cm.server.channelRegistry.PurgedChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *ChannelManager) loadRegisteredChannels() {
|
func (cm *ChannelManager) loadRegisteredChannels() {
|
||||||
registeredChannels := cm.server.channelRegistry.AllChannels()
|
rawNames := cm.server.channelRegistry.AllChannels()
|
||||||
|
registeredChannels := make(StringSet, len(rawNames))
|
||||||
|
registeredSkeletons := make(StringSet, len(rawNames))
|
||||||
|
for _, name := range rawNames {
|
||||||
|
cfname, err := CasefoldChannel(name)
|
||||||
|
if err == nil {
|
||||||
|
registeredChannels.Add(cfname)
|
||||||
|
}
|
||||||
|
skeleton, err := Skeleton(name)
|
||||||
|
if err == nil {
|
||||||
|
registeredSkeletons.Add(skeleton)
|
||||||
|
}
|
||||||
|
}
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
cm.registeredChannels = registeredChannels
|
cm.registeredChannels = registeredChannels
|
||||||
|
cm.registeredSkeletons = registeredSkeletons
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns an existing channel with name equivalent to `name`, or nil
|
// Get returns an existing channel with name equivalent to `name`, or nil
|
||||||
@ -61,33 +82,49 @@ func (cm *ChannelManager) Get(name string) (channel *Channel) {
|
|||||||
func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
|
func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
|
||||||
server := client.server
|
server := client.server
|
||||||
casefoldedName, err := CasefoldChannel(name)
|
casefoldedName, err := CasefoldChannel(name)
|
||||||
if err != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
|
skeleton, skerr := Skeleton(name)
|
||||||
|
if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
channel := func() *Channel {
|
channel, err := func() (*Channel, error) {
|
||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
|
if cm.purgedChannels.Has(casefoldedName) {
|
||||||
|
return nil, errChannelPurged
|
||||||
|
}
|
||||||
entry := cm.chans[casefoldedName]
|
entry := cm.chans[casefoldedName]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
registered := cm.registeredChannels[casefoldedName]
|
registered := cm.registeredChannels.Has(casefoldedName)
|
||||||
// enforce OpOnlyCreation
|
// enforce OpOnlyCreation
|
||||||
if !registered && server.Config().Channels.OpOnlyCreation && !client.HasRoleCapabs("chanreg") {
|
if !registered && server.Config().Channels.OpOnlyCreation && !client.HasRoleCapabs("chanreg") {
|
||||||
return nil
|
return nil, errInsufficientPrivs
|
||||||
|
}
|
||||||
|
// enforce confusables
|
||||||
|
if cm.chansSkeletons.Has(skeleton) || (!registered && cm.registeredSkeletons.Has(skeleton)) {
|
||||||
|
return nil, errConfusableIdentifier
|
||||||
}
|
}
|
||||||
entry = &channelManagerEntry{
|
entry = &channelManagerEntry{
|
||||||
channel: NewChannel(server, name, registered),
|
channel: NewChannel(server, name, casefoldedName, registered),
|
||||||
pendingJoins: 0,
|
pendingJoins: 0,
|
||||||
}
|
}
|
||||||
|
if !registered {
|
||||||
|
// for an unregistered channel, we already have the correct unfolded name
|
||||||
|
// and therefore the final skeleton. for a registered channel, we don't have
|
||||||
|
// the unfolded name yet (it needs to be loaded from the db), but we already
|
||||||
|
// have the final skeleton in `registeredSkeletons` so we don't need to track it
|
||||||
|
cm.chansSkeletons.Add(skeleton)
|
||||||
|
entry.skeleton = skeleton
|
||||||
|
}
|
||||||
cm.chans[casefoldedName] = entry
|
cm.chans[casefoldedName] = entry
|
||||||
}
|
}
|
||||||
entry.pendingJoins += 1
|
entry.pendingJoins += 1
|
||||||
return entry.channel
|
return entry.channel, nil
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if channel == nil {
|
if err != nil {
|
||||||
return errNoSuchChannel
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.EnsureLoaded()
|
channel.EnsureLoaded()
|
||||||
@ -102,8 +139,9 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
|
|||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
nameCasefolded := channel.NameCasefolded()
|
cfname := channel.NameCasefolded()
|
||||||
entry := cm.chans[nameCasefolded]
|
|
||||||
|
entry := cm.chans[cfname]
|
||||||
if entry == nil || entry.channel != channel {
|
if entry == nil || entry.channel != channel {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -112,7 +150,10 @@ func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
|
|||||||
entry.pendingJoins -= 1
|
entry.pendingJoins -= 1
|
||||||
}
|
}
|
||||||
if entry.pendingJoins == 0 && entry.channel.IsClean() {
|
if entry.pendingJoins == 0 && entry.channel.IsClean() {
|
||||||
delete(cm.chans, nameCasefolded)
|
delete(cm.chans, cfname)
|
||||||
|
if entry.skeleton != "" {
|
||||||
|
delete(cm.chansSkeletons, entry.skeleton)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +211,7 @@ func (cm *ChannelManager) SetRegistered(channelName string, account string) (err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cm.registeredChannels[cfname] = true
|
cm.registeredChannels.Add(cfname)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,13 +245,17 @@ func (cm *ChannelManager) SetUnregistered(channelName string, account string) (e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rename renames a channel (but does not notify the members)
|
// Rename renames a channel (but does not notify the members)
|
||||||
func (cm *ChannelManager) Rename(name string, newname string) (err error) {
|
func (cm *ChannelManager) Rename(name string, newName string) (err error) {
|
||||||
cfname, err := CasefoldChannel(name)
|
cfname, err := CasefoldChannel(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
|
|
||||||
cfnewname, err := CasefoldChannel(newname)
|
newCfname, err := CasefoldChannel(newName)
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidChannelName
|
||||||
|
}
|
||||||
|
newSkeleton, err := Skeleton(newName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errInvalidChannelName
|
return errInvalidChannelName
|
||||||
}
|
}
|
||||||
@ -229,22 +274,35 @@ func (cm *ChannelManager) Rename(name string, newname string) (err error) {
|
|||||||
cm.Lock()
|
cm.Lock()
|
||||||
defer cm.Unlock()
|
defer cm.Unlock()
|
||||||
|
|
||||||
if cm.chans[cfnewname] != nil || cm.registeredChannels[cfnewname] {
|
if cm.chans[newCfname] != nil || cm.registeredChannels.Has(newCfname) {
|
||||||
|
return errChannelNameInUse
|
||||||
|
}
|
||||||
|
if cm.chansSkeletons.Has(newSkeleton) || cm.registeredSkeletons.Has(newSkeleton) {
|
||||||
return errChannelNameInUse
|
return errChannelNameInUse
|
||||||
}
|
}
|
||||||
entry := cm.chans[cfname]
|
entry := cm.chans[cfname]
|
||||||
if entry == nil {
|
if entry == nil || !entry.channel.IsLoaded() {
|
||||||
return errNoSuchChannel
|
return errNoSuchChannel
|
||||||
}
|
}
|
||||||
channel = entry.channel
|
channel = entry.channel
|
||||||
info = channel.ExportRegistration(IncludeInitial)
|
info = channel.ExportRegistration(IncludeInitial)
|
||||||
|
registered := info.Founder != ""
|
||||||
delete(cm.chans, cfname)
|
delete(cm.chans, cfname)
|
||||||
cm.chans[cfnewname] = entry
|
cm.chans[newCfname] = entry
|
||||||
if cm.registeredChannels[cfname] {
|
if registered {
|
||||||
delete(cm.registeredChannels, cfname)
|
delete(cm.registeredChannels, cfname)
|
||||||
cm.registeredChannels[cfnewname] = true
|
if oldSkeleton, err := Skeleton(info.Name); err == nil {
|
||||||
|
delete(cm.registeredSkeletons, oldSkeleton)
|
||||||
|
}
|
||||||
|
cm.registeredChannels.Add(newCfname)
|
||||||
|
cm.registeredSkeletons.Add(newSkeleton)
|
||||||
|
} else {
|
||||||
|
delete(cm.chansSkeletons, entry.skeleton)
|
||||||
|
cm.chansSkeletons.Add(newSkeleton)
|
||||||
|
entry.skeleton = newSkeleton
|
||||||
|
cm.chans[cfname] = entry
|
||||||
}
|
}
|
||||||
entry.channel.Rename(newname, cfnewname)
|
entry.channel.Rename(newName, newCfname)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,3 +325,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.Add(chname)
|
||||||
|
cm.Unlock()
|
||||||
|
|
||||||
|
cm.server.channelRegistry.PurgeChannel(chname, record)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPurged queries whether a channel is purged.
|
||||||
|
func (cm *ChannelManager) IsPurged(chname string) (result bool) {
|
||||||
|
chname, err := CasefoldChannel(chname)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
result = cm.purgedChannels.Has(chname)
|
||||||
|
cm.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpurge deletes a channel's purged status.
|
||||||
|
func (cm *ChannelManager) Unpurge(chname string) (err error) {
|
||||||
|
chname, err = CasefoldChannel(chname)
|
||||||
|
if err != nil {
|
||||||
|
return errNoSuchChannel
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.Lock()
|
||||||
|
found := cm.purgedChannels.Has(chname)
|
||||||
|
delete(cm.purgedChannels, chname)
|
||||||
|
cm.Unlock()
|
||||||
|
|
||||||
|
cm.server.channelRegistry.UnpurgeChannel(chname)
|
||||||
|
if !found {
|
||||||
|
return errNoSuchChannel
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -32,6 +32,8 @@ const (
|
|||||||
keyChannelPassword = "channel.key %s"
|
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
|
||||||
@ -106,21 +114,37 @@ func (reg *ChannelRegistry) Initialize(server *Server) {
|
|||||||
reg.server = server
|
reg.server = server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reg *ChannelRegistry) AllChannels() (result map[string]bool) {
|
// AllChannels returns the uncasefolded names of all registered channels.
|
||||||
result = make(map[string]bool)
|
func (reg *ChannelRegistry) AllChannels() (result []string) {
|
||||||
|
prefix := fmt.Sprintf(keyChannelName, "")
|
||||||
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
|
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
result = append(result, value)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
prefix := fmt.Sprintf(keyChannelExists, "")
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurgedChannels returns the set of all casefolded channel names that have been purged
|
||||||
|
func (reg *ChannelRegistry) PurgedChannels() (result map[string]empty) {
|
||||||
|
result = make(map[string]empty)
|
||||||
|
|
||||||
|
prefix := fmt.Sprintf(keyChannelPurged, "")
|
||||||
reg.server.store.View(func(tx *buntdb.Tx) error {
|
reg.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
|
||||||
if !strings.HasPrefix(key, prefix) {
|
if !strings.HasPrefix(key, prefix) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
channel := strings.TrimPrefix(key, prefix)
|
channel := strings.TrimPrefix(key, prefix)
|
||||||
result[channel] = true
|
result[channel] = empty{}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,11 +215,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 +280,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 +294,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 +349,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)))
|
||||||
|
}
|
||||||
|
@ -694,11 +694,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`)
|
||||||
@ -40,6 +41,8 @@ var (
|
|||||||
errNicknameReserved = errors.New("nickname is reserved")
|
errNicknameReserved = errors.New("nickname is reserved")
|
||||||
errNoExistingBan = errors.New("Ban does not exist")
|
errNoExistingBan = errors.New("Ban does not exist")
|
||||||
errNoSuchChannel = errors.New(`No such channel`)
|
errNoSuchChannel = errors.New(`No such channel`)
|
||||||
|
errChannelPurged = errors.New(`This channel was purged by the server operators and cannot be used`)
|
||||||
|
errConfusableIdentifier = errors.New("This identifier is confusable with one already in use")
|
||||||
errInsufficientPrivs = errors.New("Insufficient privileges")
|
errInsufficientPrivs = errors.New("Insufficient privileges")
|
||||||
errInvalidUsername = errors.New("Invalid username")
|
errInvalidUsername = errors.New("Invalid username")
|
||||||
errFeatureDisabled = errors.New(`That feature is disabled`)
|
errFeatureDisabled = errors.New(`That feature is disabled`)
|
||||||
|
@ -1292,13 +1292,28 @@ func joinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
|||||||
key = keys[i]
|
key = keys[i]
|
||||||
}
|
}
|
||||||
err := server.channels.Join(client, name, key, false, rb)
|
err := server.channels.Join(client, name, key, false, rb)
|
||||||
if err == errNoSuchChannel {
|
if err != nil {
|
||||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(name), client.t("No such channel"))
|
sendJoinError(client, name, rb, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendJoinError(client *Client, name string, rb *ResponseBuffer, err error) {
|
||||||
|
var errMsg string
|
||||||
|
switch err {
|
||||||
|
case errInsufficientPrivs:
|
||||||
|
errMsg = `Only server operators can create new channels`
|
||||||
|
case errConfusableIdentifier:
|
||||||
|
errMsg = `That channel name is too close to the name of another channel`
|
||||||
|
case errChannelPurged:
|
||||||
|
errMsg = err.Error()
|
||||||
|
default:
|
||||||
|
errMsg = `No such channel`
|
||||||
|
}
|
||||||
|
rb.Add(nil, client.server.name, ERR_NOSUCHCHANNEL, client.Nick(), utils.SafeErrorParam(name), client.t(errMsg))
|
||||||
|
}
|
||||||
|
|
||||||
// SAJOIN [nick] #channel{,#channel}
|
// SAJOIN [nick] #channel{,#channel}
|
||||||
func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||||
var target *Client
|
var target *Client
|
||||||
@ -1308,7 +1323,7 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
channelString = msg.Params[0]
|
channelString = msg.Params[0]
|
||||||
} else {
|
} else {
|
||||||
if len(msg.Params) == 1 {
|
if len(msg.Params) == 1 {
|
||||||
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "KICK", client.t("Not enough parameters"))
|
rb.Add(nil, server.name, ERR_NEEDMOREPARAMS, client.Nick(), "SAJOIN", client.t("Not enough parameters"))
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
target = server.clients.Get(msg.Params[0])
|
target = server.clients.Get(msg.Params[0])
|
||||||
@ -1322,7 +1337,10 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
|||||||
|
|
||||||
channels := strings.Split(channelString, ",")
|
channels := strings.Split(channelString, ",")
|
||||||
for _, chname := range channels {
|
for _, chname := range channels {
|
||||||
server.channels.Join(target, chname, "", true, rb)
|
err := server.channels.Join(target, chname, "", true, rb)
|
||||||
|
if err != nil {
|
||||||
|
sendJoinError(client, chname, rb, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -1372,7 +1390,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
|
||||||
}
|
}
|
||||||
@ -1727,15 +1745,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(), " ")...)
|
||||||
|
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() {
|
for _, member := range channel.Members() {
|
||||||
if member == client {
|
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 {
|
|
||||||
member.Send(nil, prefix, "MODE", args...)
|
member.Send(nil, prefix, "MODE", args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2380,7 +2397,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 {
|
||||||
|
@ -316,19 +316,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:
|
||||||
@ -575,7 +575,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 {
|
||||||
|
11
irc/types.go
11
irc/types.go
@ -28,6 +28,17 @@ func (clients ClientSet) Has(client *Client) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StringSet map[string]empty
|
||||||
|
|
||||||
|
func (s StringSet) Has(str string) bool {
|
||||||
|
_, ok := s[str]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s StringSet) Add(str string) {
|
||||||
|
s[str] = empty{}
|
||||||
|
}
|
||||||
|
|
||||||
// MemberSet is a set of members with modes.
|
// MemberSet is a set of members with modes.
|
||||||
type MemberSet map[*Client]*modes.ModeSet
|
type MemberSet map[*Client]*modes.ModeSet
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user