diff --git a/irc/channel.go b/irc/channel.go index 7153277a..d3f34d0b 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -439,8 +439,12 @@ func (channel *Channel) regenerateMembersCache() { // Names sends the list of users joined to the channel to the given client. func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { - isJoined := channel.hasClient(client) + channel.stateMutex.RLock() + clientModes, isJoined := channel.members[client] + channel.stateMutex.RUnlock() isOper := client.HasMode(modes.Operator) + respectAuditorium := channel.flags.HasMode(modes.Auditorium) && !isOper && + (!isJoined || clientModes.HighestChannelUserMode() == modes.Mode(0)) isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix) isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames) @@ -464,6 +468,9 @@ func (channel *Channel) Names(client *Client, rb *ResponseBuffer) { if !isJoined && target.HasMode(modes.Invisible) && !isOper { continue } + if respectAuditorium && modeSet.HighestChannelUserMode() == modes.Mode(0) { + continue + } prefix := modeSet.Prefixes(isMultiPrefix) if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen { namesLines = append(namesLines, buffer.String()) @@ -748,8 +755,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp }() var message utils.SplitMessage + respectAuditorium := givenMode == modes.Mode(0) && channel.flags.HasMode(modes.Auditorium) // no history item for fake persistent joins - if rb != nil { + if rb != nil && !respectAuditorium { message = utils.MakeMessage("") histItem := history.Item{ Type: history.Join, @@ -772,6 +780,14 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp isAway, awayMessage := client.Away() for _, member := range channel.Members() { + if respectAuditorium { + channel.stateMutex.RLock() + memberModes, ok := channel.members[member] + channel.stateMutex.RUnlock() + if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) { + continue + } + } for _, session := range member.Sessions() { if session == rb.session { continue @@ -889,8 +905,12 @@ func (channel *Channel) playJoinForSession(session *Session) { // Part parts the given client from this channel, with the given message. func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) { - chname := channel.Name() - if !channel.hasClient(client) { + channel.stateMutex.RLock() + chname := channel.name + clientModes, ok := channel.members[client] + channel.stateMutex.RUnlock() + + if !ok { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), chname, client.t("You're not on that channel")) return } @@ -905,7 +925,17 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) if message != "" { params = append(params, message) } + respectAuditorium := channel.flags.HasMode(modes.Auditorium) && + clientModes.HighestChannelUserMode() == modes.Mode(0) for _, member := range channel.Members() { + if respectAuditorium { + channel.stateMutex.RLock() + memberModes, ok := channel.members[member] + channel.stateMutex.RUnlock() + if !ok || memberModes.HighestChannelUserMode() == modes.Mode(0) { + continue + } + } member.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) } rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", params...) @@ -915,12 +945,14 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) } } - channel.AddHistoryItem(history.Item{ - Type: history.Part, - Nick: details.nickMask, - AccountName: details.accountName, - Message: splitMessage, - }, details.account) + if !respectAuditorium { + channel.AddHistoryItem(history.Item{ + Type: history.Part, + Nick: details.nickMask, + AccountName: details.accountName, + Message: splitMessage, + }, details.account) + } client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname)) } @@ -951,20 +983,24 @@ func (channel *Channel) resumeAndAnnounce(session *Session) { // send join for old clients chname := channel.Name() details := session.client.Details() - for _, member := range channel.Members() { - for _, session := range member.Sessions() { - if session.capabilities.Has(caps.Resume) { - continue - } + // TODO: for now, skip this entirely for auditoriums, + // but really we should send it to voiced clients + if !channel.flags.HasMode(modes.Auditorium) { + for _, member := range channel.Members() { + for _, session := range member.Sessions() { + if session.capabilities.Has(caps.Resume) { + continue + } - if session.capabilities.Has(caps.ExtendedJoin) { - session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) - } else { - session.Send(nil, details.nickMask, "JOIN", chname) - } + if session.capabilities.Has(caps.ExtendedJoin) { + session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) + } else { + session.Send(nil, details.nickMask, "JOIN", chname) + } - if 0 < len(oldModes) { - session.Send(nil, channel.server.name, "MODE", chname, oldModes, details.nick) + if 0 < len(oldModes) { + session.Send(nil, channel.server.name, "MODE", chname, oldModes, details.nick) + } } } } @@ -1451,6 +1487,30 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf } } +// returns who the client can "see" in the channel, respecting the auditorium mode +func (channel *Channel) auditoriumFriends(client *Client) (friends []*Client) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + + clientModes := channel.members[client] + if clientModes == nil { + return // non-members have no friends + } + if !channel.flags.HasMode(modes.Auditorium) { + return channel.membersCache // default behavior for members + } + if clientModes.HighestChannelUserMode() != modes.Mode(0) { + return channel.membersCache // +v and up can see everyone in the auditorium + } + // without +v, your friends are those with +v and up + for member, memberModes := range channel.members { + if memberModes.HighestChannelUserMode() != modes.Mode(0) { + friends = append(friends, member) + } + } + return +} + // data for RPL_LIST func (channel *Channel) listData() (memberCount int, name, topic string) { channel.stateMutex.RLock() diff --git a/irc/client.go b/irc/client.go index da4829e5..40406fda 100644 --- a/irc/client.go +++ b/irc/client.go @@ -934,7 +934,7 @@ func (session *Session) playResume() { // work out how much time, if any, is not covered by history buffers // assume that a persistent buffer covers the whole resume period for _, channel := range client.Channels() { - for _, member := range channel.Members() { + for _, member := range channel.auditoriumFriends(client) { friends.Add(member) } status, _ := channel.historyStatus(config) @@ -1161,7 +1161,7 @@ func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]em addFriendsToSet(result, client, capabs...) for _, channel := range client.Channels() { - for _, member := range channel.Members() { + for _, member := range channel.auditoriumFriends(client) { addFriendsToSet(result, member, capabs...) } } @@ -1512,10 +1512,10 @@ func (client *Client) destroy(session *Session) { friends := make(ClientSet) channels = client.Channels() for _, channel := range channels { - channel.Quit(client) - for _, member := range channel.Members() { + for _, member := range channel.auditoriumFriends(client) { friends.Add(member) } + channel.Quit(client) } friends.Remove(client) diff --git a/irc/handlers.go b/irc/handlers.go index 89604db2..431a6c7d 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -3051,7 +3051,13 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo if channel != nil { isJoined := channel.hasClient(client) if !channel.flags.HasMode(modes.Secret) || isJoined || isOper { - for _, member := range channel.Members() { + var members []*Client + if isOper { + members = channel.Members() + } else { + members = channel.auditoriumFriends(client) + } + for _, member := range members { if !member.HasMode(modes.Invisible) || isJoined || isOper { client.rplWhoReply(channel, member, rb, isWhox, fields, whoType) } @@ -3072,6 +3078,9 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo } for _, channel := range otherClient.Channels() { + if channel.flags.HasMode(modes.Auditorium) { + return false // TODO this should respect +v etc. + } if _, present := userChannels[channel]; present { return true } diff --git a/irc/help.go b/irc/help.go index 62b7d81f..8e9801a3 100644 --- a/irc/help.go +++ b/irc/help.go @@ -54,6 +54,8 @@ Oragono supports the following channel modes: +t | Only channel opers can modify the topic. +E | Roleplaying commands are enabled in the channel. +C | Clients are blocked from sending CTCP messages in the channel. + +u | Auditorium mode: JOIN, PART, QUIT, NAMES, and WHO are hidden + hidden from unvoiced clients. = Prefixes = @@ -74,7 +76,7 @@ Oragono supports the following user modes: +Z | User is connected via TLS. +B | User is a bot. +E | User can receive roleplaying commands. - +T | User is blocked from sending CTCP messages.` + +T | CTCP messages to the user are blocked.` snomaskHelpText = `== Server Notice Masks == Oragono supports the following server notice masks for operators: diff --git a/irc/modes/modes.go b/irc/modes/modes.go index 0dfbe5f6..237ef0ca 100644 --- a/irc/modes/modes.go +++ b/irc/modes/modes.go @@ -23,7 +23,7 @@ var ( SupportedChannelModes = Modes{ BanMask, ChanRoleplaying, ExceptMask, InviteMask, InviteOnly, Key, Moderated, NoOutside, OpOnlyTopic, RegisteredOnly, RegisteredOnlySpeak, - Secret, UserLimit, NoCTCP, + Secret, UserLimit, NoCTCP, Auditorium, } ) @@ -113,6 +113,7 @@ const ( // Channel Modes const ( + Auditorium Mode = 'u' // flag BanMask Mode = 'b' // arg ChanRoleplaying Mode = 'E' // flag ExceptMask Mode = 'e' // arg diff --git a/irctest b/irctest index 0287b837..616785ea 160000 --- a/irctest +++ b/irctest @@ -1 +1 @@ -Subproject commit 0287b837971c27ee55bc4dca95d31afc68d2aeea +Subproject commit 616785eae403536954f1b1181f74ef51343e33f7