diff --git a/irc/channel.go b/irc/channel.go index bb2ed0fe..b0897ccd 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -15,6 +15,7 @@ import ( "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/modes" ) var ( @@ -23,8 +24,8 @@ var ( // Channel represents a channel that clients can join. type Channel struct { - flags ModeSet - lists map[Mode]*UserMaskSet + flags modes.ModeSet + lists map[modes.Mode]*UserMaskSet key string members MemberSet membersCache []*Client // allow iteration over channel members without holding the lock @@ -53,11 +54,11 @@ func NewChannel(s *Server, name string, addDefaultModes bool, regInfo *Registere channel := &Channel{ createdTime: time.Now(), // may be overwritten by applyRegInfo - flags: make(ModeSet), - lists: map[Mode]*UserMaskSet{ - BanMask: NewUserMaskSet(), - ExceptMask: NewUserMaskSet(), - InviteMask: NewUserMaskSet(), + flags: make(modes.ModeSet), + lists: map[modes.Mode]*UserMaskSet{ + modes.BanMask: NewUserMaskSet(), + modes.ExceptMask: NewUserMaskSet(), + modes.InviteMask: NewUserMaskSet(), }, members: make(MemberSet), name: name, @@ -88,13 +89,13 @@ func (channel *Channel) applyRegInfo(chanReg *RegisteredChannel) { channel.name = chanReg.Name channel.createdTime = chanReg.RegisteredAt for _, mask := range chanReg.Banlist { - channel.lists[BanMask].Add(mask) + channel.lists[modes.BanMask].Add(mask) } for _, mask := range chanReg.Exceptlist { - channel.lists[ExceptMask].Add(mask) + channel.lists[modes.ExceptMask].Add(mask) } for _, mask := range chanReg.Invitelist { - channel.lists[InviteMask].Add(mask) + channel.lists[modes.InviteMask].Add(mask) } } @@ -111,13 +112,13 @@ func (channel *Channel) ExportRegistration(includeLists bool) (info RegisteredCh info.RegisteredAt = channel.registeredTime if includeLists { - for mask := range channel.lists[BanMask].masks { + for mask := range channel.lists[modes.BanMask].masks { info.Banlist = append(info.Banlist, mask) } - for mask := range channel.lists[ExceptMask].masks { + for mask := range channel.lists[modes.ExceptMask].masks { info.Exceptlist = append(info.Exceptlist, mask) } - for mask := range channel.lists[InviteMask].masks { + for mask := range channel.lists[modes.InviteMask].masks { info.Invitelist = append(info.Invitelist, mask) } } @@ -201,7 +202,7 @@ func (channel *Channel) Names(client *Client) { } // ClientIsAtLeast returns whether the client has at least the given channel privilege. -func (channel *Channel) ClientIsAtLeast(client *Client, permission Mode) bool { +func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() @@ -211,7 +212,7 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission Mode) bool { } // check regular modes - for _, mode := range ChannelPrivModes { + for _, mode := range modes.ChannelPrivModes { if channel.members.HasMode(client, mode) { return true } @@ -224,27 +225,6 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission Mode) bool { return false } -// Prefixes returns a list of prefixes for the given set of channel modes. -func (modes ModeSet) Prefixes(isMultiPrefix bool) string { - var prefixes string - - // add prefixes in order from highest to lowest privs - for _, mode := range ChannelPrivModes { - if modes[mode] { - prefixes += ChannelModePrefixes[mode] - } - } - if modes[Voice] { - prefixes += ChannelModePrefixes[Voice] - } - - if !isMultiPrefix && len(prefixes) > 1 { - prefixes = string(prefixes[0]) - } - - return prefixes -} - func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() @@ -263,11 +243,11 @@ func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool clientModes := channel.members[client] targetModes := channel.members[target] result := false - for _, mode := range ChannelPrivModes { + for _, mode := range modes.ChannelPrivModes { if clientModes[mode] { result = true // admins cannot kick other admins - if mode == ChannelAdmin && targetModes[ChannelAdmin] { + if mode == modes.ChannelAdmin && targetModes[modes.ChannelAdmin] { result = false } break @@ -318,18 +298,18 @@ func (channel *Channel) hasClient(client *Client) bool { // func (channel *Channel) modeStrings(client *Client) (result []string) { - isMember := client.HasMode(Operator) || channel.hasClient(client) + isMember := client.HasMode(modes.Operator) || channel.hasClient(client) showKey := isMember && (channel.key != "") showUserLimit := channel.userLimit > 0 - modes := "+" + mods := "+" // flags with args if showKey { - modes += Key.String() + mods += modes.Key.String() } if showUserLimit { - modes += UserLimit.String() + mods += modes.UserLimit.String() } channel.stateMutex.RLock() @@ -337,10 +317,10 @@ func (channel *Channel) modeStrings(client *Client) (result []string) { // flags for mode := range channel.flags { - modes += mode.String() + mods += mode.String() } - result = []string{modes} + result = []string{mods} // args for flags with args: The order must match above to keep // positional arguments in place. @@ -390,15 +370,15 @@ func (channel *Channel) Join(client *Client, key string) { return } - isInvited := channel.lists[InviteMask].Match(client.nickMaskCasefolded) - if channel.flags[InviteOnly] && !isInvited { + isInvited := channel.lists[modes.InviteMask].Match(client.nickMaskCasefolded) + if channel.flags[modes.InviteOnly] && !isInvited { client.Send(nil, client.server.name, ERR_INVITEONLYCHAN, channel.name, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i")) return } - if channel.lists[BanMask].Match(client.nickMaskCasefolded) && + if channel.lists[modes.BanMask].Match(client.nickMaskCasefolded) && !isInvited && - !channel.lists[ExceptMask].Match(client.nickMaskCasefolded) { + !channel.lists[modes.ExceptMask].Match(client.nickMaskCasefolded) { client.Send(nil, client.server.name, ERR_BANNEDFROMCHAN, channel.name, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b")) return } @@ -423,11 +403,11 @@ func (channel *Channel) Join(client *Client, key string) { // give channel mode if necessary newChannel := firstJoin && !channel.IsRegistered() - var givenMode *Mode + var givenMode *modes.Mode if client.AccountName() == channel.registeredFounder { - givenMode = &ChannelFounder + givenMode = &modes.ChannelFounder } else if newChannel { - givenMode = &ChannelOperator + givenMode = &modes.ChannelOperator } if givenMode != nil { channel.stateMutex.Lock() @@ -492,12 +472,12 @@ func (channel *Channel) SendTopic(client *Client) { // SetTopic sets the topic of this channel, if the client is allowed to do so. func (channel *Channel) SetTopic(client *Client, topic string) { - if !(client.flags[Operator] || channel.hasClient(client)) { + if !(client.flags[modes.Operator] || channel.hasClient(client)) { client.Send(nil, client.server.name, ERR_NOTONCHANNEL, channel.name, client.t("You're not on that channel")) return } - if channel.HasMode(OpOnlyTopic) && !channel.ClientIsAtLeast(client, ChannelOperator) { + if channel.HasMode(modes.OpOnlyTopic) && !channel.ClientIsAtLeast(client, modes.ChannelOperator) { client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator")) return } @@ -525,32 +505,32 @@ func (channel *Channel) CanSpeak(client *Client) bool { defer channel.stateMutex.RUnlock() _, hasClient := channel.members[client] - if channel.flags[NoOutside] && !hasClient { + if channel.flags[modes.NoOutside] && !hasClient { return false } - if channel.flags[Moderated] && !channel.ClientIsAtLeast(client, Voice) { + if channel.flags[modes.Moderated] && !channel.ClientIsAtLeast(client, modes.Voice) { return false } - if channel.flags[RegisteredOnly] && client.account == &NoAccount { + if channel.flags[modes.RegisteredOnly] && client.account == &NoAccount { return false } return true } // TagMsg sends a tag message to everyone in this channel who can accept them. -func (channel *Channel) TagMsg(msgid string, minPrefix *Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client) { +func (channel *Channel) TagMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client) { channel.sendMessage(msgid, "TAGMSG", []caps.Capability{caps.MessageTags}, minPrefix, clientOnlyTags, client, nil) } // sendMessage sends a given message to everyone on this channel. -func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capability, minPrefix *Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *string) { +func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capability, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *string) { if !channel.CanSpeak(client) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) return } // for STATUSMSG - var minPrefixMode Mode + var minPrefixMode modes.Mode if minPrefix != nil { minPrefixMode = *minPrefix } @@ -587,23 +567,23 @@ func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capab } // SplitPrivMsg sends a private message to everyone in this channel. -func (channel *Channel) SplitPrivMsg(msgid string, minPrefix *Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage) { +func (channel *Channel) SplitPrivMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage) { channel.sendSplitMessage(msgid, "PRIVMSG", minPrefix, clientOnlyTags, client, &message) } // SplitNotice sends a private message to everyone in this channel. -func (channel *Channel) SplitNotice(msgid string, minPrefix *Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage) { +func (channel *Channel) SplitNotice(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage) { channel.sendSplitMessage(msgid, "NOTICE", minPrefix, clientOnlyTags, client, &message) } -func (channel *Channel) sendSplitMessage(msgid, cmd string, minPrefix *Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *SplitMessage) { +func (channel *Channel) sendSplitMessage(msgid, cmd string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *SplitMessage) { if !channel.CanSpeak(client) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) return } // for STATUSMSG - var minPrefixMode Mode + var minPrefixMode modes.Mode if minPrefix != nil { minPrefixMode = *minPrefix } @@ -628,8 +608,7 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, minPrefix *Mode, cli } } -func (channel *Channel) applyModeMemberNoMutex(client *Client, mode Mode, - op ModeOp, nick string) *ModeChange { +func (channel *Channel) applyModeMemberNoMutex(client *Client, mode modes.Mode, op modes.ModeOp, nick string) *modes.ModeChange { if nick == "" { //TODO(dan): shouldn't this be handled before it reaches this function? client.Send(nil, client.server.name, ERR_NEEDMOREPARAMS, "MODE", client.t("Not enough parameters")) @@ -647,7 +626,7 @@ func (channel *Channel) applyModeMemberNoMutex(client *Client, mode Mode, modeset, exists := channel.members[target] var already bool if exists { - enable := op == Add + enable := op == modes.Add already = modeset[mode] == enable modeset[mode] = enable } @@ -659,25 +638,25 @@ func (channel *Channel) applyModeMemberNoMutex(client *Client, mode Mode, } else if already { return nil } else { - return &ModeChange{ - op: op, - mode: mode, - arg: nick, + return &modes.ModeChange{ + Op: op, + Mode: mode, + Arg: nick, } } } // ShowMaskList shows the given list to the client. -func (channel *Channel) ShowMaskList(client *Client, mode Mode) { +func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode) { // choose appropriate modes var rpllist, rplendoflist string - if mode == BanMask { + if mode == modes.BanMask { rpllist = RPL_BANLIST rplendoflist = RPL_ENDOFBANLIST - } else if mode == ExceptMask { + } else if mode == modes.ExceptMask { rpllist = RPL_EXCEPTLIST rplendoflist = RPL_ENDOFEXCEPTLIST - } else if mode == InviteMask { + } else if mode == modes.InviteMask { rpllist = RPL_INVITELIST rplendoflist = RPL_ENDOFINVITELIST } @@ -693,28 +672,28 @@ func (channel *Channel) ShowMaskList(client *Client, mode Mode) { client.Send(nil, client.server.name, rplendoflist, nick, channel.name, client.t("End of list")) } -func (channel *Channel) applyModeMask(client *Client, mode Mode, op ModeOp, mask string) bool { +func (channel *Channel) applyModeMask(client *Client, mode modes.Mode, op modes.ModeOp, mask string) bool { list := channel.lists[mode] if list == nil { // This should never happen, but better safe than panicky. return false } - if (op == List) || (mask == "") { + if (op == modes.List) || (mask == "") { channel.ShowMaskList(client, mode) return false } - if !channel.ClientIsAtLeast(client, ChannelOperator) { + if !channel.ClientIsAtLeast(client, modes.ChannelOperator) { client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator")) return false } - if op == Add { + if op == modes.Add { return list.Add(mask) } - if op == Remove { + if op == modes.Remove { return list.Remove(mask) } @@ -732,11 +711,11 @@ func (channel *Channel) Quit(client *Client) { } func (channel *Channel) Kick(client *Client, target *Client, comment string) { - if !(client.flags[Operator] || channel.hasClient(client)) { + if !(client.flags[modes.Operator] || channel.hasClient(client)) { client.Send(nil, client.server.name, ERR_NOTONCHANNEL, channel.name, client.t("You're not on that channel")) return } - if !channel.ClientIsAtLeast(client, ChannelOperator) { + if !channel.ClientIsAtLeast(client, modes.ChannelOperator) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) return } @@ -765,7 +744,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string) { // Invite invites the given client to the channel, if the inviter can do so. func (channel *Channel) Invite(invitee *Client, inviter *Client) { - if channel.flags[InviteOnly] && !channel.ClientIsAtLeast(inviter, ChannelOperator) { + if channel.flags[modes.InviteOnly] && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) { inviter.Send(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, channel.name, inviter.t("You're not a channel operator")) return } @@ -776,15 +755,15 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) { } //TODO(dan): handle this more nicely, keep a list of last X invited channels on invitee rather than explicitly modifying the invite list? - if channel.flags[InviteOnly] { + if channel.flags[modes.InviteOnly] { nmc := invitee.NickCasefolded() channel.stateMutex.Lock() - channel.lists[InviteMask].Add(nmc) + channel.lists[modes.InviteMask].Add(nmc) channel.stateMutex.Unlock() } for _, member := range channel.Members() { - if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, Halfop) { + if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, modes.Halfop) { member.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), channel.name) } } @@ -792,7 +771,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) { //TODO(dan): should inviter.server.name here be inviter.nickMaskString ? inviter.Send(nil, inviter.server.name, RPL_INVITING, invitee.nick, channel.name) invitee.Send(nil, inviter.nickMaskString, "INVITE", invitee.nick, channel.name) - if invitee.flags[Away] { + if invitee.flags[modes.Away] { inviter.Send(nil, inviter.server.name, RPL_AWAY, invitee.nick, invitee.awayMessage) } } diff --git a/irc/chanserv.go b/irc/chanserv.go index 85b071ff..c4e6cd53 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/goshuirc/irc-go/ircfmt" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" ) @@ -55,7 +56,7 @@ func (server *Server) chanservReceivePrivmsg(client *Client, message string) { } channelInfo := server.channels.Get(channelKey) - if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, ChannelOperator) { + if channelInfo == nil || !channelInfo.ClientIsAtLeast(client, modes.ChannelOperator) { client.ChanServNotice(client.t("You must be an oper on the channel to register it")) return } @@ -81,7 +82,7 @@ func (server *Server) chanservReceivePrivmsg(client *Client, message string) { 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 - change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.NickCasefolded()) + change := channelInfo.applyModeMemberNoMutex(client, modes.ChannelFounder, modes.Add, client.NickCasefolded()) if change != nil { //TODO(dan): we should change the name of String and make it return a slice here //TODO(dan): unify this code with code in modes.go diff --git a/irc/client.go b/irc/client.go index 35e2890a..d289deb3 100644 --- a/irc/client.go +++ b/irc/client.go @@ -21,6 +21,7 @@ import ( "github.com/goshuirc/irc-go/ircmsg" ident "github.com/oragono/go-ident" "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" ) @@ -50,7 +51,7 @@ type Client struct { class *OperClass ctime time.Time exitedSnomaskSent bool - flags map[Mode]bool + flags map[modes.Mode]bool hasQuit bool hops int hostname string @@ -95,7 +96,7 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { capVersion: caps.Cap301, channels: make(ChannelSet), ctime: now, - flags: make(map[Mode]bool), + flags: make(map[modes.Mode]bool), server: server, socket: &socket, account: &NoAccount, @@ -107,7 +108,7 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { client.recomputeMaxlens() if isTLS { - client.flags[TLS] = true + client.flags[modes.TLS] = true // error is not useful to us here anyways so we can ignore it client.certfp, _ = client.socket.CertFP() @@ -348,7 +349,7 @@ func (client *Client) TryResume() { return } - if !oldClient.HasMode(TLS) || !client.HasMode(TLS) { + if !oldClient.HasMode(modes.TLS) || !client.HasMode(modes.TLS) { client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must have TLS")) return } diff --git a/irc/commands.go b/irc/commands.go index fdb6fbf2..84498a9d 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -5,7 +5,10 @@ package irc -import "github.com/goshuirc/irc-go/ircmsg" +import ( + "github.com/goshuirc/irc-go/ircmsg" + "github.com/oragono/oragono/irc/modes" +) // Command represents a command accepted from a client. type Command struct { @@ -24,7 +27,7 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b client.Send(nil, server.name, ERR_NOTREGISTERED, client.nick, client.t("You need to register before you can use that command")) return false } - if cmd.oper && !client.flags[Operator] { + if cmd.oper && !client.flags[modes.Operator] { client.Send(nil, server.name, ERR_NOPRIVILEGES, client.nick, client.t("Permission Denied - You're not an IRC operator")) return false } diff --git a/irc/gateways.go b/irc/gateways.go index ffaee157..d0589ff2 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -10,6 +10,7 @@ import ( "fmt" "net" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/utils" ) @@ -82,9 +83,9 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (exiting bool) // set tls info client.certfp = "" if tls { - client.flags[TLS] = true + client.flags[modes.TLS] = true } else { - delete(client.flags, TLS) + delete(client.flags, modes.TLS) } return false diff --git a/irc/getters.go b/irc/getters.go index e65d232b..a00f0054 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -3,7 +3,10 @@ package irc -import "github.com/oragono/oragono/irc/isupport" +import ( + "github.com/oragono/oragono/irc/isupport" + "github.com/oragono/oragono/irc/modes" +) func (server *Server) ISupport() *isupport.List { server.configurableStateMutex.RLock() @@ -41,7 +44,7 @@ func (server *Server) WebIRCConfig() []webircConfig { return server.webirc } -func (server *Server) DefaultChannelModes() Modes { +func (server *Server) DefaultChannelModes() modes.Modes { server.configurableStateMutex.RLock() defer server.configurableStateMutex.RUnlock() return server.defaultChannelModes @@ -107,7 +110,7 @@ func (client *Client) AccountName() string { return client.account.Name } -func (client *Client) HasMode(mode Mode) bool { +func (client *Client) HasMode(mode modes.Mode) bool { client.stateMutex.RLock() defer client.stateMutex.RUnlock() return client.flags[mode] @@ -180,7 +183,7 @@ func (channel *Channel) setKey(key string) { channel.stateMutex.Unlock() } -func (channel *Channel) HasMode(mode Mode) bool { +func (channel *Channel) HasMode(mode modes.Mode) bool { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() return channel.flags[mode] @@ -193,7 +196,7 @@ func (channel *Channel) Founder() string { } // set a channel mode, return whether it was already set -func (channel *Channel) setMode(mode Mode, enable bool) (already bool) { +func (channel *Channel) setMode(mode modes.Mode, enable bool) (already bool) { channel.stateMutex.Lock() already = (channel.flags[mode] == enable) if !already { diff --git a/irc/handlers.go b/irc/handlers.go index 1d75a614..4b3682ed 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -27,6 +27,7 @@ import ( "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/custime" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" @@ -480,30 +481,30 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } if isAway { - client.flags[Away] = true + client.flags[modes.Away] = true } else { - delete(client.flags, Away) + delete(client.flags, modes.Away) } client.awayMessage = text - var op ModeOp - if client.flags[Away] { - op = Add + var op modes.ModeOp + if client.flags[modes.Away] { + op = modes.Add client.Send(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away")) } else { - op = Remove + op = modes.Remove client.Send(nil, server.name, RPL_UNAWAY, client.nick, client.t("You are no longer marked as being away")) } //TODO(dan): Should this be sent automagically as part of setting the flag/mode? - modech := ModeChanges{ModeChange{ - mode: Away, - op: op, + modech := modes.ModeChanges{modes.ModeChange{ + Mode: modes.Away, + Op: op, }} client.Send(nil, server.name, "MODE", client.nick, modech.String()) // dispatch away-notify for friend := range client.Friends(caps.AwayNotify) { - if client.flags[Away] { + if client.flags[modes.Away] { friend.SendFromClient("", client, nil, "AWAY", client.awayMessage) } else { friend.SendFromClient("", client, nil, "AWAY") @@ -581,7 +582,7 @@ func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { // DEBUG GCSTATS/NUMGOROUTINE/etc func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { - if !client.flags[Operator] { + if !client.flags[modes.Operator] { return false } @@ -854,7 +855,7 @@ Get an explanation of , or "index" for a list of help topics.`)) // handle index if argument == "index" { - if client.flags[Operator] { + if client.flags[modes.Operator] { client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndexOpers)) } else { client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndex)) @@ -864,7 +865,7 @@ Get an explanation of , or "index" for a list of help topics.`)) helpHandler, exists := Help[argument] - if exists && (!helpHandler.oper || (helpHandler.oper && client.flags[Operator])) { + if exists && (!helpHandler.oper || (helpHandler.oper && client.flags[modes.Operator])) { if helpHandler.textGenerator != nil { client.sendHelp(strings.ToUpper(argument), client.t(helpHandler.textGenerator(client))) } else { @@ -1336,7 +1337,7 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if len(channels) == 0 { for _, channel := range server.channels.Channels() { - if !client.flags[Operator] && channel.flags[Secret] { + if !client.flags[modes.Operator] && channel.flags[modes.Secret] { continue } if matcher.Matches(channel) { @@ -1345,14 +1346,14 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } } else { // limit regular users to only listing one channel - if !client.flags[Operator] { + if !client.flags[modes.Operator] { channels = channels[:1] } for _, chname := range channels { casefoldedChname, err := CasefoldChannel(chname) channel := server.channels.Get(casefoldedChname) - if err != nil || channel == nil || (!client.flags[Operator] && channel.flags[Secret]) { + if err != nil || channel == nil || (!client.flags[modes.Operator] && channel.flags[modes.Secret]) { if len(chname) > 0 { client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, chname, client.t("No such channel")) } @@ -1374,10 +1375,10 @@ func lusersHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { for _, onlineusers := range server.clients.AllClients() { totalcount++ - if onlineusers.flags[Invisible] { + if onlineusers.flags[modes.Invisible] { invisiblecount++ } - if onlineusers.flags[Operator] { + if onlineusers.flags[modes.Operator] { opercount++ } } @@ -1409,7 +1410,7 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } // applied mode changes - applied := make(ModeChanges, 0) + applied := make(modes.ModeChanges, 0) if 1 < len(msg.Params) { // parse out real mode changes @@ -1431,11 +1432,11 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { // save changes to banlist/exceptlist/invexlist var banlistUpdated, exceptlistUpdated, invexlistUpdated bool for _, change := range applied { - if change.mode == BanMask { + if change.Mode == modes.BanMask { banlistUpdated = true - } else if change.mode == ExceptMask { + } else if change.Mode == modes.ExceptMask { exceptlistUpdated = true - } else if change.mode == InviteMask { + } else if change.Mode == modes.InviteMask { invexlistUpdated = true } } @@ -1483,12 +1484,12 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } // applied mode changes - applied := make(ModeChanges, 0) + applied := make(modes.ModeChanges, 0) if 1 < len(msg.Params) { // parse out real mode changes params := msg.Params[1:] - changes, unknown := ParseUserModeChanges(params...) + changes, unknown := modes.ParseUserModeChanges(params...) // alert for unknown mode changes for char := range unknown { @@ -1506,7 +1507,7 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { client.Send(nil, client.nickMaskString, "MODE", targetNick, applied.String()) } else if hasPrivs { client.Send(nil, target.nickMaskString, RPL_UMODEIS, targetNick, target.ModeString()) - if client.flags[LocalOperator] || client.flags[Operator] { + if client.flags[modes.LocalOperator] || client.flags[modes.Operator] { masks := server.snomasks.String(client) if 0 < len(masks) { client.Send(nil, target.nickMaskString, RPL_SNOMASKIS, targetNick, masks, client.t("Server notice masks")) @@ -1687,8 +1688,8 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if i > maxTargets-1 { break } - prefixes, targetString := SplitChannelMembershipPrefixes(targetString) - lowestPrefix := GetLowestChannelModePrefix(prefixes) + prefixes, targetString := modes.SplitChannelMembershipPrefixes(targetString) + lowestPrefix := modes.GetLowestChannelModePrefix(prefixes) target, cerr := CasefoldChannel(targetString) if cerr == nil { @@ -1727,7 +1728,7 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine - if !user.flags[RegisteredOnly] || client.registered { + if !user.flags[modes.RegisteredOnly] || client.registered { user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg) } if client.capabilities.Has(caps.EchoMessage) { @@ -1788,7 +1789,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { client.Send(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect")) return true } - if client.flags[Operator] == true { + if client.flags[modes.Operator] == true { client.Send(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!")) return false } @@ -1803,7 +1804,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return true } - client.flags[Operator] = true + client.flags[modes.Operator] = true client.operName = name client.class = oper.Class client.whoisLine = oper.WhoisLine @@ -1819,9 +1820,9 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } // set new modes - var applied ModeChanges + var applied modes.ModeChanges if 0 < len(oper.Modes) { - modeChanges, unknownChanges := ParseUserModeChanges(strings.Split(oper.Modes, " ")...) + modeChanges, unknownChanges := modes.ParseUserModeChanges(strings.Split(oper.Modes, " ")...) applied = client.applyUserModeChanges(true, modeChanges) if 0 < len(unknownChanges) { var runes string @@ -1834,9 +1835,9 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { client.Send(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator")) - applied = append(applied, ModeChange{ - mode: Operator, - op: Add, + applied = append(applied, modes.ModeChange{ + Mode: modes.Operator, + Op: modes.Add, }) client.Send(nil, server.name, "MODE", client.nick, applied.String()) @@ -1912,8 +1913,8 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool if i > maxTargets-1 { break } - prefixes, targetString := SplitChannelMembershipPrefixes(targetString) - lowestPrefix := GetLowestChannelModePrefix(prefixes) + prefixes, targetString := modes.SplitChannelMembershipPrefixes(targetString) + lowestPrefix := modes.GetLowestChannelModePrefix(prefixes) // eh, no need to notify them if len(targetString) < 1 { @@ -1955,13 +1956,13 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine - if !user.flags[RegisteredOnly] || client.registered { + if !user.flags[modes.RegisteredOnly] || client.registered { user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) } if client.capabilities.Has(caps.EchoMessage) { client.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) } - if user.flags[Away] { + if user.flags[modes.Away] { //TODO(dan): possibly implement cooldown of away notifications to users client.Send(nil, server.name, RPL_AWAY, user.nick, user.awayMessage) } @@ -2059,7 +2060,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage) (resul return } //TODO(dan): allow IRCops to do this? - if !channel.ClientIsAtLeast(client, Operator) { + if !channel.ClientIsAtLeast(client, modes.Operator) { errorResponse(RenamePrivsNeeded, oldName) return } @@ -2171,8 +2172,8 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if i > maxTargets-1 { break } - prefixes, targetString := SplitChannelMembershipPrefixes(targetString) - lowestPrefix := GetLowestChannelModePrefix(prefixes) + prefixes, targetString := modes.SplitChannelMembershipPrefixes(targetString) + lowestPrefix := modes.GetLowestChannelModePrefix(prefixes) // eh, no need to notify them if len(targetString) < 1 { @@ -2212,7 +2213,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { if client.capabilities.Has(caps.EchoMessage) { client.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick) } - if user.flags[Away] { + if user.flags[modes.Away] { //TODO(dan): possibly implement cooldown of away notifications to users client.Send(nil, server.name, RPL_AWAY, user.nick, user.awayMessage) } @@ -2424,10 +2425,10 @@ func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool var isOper, isAway string - if target.flags[Operator] { + if target.flags[modes.Operator] { isOper = "*" } - if target.flags[Away] { + if target.flags[modes.Away] { isAway = "-" } else { isAway = "+" @@ -2478,7 +2479,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { lkey := strings.ToLower(key) if lkey == "tls" || lkey == "secure" { // only accept "tls" flag if the gateway's connection to us is secure as well - if client.flags[TLS] || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) { + if client.flags[modes.TLS] || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) { secure = true } } @@ -2567,7 +2568,7 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { return false } - if client.flags[Operator] { + if client.flags[modes.Operator] { masks := strings.Split(masksString, ",") for _, mask := range masks { casefoldedMask, err := Casefold(mask) diff --git a/irc/modes.go b/irc/modes.go index 8fd3eda0..6fcc044b 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -9,298 +9,59 @@ import ( "strconv" "strings" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" ) -// ModeOp is an operation performed with modes -type ModeOp rune - -func (op ModeOp) String() string { - return string(op) -} - -const ( - // Add is used when adding the given key. - Add ModeOp = '+' - // List is used when listing modes (for instance, listing the current bans on a channel). - List ModeOp = '=' - // Remove is used when taking away the given key. - Remove ModeOp = '-' -) - -// Mode represents a user/channel/server mode -type Mode rune - -func (mode Mode) String() string { - return string(mode) -} - -// ModeChange is a single mode changing -type ModeChange struct { - mode Mode - op ModeOp - arg string -} - -func (change *ModeChange) String() (str string) { - if (change.op == Add) || (change.op == Remove) { - str = change.op.String() - } - str += change.mode.String() - if change.arg != "" { - str += " " + change.arg - } - return -} - -// ModeChanges are a collection of 'ModeChange's -type ModeChanges []ModeChange - -func (changes ModeChanges) String() string { - if len(changes) == 0 { - return "" - } - - op := changes[0].op - str := changes[0].op.String() - - for _, change := range changes { - if change.op != op { - op = change.op - str += change.op.String() - } - str += change.mode.String() - } - - for _, change := range changes { - if change.arg == "" { - continue - } - str += " " + change.arg - } - return str -} - -// Modes is just a raw list of modes -type Modes []Mode - -func (modes Modes) String() string { - strs := make([]string, len(modes)) - for index, mode := range modes { - strs[index] = mode.String() - } - return strings.Join(strs, "") -} - -// User Modes -const ( - Away Mode = 'a' - Bot Mode = 'B' - Invisible Mode = 'i' - LocalOperator Mode = 'O' - Operator Mode = 'o' - Restricted Mode = 'r' - RegisteredOnly Mode = 'R' - ServerNotice Mode = 's' - TLS Mode = 'Z' - UserRoleplaying Mode = 'E' - WallOps Mode = 'w' -) - var ( - // SupportedUserModes are the user modes that we actually support (modifying). - SupportedUserModes = Modes{ - Away, Bot, Invisible, Operator, RegisteredOnly, ServerNotice, UserRoleplaying, - } - // supportedUserModesString acts as a cache for when we introduce users - supportedUserModesString = SupportedUserModes.String() -) - -// Channel Modes -const ( - BanMask Mode = 'b' // arg - ChanRoleplaying Mode = 'E' // flag - ExceptMask Mode = 'e' // arg - InviteMask Mode = 'I' // arg - InviteOnly Mode = 'i' // flag - Key Mode = 'k' // flag arg - Moderated Mode = 'm' // flag - NoOutside Mode = 'n' // flag - OpOnlyTopic Mode = 't' // flag - // RegisteredOnly mode is reused here from umode definition - Secret Mode = 's' // flag - UserLimit Mode = 'l' // flag arg -) - -var ( - ChannelFounder Mode = 'q' // arg - ChannelAdmin Mode = 'a' // arg - ChannelOperator Mode = 'o' // arg - Halfop Mode = 'h' // arg - Voice Mode = 'v' // arg - - // SupportedChannelModes are the channel modes that we support. - SupportedChannelModes = Modes{ - BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key, - Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, Secret, UserLimit, - } - // supportedChannelModesString acts as a cache for when we introduce users - supportedChannelModesString = SupportedChannelModes.String() - // DefaultChannelModes are enabled on brand new channels when they're created. // this can be overridden in the `channels` config, with the `default-modes` key - DefaultChannelModes = Modes{ - NoOutside, OpOnlyTopic, - } - - // ChannelPrivModes holds the list of modes that are privileged, ie founder/op/halfop, in order. - // voice is not in this list because it cannot perform channel operator actions. - ChannelPrivModes = Modes{ - ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, - } - - ChannelModePrefixes = map[Mode]string{ - ChannelFounder: "~", - ChannelAdmin: "&", - ChannelOperator: "@", - Halfop: "%", - Voice: "+", + DefaultChannelModes = modes.Modes{ + modes.NoOutside, modes.OpOnlyTopic, } ) -// -// channel membership prefixes -// - -// SplitChannelMembershipPrefixes takes a target and returns the prefixes on it, then the name. -func SplitChannelMembershipPrefixes(target string) (prefixes string, name string) { - name = target - for { - if len(name) > 0 && strings.Contains("~&@%+", string(name[0])) { - prefixes += string(name[0]) - name = name[1:] - } else { - break - } - } - - return prefixes, name -} - -// GetLowestChannelModePrefix returns the lowest channel prefix mode out of the given prefixes. -func GetLowestChannelModePrefix(prefixes string) *Mode { - var lowest *Mode - - if strings.Contains(prefixes, "+") { - lowest = &Voice - } else { - for i, mode := range ChannelPrivModes { - if strings.Contains(prefixes, ChannelModePrefixes[mode]) { - lowest = &ChannelPrivModes[i] - } - } - } - - return lowest -} - -// -// commands -// - -// ParseUserModeChanges returns the valid changes, and the list of unknown chars. -func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) { - changes := make(ModeChanges, 0) - unknown := make(map[rune]bool) - - op := List - - if 0 < len(params) { - modeArg := params[0] - skipArgs := 1 - - for _, mode := range modeArg { - if mode == '-' || mode == '+' { - op = ModeOp(mode) - continue - } - change := ModeChange{ - mode: Mode(mode), - op: op, - } - - // put arg into modechange if needed - switch Mode(mode) { - case ServerNotice: - // always require arg - if len(params) > skipArgs { - change.arg = params[skipArgs] - skipArgs++ - } else { - continue - } - } - - var isKnown bool - for _, supportedMode := range SupportedUserModes { - if rune(supportedMode) == mode { - isKnown = true - break - } - } - if !isKnown { - unknown[mode] = true - continue - } - - changes = append(changes, change) - } - } - - return changes, unknown -} - // applyUserModeChanges applies the given changes, and returns the applied changes. -func (client *Client) applyUserModeChanges(force bool, changes ModeChanges) ModeChanges { - applied := make(ModeChanges, 0) +func (client *Client) applyUserModeChanges(force bool, changes modes.ModeChanges) modes.ModeChanges { + applied := make(modes.ModeChanges, 0) for _, change := range changes { - switch change.mode { - case Bot, Invisible, WallOps, UserRoleplaying, Operator, LocalOperator, RegisteredOnly: - switch change.op { - case Add: - if !force && (change.mode == Operator || change.mode == LocalOperator) { + switch change.Mode { + case modes.Bot, modes.Invisible, modes.WallOps, modes.UserRoleplaying, modes.Operator, modes.LocalOperator, modes.RegisteredOnly: + switch change.Op { + case modes.Add: + if !force && (change.Mode == modes.Operator || change.Mode == modes.LocalOperator) { continue } - if client.flags[change.mode] { + if client.flags[change.Mode] { continue } - client.flags[change.mode] = true + client.flags[change.Mode] = true applied = append(applied, change) - case Remove: - if !client.flags[change.mode] { + case modes.Remove: + if !client.flags[change.Mode] { continue } - delete(client.flags, change.mode) + delete(client.flags, change.Mode) applied = append(applied, change) } - case ServerNotice: - if !client.flags[Operator] { + case modes.ServerNotice: + if !client.flags[modes.Operator] { continue } var masks []sno.Mask - if change.op == Add || change.op == Remove { - for _, char := range change.arg { + if change.Op == modes.Add || change.Op == modes.Remove { + for _, char := range change.Arg { masks = append(masks, sno.Mask(char)) } } - if change.op == Add { + if change.Op == modes.Add { client.server.snomasks.AddMasks(client, masks...) applied = append(applied, change) - } else if change.op == Remove { + } else if change.Op == modes.Remove { client.server.snomasks.RemoveMasks(client, masks...) applied = append(applied, change) } @@ -314,28 +75,28 @@ func (client *Client) applyUserModeChanges(force bool, changes ModeChanges) Mode } // ParseDefaultChannelModes parses the `default-modes` line of the config -func ParseDefaultChannelModes(config *Config) Modes { +func ParseDefaultChannelModes(config *Config) modes.Modes { if config.Channels.DefaultModes == nil { // not present in config, fall back to compile-time default return DefaultChannelModes } modeChangeStrings := strings.Split(strings.TrimSpace(*config.Channels.DefaultModes), " ") modeChanges, _ := ParseChannelModeChanges(modeChangeStrings...) - defaultChannelModes := make(Modes, 0) + defaultChannelModes := make(modes.Modes, 0) for _, modeChange := range modeChanges { - if modeChange.op == Add { - defaultChannelModes = append(defaultChannelModes, modeChange.mode) + if modeChange.Op == modes.Add { + defaultChannelModes = append(defaultChannelModes, modeChange.Mode) } } return defaultChannelModes } // ParseChannelModeChanges returns the valid changes, and the list of unknown chars. -func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { - changes := make(ModeChanges, 0) +func ParseChannelModeChanges(params ...string) (modes.ModeChanges, map[rune]bool) { + changes := make(modes.ModeChanges, 0) unknown := make(map[rune]bool) - op := List + op := modes.List if 0 < len(params) { modeArg := params[0] @@ -343,35 +104,35 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { for _, mode := range modeArg { if mode == '-' || mode == '+' { - op = ModeOp(mode) + op = modes.ModeOp(mode) continue } - change := ModeChange{ - mode: Mode(mode), - op: op, + change := modes.ModeChange{ + Mode: modes.Mode(mode), + Op: op, } // put arg into modechange if needed - switch Mode(mode) { - case BanMask, ExceptMask, InviteMask: + switch modes.Mode(mode) { + case modes.BanMask, modes.ExceptMask, modes.InviteMask: if len(params) > skipArgs { - change.arg = params[skipArgs] + change.Arg = params[skipArgs] skipArgs++ } else { - change.op = List + change.Op = modes.List } - case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice: + case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: if len(params) > skipArgs { - change.arg = params[skipArgs] + change.Arg = params[skipArgs] skipArgs++ } else { continue } - case Key, UserLimit: + case modes.Key, modes.UserLimit: // don't require value when removing - if change.op == Add { + if change.Op == modes.Add { if len(params) > skipArgs { - change.arg = params[skipArgs] + change.Arg = params[skipArgs] skipArgs++ } else { continue @@ -380,19 +141,19 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { } var isKnown bool - for _, supportedMode := range SupportedChannelModes { + for _, supportedMode := range modes.SupportedChannelModes { if rune(supportedMode) == mode { isKnown = true break } } - for _, supportedMode := range ChannelPrivModes { + for _, supportedMode := range modes.ChannelPrivModes { if rune(supportedMode) == mode { isKnown = true break } } - if mode == rune(Voice) { + if mode == rune(modes.Voice) { isKnown = true } if !isKnown { @@ -408,40 +169,40 @@ func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) { } // ApplyChannelModeChanges applies a given set of mode changes. -func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes ModeChanges) ModeChanges { +func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes modes.ModeChanges) modes.ModeChanges { // so we only output one warning for each list type when full - listFullWarned := make(map[Mode]bool) + listFullWarned := make(map[modes.Mode]bool) - clientIsOp := channel.ClientIsAtLeast(client, ChannelOperator) + clientIsOp := channel.ClientIsAtLeast(client, modes.ChannelOperator) var alreadySentPrivError bool - applied := make(ModeChanges, 0) + applied := make(modes.ModeChanges, 0) - isListOp := func(change ModeChange) bool { - return (change.op == List) || (change.arg == "") + isListOp := func(change modes.ModeChange) bool { + return (change.Op == modes.List) || (change.Arg == "") } - hasPrivs := func(change ModeChange) bool { + hasPrivs := func(change modes.ModeChange) bool { if isSamode { return true } - switch change.mode { - case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice: + switch change.Mode { + case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: // Admins can't give other people Admin or remove it from others - if change.mode == ChannelAdmin { + if change.Mode == modes.ChannelAdmin { return false } - if change.op == List { + if change.Op == modes.List { return true } - cfarg, _ := CasefoldName(change.arg) - if change.op == Remove && cfarg == client.nickCasefolded { + cfarg, _ := CasefoldName(change.Arg) + if change.Op == modes.Remove && cfarg == client.nickCasefolded { // "There is no restriction, however, on anyone `deopping' themselves" // return true } - return channel.ClientIsAtLeast(client, change.mode) - case BanMask: + return channel.ClientIsAtLeast(client, change.Mode) + case modes.BanMask: // #163: allow unprivileged users to list ban masks return clientIsOp || isListOp(change) default: @@ -458,77 +219,77 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c continue } - switch change.mode { - case BanMask, ExceptMask, InviteMask: + switch change.Mode { + case modes.BanMask, modes.ExceptMask, modes.InviteMask: if isListOp(change) { - channel.ShowMaskList(client, change.mode) + channel.ShowMaskList(client, change.Mode) continue } // confirm mask looks valid - mask, err := Casefold(change.arg) + mask, err := Casefold(change.Arg) if err != nil { continue } - switch change.op { - case Add: - if channel.lists[change.mode].Length() >= client.server.Limits().ChanListModes { - if !listFullWarned[change.mode] { - client.Send(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.mode.String(), client.t("Channel list is full")) - listFullWarned[change.mode] = true + switch change.Op { + case modes.Add: + if channel.lists[change.Mode].Length() >= client.server.Limits().ChanListModes { + if !listFullWarned[change.Mode] { + client.Send(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full")) + listFullWarned[change.Mode] = true } continue } - channel.lists[change.mode].Add(mask) + channel.lists[change.Mode].Add(mask) applied = append(applied, change) - case Remove: - channel.lists[change.mode].Remove(mask) + case modes.Remove: + channel.lists[change.Mode].Remove(mask) applied = append(applied, change) } - case UserLimit: - switch change.op { - case Add: - val, err := strconv.ParseUint(change.arg, 10, 64) + case modes.UserLimit: + switch change.Op { + case modes.Add: + val, err := strconv.ParseUint(change.Arg, 10, 64) if err == nil { channel.setUserLimit(val) applied = append(applied, change) } - case Remove: + case modes.Remove: channel.setUserLimit(0) applied = append(applied, change) } - case Key: - switch change.op { - case Add: - channel.setKey(change.arg) + case modes.Key: + switch change.Op { + case modes.Add: + channel.setKey(change.Arg) - case Remove: + case modes.Remove: channel.setKey("") } applied = append(applied, change) - case InviteOnly, Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, Secret, ChanRoleplaying: - if change.op == List { + case modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.RegisteredOnly, modes.Secret, modes.ChanRoleplaying: + if change.Op == modes.List { continue } - already := channel.setMode(change.mode, change.op == Add) + already := channel.setMode(change.Mode, change.Op == modes.Add) if !already { applied = append(applied, change) } - case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice: - if change.op == List { + case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice: + if change.Op == modes.List { continue } - change := channel.applyModeMemberNoMutex(client, change.mode, change.op, change.arg) + change := channel.applyModeMemberNoMutex(client, change.Mode, change.Op, change.Arg) if change != nil { applied = append(applied, *change) } diff --git a/irc/modes/modes.go b/irc/modes/modes.go new file mode 100644 index 00000000..47c0aa91 --- /dev/null +++ b/irc/modes/modes.go @@ -0,0 +1,286 @@ +// Copyright (c) 2012-2014 Jeremy Latt +// Copyright (c) 2014-2015 Edmund Huber +// Copyright (c) 2016-2017 Daniel Oaks +// released under the MIT license + +package modes + +import ( + "strings" +) + +var ( + // SupportedUserModes are the user modes that we actually support (modifying). + SupportedUserModes = Modes{ + Away, Bot, Invisible, Operator, RegisteredOnly, ServerNotice, UserRoleplaying, + } + + // SupportedChannelModes are the channel modes that we support. + SupportedChannelModes = Modes{ + BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key, + Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, Secret, UserLimit, + } +) + +// ModeOp is an operation performed with modes +type ModeOp rune + +func (op ModeOp) String() string { + return string(op) +} + +const ( + // Add is used when adding the given key. + Add ModeOp = '+' + // List is used when listing modes (for instance, listing the current bans on a channel). + List ModeOp = '=' + // Remove is used when taking away the given key. + Remove ModeOp = '-' +) + +// Mode represents a user/channel/server mode +type Mode rune + +func (mode Mode) String() string { + return string(mode) +} + +// ModeChange is a single mode changing +type ModeChange struct { + Mode Mode + Op ModeOp + Arg string +} + +func (change *ModeChange) String() (str string) { + if (change.Op == Add) || (change.Op == Remove) { + str = change.Op.String() + } + str += change.Mode.String() + if change.Arg != "" { + str += " " + change.Arg + } + return +} + +// ModeChanges are a collection of 'ModeChange's +type ModeChanges []ModeChange + +func (changes ModeChanges) String() string { + if len(changes) == 0 { + return "" + } + + op := changes[0].Op + str := changes[0].Op.String() + + for _, change := range changes { + if change.Op != op { + op = change.Op + str += change.Op.String() + } + str += change.Mode.String() + } + + for _, change := range changes { + if change.Arg == "" { + continue + } + str += " " + change.Arg + } + return str +} + +// Modes is just a raw list of modes +type Modes []Mode + +func (modes Modes) String() string { + strs := make([]string, len(modes)) + for index, mode := range modes { + strs[index] = mode.String() + } + return strings.Join(strs, "") +} + +// User Modes +const ( + Away Mode = 'a' + Bot Mode = 'B' + Invisible Mode = 'i' + LocalOperator Mode = 'O' + Operator Mode = 'o' + Restricted Mode = 'r' + RegisteredOnly Mode = 'R' + ServerNotice Mode = 's' + TLS Mode = 'Z' + UserRoleplaying Mode = 'E' + WallOps Mode = 'w' +) + +// Channel Modes +const ( + BanMask Mode = 'b' // arg + ChanRoleplaying Mode = 'E' // flag + ExceptMask Mode = 'e' // arg + InviteMask Mode = 'I' // arg + InviteOnly Mode = 'i' // flag + Key Mode = 'k' // flag arg + Moderated Mode = 'm' // flag + NoOutside Mode = 'n' // flag + OpOnlyTopic Mode = 't' // flag + // RegisteredOnly mode is reused here from umode definition + Secret Mode = 's' // flag + UserLimit Mode = 'l' // flag arg +) + +var ( + ChannelFounder Mode = 'q' // arg + ChannelAdmin Mode = 'a' // arg + ChannelOperator Mode = 'o' // arg + Halfop Mode = 'h' // arg + Voice Mode = 'v' // arg + + // ChannelPrivModes holds the list of modes that are privileged, ie founder/op/halfop, in order. + // voice is not in this list because it cannot perform channel operator actions. + ChannelPrivModes = Modes{ + ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, + } + + ChannelModePrefixes = map[Mode]string{ + ChannelFounder: "~", + ChannelAdmin: "&", + ChannelOperator: "@", + Halfop: "%", + Voice: "+", + } +) + +// +// channel membership prefixes +// + +// SplitChannelMembershipPrefixes takes a target and returns the prefixes on it, then the name. +func SplitChannelMembershipPrefixes(target string) (prefixes string, name string) { + name = target + for { + if len(name) > 0 && strings.Contains("~&@%+", string(name[0])) { + prefixes += string(name[0]) + name = name[1:] + } else { + break + } + } + + return prefixes, name +} + +// GetLowestChannelModePrefix returns the lowest channel prefix mode out of the given prefixes. +func GetLowestChannelModePrefix(prefixes string) *Mode { + var lowest *Mode + + if strings.Contains(prefixes, "+") { + lowest = &Voice + } else { + for i, mode := range ChannelPrivModes { + if strings.Contains(prefixes, ChannelModePrefixes[mode]) { + lowest = &ChannelPrivModes[i] + } + } + } + + return lowest +} + +// +// commands +// + +// ParseUserModeChanges returns the valid changes, and the list of unknown chars. +func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) { + changes := make(ModeChanges, 0) + unknown := make(map[rune]bool) + + op := List + + if 0 < len(params) { + modeArg := params[0] + skipArgs := 1 + + for _, mode := range modeArg { + if mode == '-' || mode == '+' { + op = ModeOp(mode) + continue + } + change := ModeChange{ + Mode: Mode(mode), + Op: op, + } + + // put arg into modechange if needed + switch Mode(mode) { + case ServerNotice: + // always require arg + if len(params) > skipArgs { + change.Arg = params[skipArgs] + skipArgs++ + } else { + continue + } + } + + var isKnown bool + for _, supportedMode := range SupportedUserModes { + if rune(supportedMode) == mode { + isKnown = true + break + } + } + if !isKnown { + unknown[mode] = true + continue + } + + changes = append(changes, change) + } + } + + return changes, unknown +} + +// ModeSet holds a set of modes. +type ModeSet map[Mode]bool + +// String returns the modes in this set. +func (set ModeSet) String() string { + if len(set) == 0 { + return "" + } + strs := make([]string, len(set)) + index := 0 + for mode := range set { + strs[index] = mode.String() + index++ + } + return strings.Join(strs, "") +} + +// Prefixes returns a list of prefixes for the given set of channel modes. +func (set ModeSet) Prefixes(isMultiPrefix bool) string { + var prefixes string + + // add prefixes in order from highest to lowest privs + for _, mode := range ChannelPrivModes { + if set[mode] { + prefixes += ChannelModePrefixes[mode] + } + } + if set[Voice] { + prefixes += ChannelModePrefixes[Voice] + } + + if !isMultiPrefix && len(prefixes) > 1 { + prefixes = string(prefixes[0]) + } + + return prefixes +} diff --git a/irc/roleplay.go b/irc/roleplay.go index 0942c826..89494e1e 100644 --- a/irc/roleplay.go +++ b/irc/roleplay.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/oragono/oragono/irc/caps" + "github.com/oragono/oragono/irc/modes" ) const ( @@ -34,7 +35,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt return } - if !channel.flags[ChanRoleplaying] { + if !channel.flags[modes.ChanRoleplaying] { client.Send(nil, client.server.name, ERR_CANNOTSENDRP, channel.name, client.t("Channel doesn't have roleplaying mode available")) return } @@ -53,7 +54,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt return } - if !user.flags[UserRoleplaying] { + if !user.flags[modes.UserRoleplaying] { client.Send(nil, client.server.name, ERR_CANNOTSENDRP, user.nick, client.t("User doesn't have roleplaying mode enabled")) return } @@ -62,7 +63,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt if client.capabilities.Has(caps.EchoMessage) { client.Send(nil, source, "PRIVMSG", user.nick, message) } - if user.flags[Away] { + if user.flags[modes.Away] { //TODO(dan): possibly implement cooldown of away notifications to users client.Send(nil, server.name, RPL_AWAY, user.nick, user.awayMessage) } diff --git a/irc/server.go b/irc/server.go index 32c881d2..c704aad4 100644 --- a/irc/server.go +++ b/irc/server.go @@ -29,6 +29,7 @@ import ( "github.com/oragono/oragono/irc/isupport" "github.com/oragono/oragono/irc/languages" "github.com/oragono/oragono/irc/logger" + "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/passwd" "github.com/oragono/oragono/irc/sno" "github.com/oragono/oragono/irc/utils" @@ -43,6 +44,11 @@ var ( couldNotParseIPMsg, _ = (&[]ircmsg.IrcMessage{ircmsg.MakeMessage(nil, "", "ERROR", "Unable to parse your IP address")}[0]).Line() RenamePrivsNeeded = errors.New("Only chanops can rename channels") + + // supportedUserModesString acts as a cache for when we introduce users + supportedUserModesString = modes.SupportedUserModes.String() + // supportedChannelModesString acts as a cache for when we introduce users + supportedChannelModesString = modes.SupportedChannelModes.String() ) // Limits holds the maximum limits for various things such as topic lengths. @@ -89,7 +95,7 @@ type Server struct { connectionLimiter *connection_limits.Limiter connectionThrottler *connection_limits.Throttler ctime time.Time - defaultChannelModes Modes + defaultChannelModes modes.Modes dlines *DLineManager loggingRawIO bool isupport *isupport.List @@ -179,7 +185,7 @@ func (server *Server) setISupport() { isupport := isupport.NewList() isupport.Add("AWAYLEN", strconv.Itoa(server.limits.AwayLen)) isupport.Add("CASEMAPPING", "ascii") - isupport.Add("CHANMODES", strings.Join([]string{Modes{BanMask, ExceptMask, InviteMask}.String(), "", Modes{UserLimit, Key}.String(), Modes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, ChanRoleplaying, Secret}.String()}, ",")) + isupport.Add("CHANMODES", strings.Join([]string{modes.Modes{modes.BanMask, modes.ExceptMask, modes.InviteMask}.String(), "", modes.Modes{modes.UserLimit, modes.Key}.String(), modes.Modes{modes.InviteOnly, modes.Moderated, modes.NoOutside, modes.OpOnlyTopic, modes.ChanRoleplaying, modes.Secret}.String()}, ",")) isupport.Add("CHANNELLEN", strconv.Itoa(server.limits.ChannelLen)) isupport.Add("CHANTYPES", "#") isupport.Add("ELIST", "U") @@ -224,7 +230,7 @@ func (server *Server) setISupport() { server.configurableStateMutex.Unlock() } -func loadChannelList(channel *Channel, list string, maskMode Mode) { +func loadChannelList(channel *Channel, list string, maskMode modes.Mode) { if list == "" { return } @@ -584,7 +590,7 @@ func (client *Client) WhoisChannelsNames(target *Client) []string { var chstrs []string for _, channel := range client.Channels() { // channel is secret and the target can't see it - if !target.flags[Operator] && channel.HasMode(Secret) && !channel.hasClient(target) { + if !target.flags[modes.Operator] && channel.HasMode(modes.Secret) && !channel.hasClient(target) { continue } chstrs = append(chstrs, channel.ClientPrefixes(client, isMultiPrefix)+channel.name) @@ -605,17 +611,17 @@ func (client *Client) getWhoisOf(target *Client) { if target.class != nil { client.Send(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine) } - if client.flags[Operator] || client == target { + if client.flags[modes.Operator] || client == target { client.Send(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP")) } - if target.flags[TLS] { + if target.flags[modes.TLS] { client.Send(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection")) } accountName := target.AccountName() if accountName != "" { client.Send(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, accountName, client.t("is logged in as")) } - if target.flags[Bot] { + if target.flags[modes.Bot] { client.Send(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName))) } @@ -628,7 +634,7 @@ func (client *Client) getWhoisOf(target *Client) { client.Send(nil, client.server.name, RPL_WHOISLANGUAGE, params...) } - if target.certfp != "" && (client.flags[Operator] || client == target) { + if target.certfp != "" && (client.flags[modes.Operator] || client == target) { client.Send(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp)) } client.Send(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time")) @@ -641,12 +647,12 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client) { channelName := "*" flags := "" - if client.HasMode(Away) { + if client.HasMode(modes.Away) { flags = "G" } else { flags = "H" } - if client.HasMode(Operator) { + if client.HasMode(modes.Operator) { flags += "*" } @@ -659,7 +665,7 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client) { func whoChannel(client *Client, channel *Channel, friends ClientSet) { for _, member := range channel.Members() { - if !client.flags[Invisible] || friends[client] { + if !client.flags[modes.Invisible] || friends[client] { client.rplWhoReply(channel, member) } } @@ -1095,7 +1101,7 @@ func (server *Server) setupListeners(config *Config) { } // GetDefaultChannelModes returns our default channel modes. -func (server *Server) GetDefaultChannelModes() Modes { +func (server *Server) GetDefaultChannelModes() modes.Modes { server.configurableStateMutex.RLock() defer server.configurableStateMutex.RUnlock() return server.defaultChannelModes @@ -1130,11 +1136,11 @@ func (matcher *elistMatcher) Matches(channel *Channel) bool { func (target *Client) RplList(channel *Channel) { // get the correct number of channel members var memberCount int - if target.flags[Operator] || channel.hasClient(target) { + if target.flags[modes.Operator] || channel.hasClient(target) { memberCount = len(channel.Members()) } else { for _, member := range channel.Members() { - if !member.HasMode(Invisible) { + if !member.HasMode(modes.Invisible) { memberCount++ } } @@ -1162,7 +1168,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { } // limit regular users to only listing one channel - if !client.flags[Operator] { + if !client.flags[modes.Operator] { channels = channels[:1] } diff --git a/irc/types.go b/irc/types.go index 3595a021..d4e9e018 100644 --- a/irc/types.go +++ b/irc/types.go @@ -5,26 +5,7 @@ package irc -import ( - "strings" -) - -// ModeSet holds a set of modes. -type ModeSet map[Mode]bool - -// String returns the modes in this set. -func (set ModeSet) String() string { - if len(set) == 0 { - return "" - } - strs := make([]string, len(set)) - index := 0 - for mode := range set { - strs[index] = mode.String() - index++ - } - return strings.Join(strs, "") -} +import "github.com/oragono/oragono/irc/modes" // ClientSet is a set of clients. type ClientSet map[*Client]bool @@ -45,11 +26,11 @@ func (clients ClientSet) Has(client *Client) bool { } // MemberSet is a set of members with modes. -type MemberSet map[*Client]ModeSet +type MemberSet map[*Client]modes.ModeSet // Add adds the given client to this set. func (members MemberSet) Add(member *Client) { - members[member] = make(ModeSet) + members[member] = make(modes.ModeSet) } // Remove removes the given client from this set. @@ -64,7 +45,7 @@ func (members MemberSet) Has(member *Client) bool { } // HasMode returns true if the given client is in this set with the given mode. -func (members MemberSet) HasMode(member *Client, mode Mode) bool { +func (members MemberSet) HasMode(member *Client, mode modes.Mode) bool { modes, ok := members[member] if !ok { return false @@ -73,7 +54,7 @@ func (members MemberSet) HasMode(member *Client, mode Mode) bool { } // AnyHasMode returns true if any of our clients has the given mode. -func (members MemberSet) AnyHasMode(mode Mode) bool { +func (members MemberSet) AnyHasMode(mode modes.Mode) bool { for _, modes := range members { if modes[mode] { return true