diff --git a/CHANGELOG.md b/CHANGELOG.md index 589fbdf5..72eb5e0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ New release of Oragono! ### Added * Added `REHASH` command. +* Added ability to message channel members with a specific privelege (i.e. support for `STATUSMSG`). * Added ability to enable and disable SASL. * Added support for IRCv3 capabilities [`cap-notify`](http://ircv3.net/specs/extensions/cap-notify-3.2.html) and [`echo-message`](http://ircv3.net/specs/extensions/echo-message-3.2.html). ### Changed +* Server operators no longer have permissions to do everything in channels. ### Removed diff --git a/irc/channel.go b/irc/channel.go index f936a8b7..d8209949 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -91,14 +91,25 @@ func (channel *Channel) Names(client *Client) { client.Send(nil, client.server.name, RPL_ENDOFNAMES, client.nick, channel.name, "End of NAMES list") } -// ClientIsHalfOp returns whether client is at least a halfop. -func (channel *Channel) ClientIsHalfOp(client *Client) bool { - return client.flags[Operator] || channel.members.HasMode(client, Halfop) || channel.members.HasMode(client, ChannelOperator) || channel.members.HasMode(client, ChannelAdmin) || channel.members.HasMode(client, ChannelFounder) -} +// ClientIsAtLeast returns whether the client has at least the given channel privilege. +func (channel *Channel) ClientIsAtLeast(client *Client, permission ChannelMode) bool { + // get voice, since it's not a part of ChannelPrivModes + if channel.members.HasMode(client, permission) { + return true + } -// ClientIsOperator returns whether client is at least a chanop. -func (channel *Channel) ClientIsOperator(client *Client) bool { - return client.flags[Operator] || channel.members.HasMode(client, ChannelOperator) || channel.members.HasMode(client, ChannelAdmin) || channel.members.HasMode(client, ChannelFounder) + // check regular modes + for _, mode := range ChannelPrivModes { + if channel.members.HasMode(client, mode) { + return true + } + + if mode == permission { + break + } + } + + return false } // Prefixes returns a list of prefixes for the given set of channel modes. @@ -276,7 +287,7 @@ func (channel *Channel) SetTopic(client *Client, topic string) { return } - if channel.flags[OpOnlyTopic] && !channel.ClientIsOperator(client) { + if channel.flags[OpOnlyTopic] && !channel.ClientIsAtLeast(client, ChannelOperator) { client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator") return } @@ -308,12 +319,22 @@ func (channel *Channel) CanSpeak(client *Client) bool { return true } -func (channel *Channel) PrivMsg(clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message string) { +// PrivMsg sends a private message to everyone in this channel. +func (channel *Channel) PrivMsg(minPrefix *ChannelMode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message string) { if !channel.CanSpeak(client) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, "Cannot send to channel") return } + // for STATUSMSG + var minPrefixMode ChannelMode + if minPrefix != nil { + minPrefixMode = *minPrefix + } for member := range channel.members { + if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) { + // STATUSMSG + continue + } if member == client && !client.capabilities[EchoMessage] { continue } @@ -327,7 +348,7 @@ func (channel *Channel) PrivMsg(clientOnlyTags *map[string]ircmsg.TagValue, clie func (channel *Channel) applyModeFlag(client *Client, mode ChannelMode, op ModeOp) bool { - if !channel.ClientIsOperator(client) { + if !channel.ClientIsAtLeast(client, ChannelOperator) { client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator") return false } @@ -418,7 +439,7 @@ func (channel *Channel) applyModeMask(client *Client, mode ChannelMode, op ModeO return false } - if !channel.ClientIsOperator(client) { + if !channel.ClientIsAtLeast(client, ChannelOperator) { client.Send(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator") return false } @@ -461,7 +482,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string) { client.Send(nil, client.server.name, ERR_NOTONCHANNEL, channel.name, "You're not on that channel") return } - if !channel.ClientIsOperator(client) { + if !channel.ClientIsAtLeast(client, ChannelOperator) { client.Send(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, "Cannot send to channel") return } @@ -481,7 +502,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string) { } func (channel *Channel) Invite(invitee *Client, inviter *Client) { - if channel.flags[InviteOnly] && !channel.ClientIsOperator(inviter) { + if channel.flags[InviteOnly] && !channel.ClientIsAtLeast(inviter, ChannelOperator) { inviter.Send(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, channel.name, "You're not a channel operator") return } @@ -498,7 +519,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client) { // send invite-notify for member := range channel.members { - if member.capabilities[InviteNotify] && member != inviter && member != invitee && channel.ClientIsHalfOp(member) { + if member.capabilities[InviteNotify] && member != inviter && member != invitee && channel.ClientIsAtLeast(member, Halfop) { member.Send(nil, inviter.nickMaskString, "INVITE", invitee.nick, channel.name) } } diff --git a/irc/modes.go b/irc/modes.go index 431c238c..e4e7a1a9 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -156,12 +156,6 @@ var ( ) const ( - ChannelFounder ChannelMode = 'q' // arg - ChannelAdmin ChannelMode = 'a' // arg - ChannelOperator ChannelMode = 'o' // arg - Halfop ChannelMode = 'h' // arg - Voice ChannelMode = 'v' // arg - BanMask ChannelMode = 'b' // arg ExceptMask ChannelMode = 'e' // arg InviteMask ChannelMode = 'I' // arg @@ -175,6 +169,12 @@ const ( ) var ( + ChannelFounder ChannelMode = 'q' // arg + ChannelAdmin ChannelMode = 'a' // arg + ChannelOperator ChannelMode = 'o' // arg + Halfop ChannelMode = 'h' // arg + Voice ChannelMode = 'v' // arg + SupportedChannelModes = ChannelModes{ BanMask, ExceptMask, InviteMask, InviteOnly, Key, NoOutside, OpOnlyTopic, Secret, UserLimit, @@ -201,6 +201,38 @@ var ( } ) +// 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) *ChannelMode { + var lowest *ChannelMode + + if strings.Contains(prefixes, "+") { + lowest = &Voice + } else { + for i, mode := range ChannelPrivModes { + if strings.Contains(prefixes, ChannelModePrefixes[mode]) { + lowest = &ChannelPrivModes[i] + } + } + } + + return lowest +} + // // commands // diff --git a/irc/server.go b/irc/server.go index b8a8c3c7..22b7c31b 100644 --- a/irc/server.go +++ b/irc/server.go @@ -236,7 +236,7 @@ func (server *Server) setISupport() { server.isupport.Add("NETWORK", server.networkName) server.isupport.Add("NICKLEN", strconv.Itoa(server.limits.NickLen)) server.isupport.Add("PREFIX", "(qaohv)~&@%+") - // server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Support STATUSMSG + server.isupport.Add("STATUSMSG", "~&@%+") // server.isupport.Add("TARGMAX", "") //TODO(dan): Support this server.isupport.Add("TOPICLEN", strconv.Itoa(server.limits.TopicLen)) @@ -686,6 +686,9 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool message := msg.Params[1] for _, targetString := range targets { + prefixes, targetString := SplitChannelMembershipPrefixes(targetString) + lowestPrefix := GetLowestChannelModePrefix(prefixes) + target, err := CasefoldChannel(targetString) if err == nil { channel := server.channels.Get(target) @@ -693,7 +696,7 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool client.Send(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, targetString, "No such channel") continue } - channel.PrivMsg(clientOnlyTags, client, message) + channel.PrivMsg(lowestPrefix, clientOnlyTags, client, message) } else { target, err = CasefoldName(targetString) user := server.clients.Get(target) @@ -1112,6 +1115,9 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { message := msg.Params[1] for _, targetString := range targets { + prefixes, targetString := SplitChannelMembershipPrefixes(targetString) + lowestPrefix := GetLowestChannelModePrefix(prefixes) + target, cerr := CasefoldChannel(targetString) if cerr == nil { channel := server.channels.Get(target) @@ -1119,7 +1125,7 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { // errors silently ignored with NOTICE as per RFC continue } - channel.PrivMsg(clientOnlyTags, client, message) + channel.PrivMsg(lowestPrefix, clientOnlyTags, client, message) } else { target, err := CasefoldName(targetString) if err != nil {