diff --git a/irc/channel.go b/irc/channel.go index 10f924a1..ea502d14 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -49,25 +49,33 @@ func (channel *Channel) ClientIsOperator(client *Client) bool { return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator) } +// Prefixes returns a list of prefixes for the given set of channel modes. +func (modes ChannelModeSet) 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) Nicks(target *Client) []string { isMultiPrefix := (target != nil) && target.capabilities[MultiPrefix] nicks := make([]string, len(channel.members)) i := 0 for client, modes := range channel.members { - if isMultiPrefix { - if modes[ChannelOperator] { - nicks[i] += "@" - } - if modes[Voice] { - nicks[i] += "+" - } - } else { - if modes[ChannelOperator] { - nicks[i] += "@" - } else if modes[Voice] { - nicks[i] += "+" - } - } + nicks[i] += modes.Prefixes(isMultiPrefix) nicks[i] += client.Nick().String() i += 1 } @@ -160,7 +168,7 @@ func (channel *Channel) Join(client *Client, key Text) { client.channels.Add(channel) channel.members.Add(client) if !channel.flags[Persistent] && (len(channel.members) == 1) { - channel.members[client][ChannelCreator] = true + channel.members[client][ChannelFounder] = true channel.members[client][ChannelOperator] = true } @@ -400,9 +408,38 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo channel.userLimit = limit return true - case ChannelOperator, Voice: - return channel.applyModeMember(client, change.mode, change.op, - NewName(change.arg)) + case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice: + var hasPrivs bool + + // make sure client has privs to edit the given prefix + for _, mode := range ChannelPrivModes { + if channel.members[client][mode] { + hasPrivs = true + + // Admins can't give other people Admin or remove it from others, + // standard for that channel mode, we worry about this later + if mode == ChannelAdmin && change.mode == ChannelAdmin { + hasPrivs = false + } + + break + } else if mode == change.mode { + break + } + } + + name := NewName(change.arg) + + if !hasPrivs { + if change.op == Remove && name.ToLower() == client.nick.ToLower() { + // success! + } else { + client.ErrChanOPrivIsNeeded(channel) + return false + } + } + + return channel.applyModeMember(client, change.mode, change.op, name) default: client.ErrUnknownMode(change.mode, channel) diff --git a/irc/commands.go b/irc/commands.go index fe253716..7ad41283 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -545,8 +545,9 @@ func ParseChannelModeCommand(channel Name, args []string) (Command, error) { op: op, } switch change.mode { + // TODO(dan): separate this into the type A/B/C/D args and use those lists here case Key, BanMask, ExceptMask, InviteMask, UserLimit, - ChannelOperator, ChannelCreator, Voice: + ChannelOperator, ChannelFounder, ChannelAdmin, Halfop, Voice: if len(args) > skipArgs { change.arg = args[skipArgs] skipArgs += 1 diff --git a/irc/modes.go b/irc/modes.go index 040f63de..caa99b04 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -67,25 +67,26 @@ var ( ) const ( - Anonymous ChannelMode = 'a' // flag - BanMask ChannelMode = 'b' // arg - ChannelCreator ChannelMode = 'O' // flag + ChannelFounder ChannelMode = 'q' // arg + ChannelAdmin ChannelMode = 'a' // arg ChannelOperator ChannelMode = 'o' // arg - ExceptMask ChannelMode = 'e' // arg - InviteMask ChannelMode = 'I' // arg - InviteOnly ChannelMode = 'i' // flag - Key ChannelMode = 'k' // flag arg - Moderated ChannelMode = 'm' // flag - NoOutside ChannelMode = 'n' // flag - OpOnlyTopic ChannelMode = 't' // flag - Persistent ChannelMode = 'P' // flag - Private ChannelMode = 'p' // flag - Quiet ChannelMode = 'q' // flag - ReOp ChannelMode = 'r' // flag - Secret ChannelMode = 's' // flag, deprecated - Theater ChannelMode = 'T' // flag, nonstandard - UserLimit ChannelMode = 'l' // flag arg + Halfop ChannelMode = 'h' // arg Voice ChannelMode = 'v' // arg + + BanMask ChannelMode = 'b' // arg + ExceptMask ChannelMode = 'e' // arg + InviteMask ChannelMode = 'I' // arg + InviteOnly ChannelMode = 'i' // flag + Key ChannelMode = 'k' // flag arg + Moderated ChannelMode = 'm' // flag + NoOutside ChannelMode = 'n' // flag + OpOnlyTopic ChannelMode = 't' // flag + Persistent ChannelMode = 'P' // flag + Private ChannelMode = 'p' // flag + ReOp ChannelMode = 'r' // flag + Secret ChannelMode = 's' // flag, deprecated + Theater ChannelMode = 'T' // flag, nonstandard + UserLimit ChannelMode = 'l' // flag arg ) var ( @@ -93,6 +94,20 @@ var ( BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, OpOnlyTopic, Persistent, Private, Theater, UserLimit, } + + // 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 = ChannelModes{ + ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, + } + + ChannelModePrefixes = map[ChannelMode]string{ + ChannelFounder: "~", + ChannelAdmin: "&", + ChannelOperator: "@", + Halfop: "%", + Voice: "+", + } ) // diff --git a/irc/reply.go b/irc/reply.go index cdf3bb19..3cecce29 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -298,21 +298,8 @@ func (target *Client) RplWhoReply(channel *Channel, client *Client) { } if channel != nil { + flags += channel.members[client].Prefixes(target.capabilities[MultiPrefix]) channelName = channel.name.String() - if target.capabilities[MultiPrefix] { - if channel.members[client][ChannelOperator] { - flags += "@" - } - if channel.members[client][Voice] { - flags += "+" - } - } else { - if channel.members[client][ChannelOperator] { - flags += "@" - } else if channel.members[client][Voice] { - flags += "+" - } - } } target.NumericReply(RPL_WHOREPLY, "%s %s %s %s %s %s :%d %s", channelName, client.username, client.hostname, @@ -432,7 +419,7 @@ func (target *Client) RplNamReply(channel *Channel) { } func (target *Client) RplWhoisChannels(client *Client) { - target.MultilineReply(client.WhoisChannelsNames(), RPL_WHOISCHANNELS, + target.MultilineReply(client.WhoisChannelsNames(target.capabilities[MultiPrefix]), RPL_WHOISCHANNELS, "%s :%s", client.Nick()) } diff --git a/irc/server.go b/irc/server.go index 3e77511e..bc03df31 100644 --- a/irc/server.go +++ b/irc/server.go @@ -111,7 +111,7 @@ func NewServer(config *Config) *Server { // server.isupport.Add("MODES", "") //TODO(dan): Support max modes? server.isupport.Add("NETWORK", config.Network.Name) // server.isupport.Add("NICKLEN", "") //TODO(dan): Support nick length - server.isupport.Add("PREFIX", "(ov)@+") + server.isupport.Add("PREFIX", "(qaohv)~&@%+") // server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Autogenerate based on PREFIXes, support STATUSMSG // server.isupport.Add("TARGMAX", "") //TODO(dan): Support this // server.isupport.Add("TOPICLEN", "") //TODO(dan): Support topic length @@ -504,20 +504,12 @@ func (msg *PrivMsgCommand) HandleServer(server *Server) { } } -func (client *Client) WhoisChannelsNames() []string { +func (client *Client) WhoisChannelsNames(isMultiPrefix bool) []string { chstrs := make([]string, len(client.channels)) index := 0 + //TODO(dan): handle secret (+s) channels here properly? for channel := range client.channels { - switch { - case channel.members[client][ChannelOperator]: - chstrs[index] = "@" + channel.name.String() - - case channel.members[client][Voice]: - chstrs[index] = "+" + channel.name.String() - - default: - chstrs[index] = channel.name.String() - } + chstrs[index] = channel.members[client].Prefixes(isMultiPrefix) + channel.name.String() index += 1 } return chstrs @@ -664,7 +656,28 @@ func (msg *KickCommand) HandleServer(server *Server) { continue } - channel.Kick(client, target, msg.Comment()) + // make sure client has privs to kick the given user + var hasPrivs bool + for _, mode := range ChannelPrivModes { + if channel.members[client][mode] { + hasPrivs = true + + // admins cannot kick other admins + if mode == ChannelAdmin && channel.members[target][ChannelAdmin] { + hasPrivs = false + } + + break + } else if channel.members[target][mode] { + break + } + } + + if hasPrivs { + channel.Kick(client, target, msg.Comment()) + } else { + client.ErrChanOPrivIsNeeded(channel) + } } }