// Copyright (c) 2012-2014 Jeremy Latt // Copyright (c) 2014-2015 Edmund Huber // Copyright (c) 2016-2017 Daniel Oaks // released under the MIT license package irc import ( "bytes" "fmt" "strconv" "strings" "time" "sync" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/history" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/utils" ) // Channel represents a channel that clients can join. type Channel struct { flags modes.ModeSet lists map[modes.Mode]*UserMaskSet key string members MemberSet membersCache []*Client // allow iteration over channel members without holding the lock name string nameCasefolded string server *Server createdTime time.Time registeredFounder string registeredTime time.Time topic string topicSetBy string topicSetTime time.Time userLimit int accountToUMode map[string]modes.Mode history history.Buffer stateMutex sync.RWMutex // tier 1 writerSemaphore utils.Semaphore // tier 1.5 joinPartMutex sync.Mutex // tier 3 ensureLoaded utils.Once // manages loading stored registration info from the database dirtyBits uint } // NewChannel creates a new channel from a `Server` and a `name` // string, which must be unique on the server. func NewChannel(s *Server, name string, registered bool) *Channel { casefoldedName, err := CasefoldChannel(name) if err != nil { s.logger.Error("internal", "Bad channel name", name, err.Error()) return nil } channel := &Channel{ createdTime: time.Now(), // may be overwritten by applyRegInfo lists: map[modes.Mode]*UserMaskSet{ modes.BanMask: NewUserMaskSet(), modes.ExceptMask: NewUserMaskSet(), modes.InviteMask: NewUserMaskSet(), }, members: make(MemberSet), name: name, nameCasefolded: casefoldedName, server: s, accountToUMode: make(map[string]modes.Mode), } config := s.Config() channel.writerSemaphore.Initialize(1) channel.history.Initialize(config.History.ChannelLength) if !registered { for _, mode := range config.Channels.defaultModes { channel.flags.SetMode(mode, true) } // no loading to do, so "mark" the load operation as "done": channel.ensureLoaded.Do(func() {}) } // else: modes will be loaded before first join return channel } // EnsureLoaded blocks until the channel's registration info has been loaded // from the database. func (channel *Channel) EnsureLoaded() { channel.ensureLoaded.Do(func() { nmc := channel.NameCasefolded() info, err := channel.server.channelRegistry.LoadChannel(nmc) if err == nil { channel.applyRegInfo(info) } else { channel.server.logger.Error("internal", "couldn't load channel", nmc, err.Error()) } }) } func (channel *Channel) IsLoaded() bool { return channel.ensureLoaded.Done() } // read in channel state that was persisted in the DB func (channel *Channel) applyRegInfo(chanReg RegisteredChannel) { channel.stateMutex.Lock() defer channel.stateMutex.Unlock() channel.registeredFounder = chanReg.Founder channel.registeredTime = chanReg.RegisteredAt channel.topic = chanReg.Topic channel.topicSetBy = chanReg.TopicSetBy channel.topicSetTime = chanReg.TopicSetTime channel.name = chanReg.Name channel.createdTime = chanReg.RegisteredAt channel.key = chanReg.Key for _, mode := range chanReg.Modes { channel.flags.SetMode(mode, true) } for _, mask := range chanReg.Banlist { channel.lists[modes.BanMask].Add(mask) } for _, mask := range chanReg.Exceptlist { channel.lists[modes.ExceptMask].Add(mask) } for _, mask := range chanReg.Invitelist { channel.lists[modes.InviteMask].Add(mask) } for account, mode := range chanReg.AccountToUMode { channel.accountToUMode[account] = mode } } // obtain a consistent snapshot of the channel state that can be persisted to the DB func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredChannel) { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() info.Name = channel.name info.NameCasefolded = channel.nameCasefolded info.Founder = channel.registeredFounder info.RegisteredAt = channel.registeredTime if includeFlags&IncludeTopic != 0 { info.Topic = channel.topic info.TopicSetBy = channel.topicSetBy info.TopicSetTime = channel.topicSetTime } if includeFlags&IncludeModes != 0 { info.Key = channel.key info.Modes = channel.flags.AllModes() } if includeFlags&IncludeLists != 0 { for mask := range channel.lists[modes.BanMask].masks { info.Banlist = append(info.Banlist, mask) } for mask := range channel.lists[modes.ExceptMask].masks { info.Exceptlist = append(info.Exceptlist, mask) } for mask := range channel.lists[modes.InviteMask].masks { info.Invitelist = append(info.Invitelist, mask) } info.AccountToUMode = make(map[string]modes.Mode) for account, mode := range channel.accountToUMode { info.AccountToUMode[account] = mode } } return } // begin: asynchronous database writeback implementation, modeled on irc/socket.go // MarkDirty marks part (or all) of a channel's data as needing to be written back // to the database, then starts a writer goroutine if necessary. // This is the equivalent of Socket.Write(). func (channel *Channel) MarkDirty(dirtyBits uint) { channel.stateMutex.Lock() isRegistered := channel.registeredFounder != "" channel.dirtyBits = channel.dirtyBits | dirtyBits channel.stateMutex.Unlock() if !isRegistered { return } channel.wakeWriter() } // IsClean returns whether a channel can be safely removed from the server. // To avoid the obvious TOCTOU race condition, it must be called while holding // ChannelManager's lock (that way, no one can join and make the channel dirty again // between this method exiting and the actual deletion). func (channel *Channel) IsClean() bool { if !channel.writerSemaphore.TryAcquire() { // a database write (which may fail) is in progress, the channel cannot be cleaned up return false } defer channel.writerSemaphore.Release() channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() // the channel must be empty, and either be unregistered or fully written to the DB return len(channel.members) == 0 && (channel.registeredFounder == "" || channel.dirtyBits == 0) } func (channel *Channel) wakeWriter() { if channel.writerSemaphore.TryAcquire() { go channel.writeLoop() } } // equivalent of Socket.send() func (channel *Channel) writeLoop() { for { // TODO(#357) check the error value of this and implement timed backoff channel.performWrite(0) channel.writerSemaphore.Release() channel.stateMutex.RLock() isDirty := channel.dirtyBits != 0 isEmpty := len(channel.members) == 0 channel.stateMutex.RUnlock() if !isDirty { if isEmpty { channel.server.channels.Cleanup(channel) } return // nothing to do } // else: isDirty, so we need to write again if !channel.writerSemaphore.TryAcquire() { return } } } // Store writes part (or all) of the channel's data back to the database, // blocking until the write is complete. This is the equivalent of // Socket.BlockingWrite. func (channel *Channel) Store(dirtyBits uint) (err error) { defer func() { channel.stateMutex.Lock() isDirty := channel.dirtyBits != 0 isEmpty := len(channel.members) == 0 channel.stateMutex.Unlock() if isDirty { channel.wakeWriter() } else if isEmpty { channel.server.channels.Cleanup(channel) } }() channel.writerSemaphore.Acquire() defer channel.writerSemaphore.Release() return channel.performWrite(dirtyBits) } // do an individual write; equivalent of Socket.send() func (channel *Channel) performWrite(additionalDirtyBits uint) (err error) { channel.stateMutex.Lock() dirtyBits := channel.dirtyBits | additionalDirtyBits channel.dirtyBits = 0 isRegistered := channel.registeredFounder != "" channel.stateMutex.Unlock() if !isRegistered || dirtyBits == 0 { return } info := channel.ExportRegistration(dirtyBits) err = channel.server.channelRegistry.StoreChannel(info, dirtyBits) if err != nil { channel.stateMutex.Lock() channel.dirtyBits = channel.dirtyBits | dirtyBits channel.stateMutex.Unlock() } return } // SetRegistered registers the channel, returning an error if it was already registered. func (channel *Channel) SetRegistered(founder string) error { channel.stateMutex.Lock() defer channel.stateMutex.Unlock() if channel.registeredFounder != "" { return errChannelAlreadyRegistered } channel.registeredFounder = founder channel.registeredTime = time.Now() channel.accountToUMode[founder] = modes.ChannelFounder return nil } // SetUnregistered deletes the channel's registration information. func (channel *Channel) SetUnregistered(expectedFounder string) { channel.stateMutex.Lock() defer channel.stateMutex.Unlock() if channel.registeredFounder != expectedFounder { return } channel.registeredFounder = "" var zeroTime time.Time channel.registeredTime = zeroTime channel.accountToUMode = make(map[string]modes.Mode) } // IsRegistered returns whether the channel is registered. func (channel *Channel) IsRegistered() bool { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() return channel.registeredFounder != "" } func (channel *Channel) regenerateMembersCache() { channel.stateMutex.RLock() result := make([]*Client, len(channel.members)) i := 0 for client := range channel.members { result[i] = client i++ } channel.stateMutex.RUnlock() channel.stateMutex.Lock() channel.membersCache = result channel.stateMutex.Unlock() } // 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) isOper := client.HasMode(modes.Operator) isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix) isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames) maxNamLen := 480 - len(client.server.name) - len(client.Nick()) var namesLines []string var buffer bytes.Buffer if isJoined || !channel.flags.HasMode(modes.Secret) || isOper { for _, target := range channel.Members() { var nick string if isUserhostInNames { nick = target.NickMaskString() } else { nick = target.Nick() } channel.stateMutex.RLock() modeSet := channel.members[target] channel.stateMutex.RUnlock() if modeSet == nil { continue } if !isJoined && target.flags.HasMode(modes.Invisible) && !isOper { continue } prefix := modeSet.Prefixes(isMultiPrefix) if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen { namesLines = append(namesLines, buffer.String()) buffer.Reset() } if buffer.Len() > 0 { buffer.WriteString(" ") } buffer.WriteString(prefix) buffer.WriteString(nick) } if buffer.Len() > 0 { namesLines = append(namesLines, buffer.String()) } } for _, line := range namesLines { if buffer.Len() > 0 { rb.Add(nil, client.server.name, RPL_NAMREPLY, client.nick, "=", channel.name, line) } } rb.Add(nil, client.server.name, RPL_ENDOFNAMES, client.nick, channel.name, client.t("End of NAMES list")) } // does `clientMode` give you privileges to grant/remove `targetMode` to/from people, // or to kick them? func channelUserModeHasPrivsOver(clientMode modes.Mode, targetMode modes.Mode) bool { switch clientMode { case modes.ChannelFounder: return true case modes.ChannelAdmin, modes.ChannelOperator: // admins cannot kick other admins, operators *can* kick other operators return targetMode != modes.ChannelFounder && targetMode != modes.ChannelAdmin case modes.Halfop: // halfops cannot kick other halfops return targetMode != modes.ChannelFounder && targetMode != modes.ChannelAdmin && targetMode != modes.Halfop default: // voice and unprivileged cannot kick anyone return false } } // ClientIsAtLeast returns whether the client has at least the given channel privilege. func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) bool { channel.stateMutex.RLock() clientModes := channel.members[client] channel.stateMutex.RUnlock() for _, mode := range modes.ChannelUserModes { if clientModes.HasMode(mode) { return true } if mode == permission { break } } return false } func (channel *Channel) ClientPrefixes(client *Client, isMultiPrefix bool) string { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() modes, present := channel.members[client] if !present { return "" } else { return modes.Prefixes(isMultiPrefix) } } func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { channel.stateMutex.RLock() clientModes := channel.members[client] targetModes := channel.members[target] channel.stateMutex.RUnlock() return channelUserModeHasPrivsOver(clientModes.HighestChannelUserMode(), targetModes.HighestChannelUserMode()) } func (channel *Channel) hasClient(client *Client) bool { channel.stateMutex.RLock() _, present := channel.members[client] channel.stateMutex.RUnlock() return present } // func (channel *Channel) modeStrings(client *Client) (result []string) { isMember := client.HasMode(modes.Operator) || channel.hasClient(client) showKey := isMember && (channel.key != "") showUserLimit := channel.userLimit > 0 mods := "+" // flags with args if showKey { mods += modes.Key.String() } if showUserLimit { mods += modes.UserLimit.String() } mods += channel.flags.String() channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() result = []string{mods} // args for flags with args: The order must match above to keep // positional arguments in place. if showKey { result = append(result, channel.key) } if showUserLimit { result = append(result, strconv.Itoa(channel.userLimit)) } return } func (channel *Channel) IsEmpty() bool { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() return len(channel.members) == 0 } // Join joins the given client to this channel (if they can be joined). func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *ResponseBuffer) { details := client.Details() channel.stateMutex.RLock() chname := channel.name chcfname := channel.nameCasefolded founder := channel.registeredFounder chkey := channel.key limit := channel.userLimit chcount := len(channel.members) _, alreadyJoined := channel.members[client] persistentMode := channel.accountToUMode[details.account] channel.stateMutex.RUnlock() if alreadyJoined { // no message needs to be sent return } // the founder can always join (even if they disabled auto +q on join); // anyone who automatically receives halfop or higher can always join hasPrivs := isSajoin || (founder != "" && founder == details.account) || (persistentMode != 0 && persistentMode != modes.Voice) if !hasPrivs && limit != 0 && chcount >= limit { rb.Add(nil, client.server.name, ERR_CHANNELISFULL, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "l")) return } if !hasPrivs && chkey != "" && !utils.SecretTokensMatch(chkey, key) { rb.Add(nil, client.server.name, ERR_BADCHANNELKEY, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "k")) return } isInvited := client.CheckInvited(chcfname) || channel.lists[modes.InviteMask].Match(details.nickMaskCasefolded) if !hasPrivs && channel.flags.HasMode(modes.InviteOnly) && !isInvited { rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i")) return } if !hasPrivs && channel.lists[modes.BanMask].Match(details.nickMaskCasefolded) && !isInvited && !channel.lists[modes.ExceptMask].Match(details.nickMaskCasefolded) { rb.Add(nil, client.server.name, ERR_BANNEDFROMCHAN, details.nick, chname, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "b")) return } client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname)) var message utils.SplitMessage givenMode := func() (givenMode modes.Mode) { channel.joinPartMutex.Lock() defer channel.joinPartMutex.Unlock() func() { channel.stateMutex.Lock() defer channel.stateMutex.Unlock() channel.members.Add(client) firstJoin := len(channel.members) == 1 newChannel := firstJoin && channel.registeredFounder == "" if newChannel { givenMode = modes.ChannelOperator } else { givenMode = persistentMode } if givenMode != 0 { channel.members[client].SetMode(givenMode, true) } }() channel.regenerateMembersCache() message = utils.MakeSplitMessage("", true) histItem := history.Item{ Type: history.Join, Nick: details.nickMask, AccountName: details.accountName, Message: message, } histItem.Params[0] = details.realname channel.history.Add(histItem) return }() client.addChannel(channel) var modestr string if givenMode != 0 { modestr = fmt.Sprintf("+%v", givenMode) } for _, member := range channel.Members() { for _, session := range member.Sessions() { if session == rb.session { continue } else if client == session.client { channel.playJoinForSession(session) continue } if session.capabilities.Has(caps.ExtendedJoin) { session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) } else { session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) } if givenMode != 0 { session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) } } } if rb.session.capabilities.Has(caps.ExtendedJoin) { rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) } else { rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) } if rb.session.client == client { // don't send topic and names for a SAJOIN of a different client channel.SendTopic(client, rb, false) channel.Names(client, rb) } // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex rb.Flush(true) replayLimit := channel.server.Config().History.AutoreplayOnJoin if 0 < replayLimit { // TODO don't replay the client's own JOIN line? items := channel.history.Latest(replayLimit) if 0 < len(items) { channel.replayHistoryItems(rb, items, true) rb.Flush(true) } } } // plays channel join messages (the JOIN line, topic, and names) to a session. // this is used when attaching a new session to an existing client that already has // channels, and also when one session of a client initiates a JOIN and the other // sessions need to receive the state change func (channel *Channel) playJoinForSession(session *Session) { client := session.client sessionRb := NewResponseBuffer(session) if session.capabilities.Has(caps.ExtendedJoin) { sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname()) } else { sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name()) } channel.SendTopic(client, sessionRb, false) channel.Names(client, sessionRb) sessionRb.Send(false) } // 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) { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), chname, client.t("You're not on that channel")) return } channel.Quit(client) splitMessage := utils.MakeSplitMessage(message, true) details := client.Details() for _, member := range channel.Members() { member.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) } rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) for _, session := range client.Sessions() { if session != rb.session { session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) } } channel.history.Add(history.Item{ Type: history.Part, Nick: details.nickMask, AccountName: details.accountName, Message: splitMessage, }) client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname)) } // Resume is called after a successful global resume to: // 1. Replace the old client with the new in the channel's data structures // 2. Send JOIN and MODE lines to channel participants (including the new client) // 3. Replay missed message history to the client func (channel *Channel) Resume(newClient, oldClient *Client, timestamp time.Time) { now := time.Now() channel.resumeAndAnnounce(newClient, oldClient) if !timestamp.IsZero() { channel.replayHistoryForResume(newClient, timestamp, now) } } func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { var oldModeSet *modes.ModeSet func() { channel.joinPartMutex.Lock() defer channel.joinPartMutex.Unlock() defer channel.regenerateMembersCache() channel.stateMutex.Lock() defer channel.stateMutex.Unlock() newClient.channels[channel] = true oldModeSet = channel.members[oldClient] if oldModeSet == nil { oldModeSet = modes.NewModeSet() } channel.members.Remove(oldClient) channel.members[newClient] = oldModeSet }() // construct fake modestring if necessary oldModes := oldModeSet.String() if 0 < len(oldModes) { oldModes = "+" + oldModes } // send join for old clients nick := newClient.Nick() nickMask := newClient.NickMaskString() accountName := newClient.AccountName() realName := newClient.Realname() 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, nickMask, "JOIN", channel.name, accountName, realName) } else { session.Send(nil, nickMask, "JOIN", channel.name) } if 0 < len(oldModes) { session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick) } } } rb := NewResponseBuffer(newClient.Sessions()[0]) // use blocking i/o to synchronize with the later history replay if rb.session.capabilities.Has(caps.ExtendedJoin) { rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName) } else { rb.Add(nil, nickMask, "JOIN", channel.name) } channel.SendTopic(newClient, rb, false) channel.Names(newClient, rb) if 0 < len(oldModes) { rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick) } rb.Send(true) } func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) { items, complete := channel.history.Between(after, before, false, 0) rb := NewResponseBuffer(newClient.Sessions()[0]) channel.replayHistoryItems(rb, items, false) if !complete && !newClient.resumeDetails.HistoryIncomplete { // warn here if we didn't warn already rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost")) } rb.Send(true) } func stripMaskFromNick(nickMask string) (nick string) { index := strings.Index(nickMask, "!") if index == -1 { return nickMask } return nickMask[0:index] } // munge the msgid corresponding to a replayable event, // yielding a consistent msgid for the fake PRIVMSG from HistServ func mungeMsgidForHistserv(token string) (result string) { return fmt.Sprintf("_%s", token) } func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) { chname := channel.Name() client := rb.target eventPlayback := rb.session.capabilities.Has(caps.EventPlayback) extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin) if len(items) == 0 { return } batchID := rb.StartNestedHistoryBatch(chname) defer rb.EndNestedBatch(batchID) for _, item := range items { nick := stripMaskFromNick(item.Nick) switch item.Type { case history.Privmsg: rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "PRIVMSG", chname, item.Message) case history.Notice: rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "NOTICE", chname, item.Message) case history.Tagmsg: if rb.session.capabilities.Has(caps.MessageTags) { rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "TAGMSG", chname, item.Message) } case history.Join: if eventPlayback { if extendedJoin { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0]) } else { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname) } } else { if autoreplay { continue // #474 } var message string if item.AccountName == "*" { message = fmt.Sprintf(client.t("%s joined the channel"), nick) } else { message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) } rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Part: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message) } else { if autoreplay { continue // #474 } message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Kick: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message) } else { message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message) rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Quit: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message) } else { if autoreplay { continue // #474 } message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Nick: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0]) } else { message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } } } } // SendTopic sends the channel topic to the given client. // `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) { channel.stateMutex.RLock() name := channel.name topic := channel.topic topicSetBy := channel.topicSetBy topicSetTime := channel.topicSetTime _, hasClient := channel.members[client] channel.stateMutex.RUnlock() if !hasClient { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel")) return } if topic == "" { if sendNoTopic { rb.Add(nil, client.server.name, RPL_NOTOPIC, client.nick, name, client.t("No topic is set")) } return } rb.Add(nil, client.server.name, RPL_TOPIC, client.nick, name, topic) rb.Add(nil, client.server.name, RPL_TOPICTIME, client.nick, name, topicSetBy, strconv.FormatInt(topicSetTime.Unix(), 10)) } // SetTopic sets the topic of this channel, if the client is allowed to do so. func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffer) { if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) return } if channel.flags.HasMode(modes.OpOnlyTopic) && !channel.ClientIsAtLeast(client, modes.ChannelOperator) { rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You're not a channel operator")) return } topicLimit := client.server.Limits().TopicLen if len(topic) > topicLimit { topic = topic[:topicLimit] } channel.stateMutex.Lock() channel.topic = topic channel.topicSetBy = client.nickMaskString channel.topicSetTime = time.Now() channel.stateMutex.Unlock() prefix := client.NickMaskString() for _, member := range channel.Members() { for _, session := range member.Sessions() { if session == rb.session { rb.Add(nil, prefix, "TOPIC", channel.name, topic) } else { session.Send(nil, prefix, "TOPIC", channel.name, topic) } } } channel.MarkDirty(IncludeTopic) } // CanSpeak returns true if the client can speak on this channel. func (channel *Channel) CanSpeak(client *Client) bool { channel.stateMutex.RLock() defer channel.stateMutex.RUnlock() _, hasClient := channel.members[client] if channel.flags.HasMode(modes.NoOutside) && !hasClient { return false } if channel.flags.HasMode(modes.Moderated) && !channel.ClientIsAtLeast(client, modes.Voice) { return false } if channel.flags.HasMode(modes.RegisteredOnly) && client.Account() == "" { return false } return true } func msgCommandToHistType(server *Server, command string) (history.ItemType, error) { switch command { case "PRIVMSG": return history.Privmsg, nil case "NOTICE": return history.Notice, nil case "TAGMSG": return history.Tagmsg, nil default: server.logger.Error("internal", "unrecognized messaging command", command) return history.ItemType(0), errInvalidParams } } func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mode, clientOnlyTags map[string]string, client *Client, message utils.SplitMessage, rb *ResponseBuffer) { histType, err := msgCommandToHistType(channel.server, command) if err != nil { return } if !channel.CanSpeak(client) { if histType != history.Notice { rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, client.Nick(), channel.Name(), client.t("Cannot send to channel")) } return } nickmask := client.NickMaskString() account := client.AccountName() chname := channel.Name() now := time.Now().UTC() // STATUSMSG targets are prefixed with the supplied min-prefix, e.g., @#channel if minPrefixMode != modes.Mode(0) { chname = fmt.Sprintf("%s%s", modes.ChannelModePrefixes[minPrefixMode], chname) } // send echo-message // TODO this should use `now` as the time for consistency if rb.session.capabilities.Has(caps.EchoMessage) { var tagsToUse map[string]string if rb.session.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags } if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { rb.AddFromClient(message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname) } else { rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message) } } // send echo-message to other connected sessions for _, session := range client.Sessions() { if session == rb.session || !session.capabilities.SelfMessagesEnabled() { continue } var tagsToUse map[string]string if session.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags } if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) { session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname) } else { session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message) } } for _, member := range channel.Members() { // echo-message is handled above, so skip sending the msg to the user themselves as well if member == client { continue } if minPrefixMode != modes.Mode(0) && !channel.ClientIsAtLeast(member, minPrefixMode) { // STATUSMSG continue } for _, session := range member.Sessions() { var tagsToUse map[string]string if session.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags } else if histType == history.Tagmsg { continue } if histType == history.Tagmsg { session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname) } else { session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message) } } } channel.history.Add(history.Item{ Type: histType, Message: message, Nick: nickmask, AccountName: account, Tags: clientOnlyTags, }) } func (channel *Channel) applyModeToMember(client *Client, mode modes.Mode, op modes.ModeOp, nick string, rb *ResponseBuffer) (result *modes.ModeChange) { casefoldedName, err := CasefoldName(nick) target := channel.server.clients.Get(casefoldedName) if err != nil || target == nil { rb.Add(nil, client.server.name, ERR_NOSUCHNICK, client.Nick(), nick, client.t("No such nick")) return nil } channel.stateMutex.Lock() modeset, exists := channel.members[target] if exists { if modeset.SetMode(mode, op == modes.Add) { result = &modes.ModeChange{ Op: op, Mode: mode, Arg: nick, } } } channel.stateMutex.Unlock() if !exists { rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel")) } return } // ShowMaskList shows the given list to the client. func (channel *Channel) ShowMaskList(client *Client, mode modes.Mode, rb *ResponseBuffer) { // choose appropriate modes var rpllist, rplendoflist string if mode == modes.BanMask { rpllist = RPL_BANLIST rplendoflist = RPL_ENDOFBANLIST } else if mode == modes.ExceptMask { rpllist = RPL_EXCEPTLIST rplendoflist = RPL_ENDOFEXCEPTLIST } else if mode == modes.InviteMask { rpllist = RPL_INVITELIST rplendoflist = RPL_ENDOFINVITELIST } nick := client.Nick() channel.stateMutex.RLock() // XXX don't acquire any new locks in this section, besides Socket.Write for mask := range channel.lists[mode].masks { rb.Add(nil, client.server.name, rpllist, nick, channel.name, mask) } channel.stateMutex.RUnlock() rb.Add(nil, client.server.name, rplendoflist, nick, channel.name, client.t("End of list")) } func (channel *Channel) applyModeMask(client *Client, mode modes.Mode, op modes.ModeOp, mask string, rb *ResponseBuffer) bool { list := channel.lists[mode] if list == nil { // This should never happen, but better safe than panicky. return false } if (op == modes.List) || (mask == "") { channel.ShowMaskList(client, mode, rb) return false } if !channel.ClientIsAtLeast(client, modes.ChannelOperator) { rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You're not a channel operator")) return false } if op == modes.Add { return list.Add(mask) } if op == modes.Remove { return list.Remove(mask) } return false } // Quit removes the given client from the channel func (channel *Channel) Quit(client *Client) { channelEmpty := func() bool { channel.joinPartMutex.Lock() defer channel.joinPartMutex.Unlock() channel.stateMutex.Lock() channel.members.Remove(client) channelEmpty := len(channel.members) == 0 channel.stateMutex.Unlock() channel.regenerateMembersCache() return channelEmpty }() if channelEmpty { client.server.channels.Cleanup(channel) } client.removeChannel(channel) } func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) { if !(client.HasMode(modes.Operator) || channel.hasClient(client)) { rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.Name(), client.t("You're not on that channel")) return } if !channel.hasClient(target) { rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel")) return } if !channel.ClientHasPrivsOver(client, target) { rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, client.Nick(), channel.Name(), client.t("You don't have enough channel privileges")) return } kicklimit := client.server.Limits().KickLen if len(comment) > kicklimit { comment = comment[:kicklimit] } message := utils.MakeSplitMessage(comment, true) clientMask := client.NickMaskString() clientAccount := client.AccountName() targetNick := target.Nick() chname := channel.Name() for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { session.sendFromClientInternal(false, message.Time, message.Msgid, clientMask, clientAccount, nil, "KICK", chname, targetNick, comment) } } } rb.Add(nil, clientMask, "KICK", chname, targetNick, comment) histItem := history.Item{ Type: history.Kick, Nick: clientMask, AccountName: target.AccountName(), Message: message, } histItem.Params[0] = targetNick channel.history.Add(histItem) channel.Quit(target) } // Invite invites the given client to the channel, if the inviter can do so. func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) { chname := channel.Name() if channel.flags.HasMode(modes.InviteOnly) && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) { rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, inviter.Nick(), channel.Name(), inviter.t("You're not a channel operator")) return } if !channel.hasClient(inviter) { rb.Add(nil, inviter.server.name, ERR_NOTONCHANNEL, inviter.Nick(), channel.Name(), inviter.t("You're not on that channel")) return } if channel.flags.HasMode(modes.InviteOnly) { invitee.Invite(channel.NameCasefolded()) } for _, member := range channel.Members() { if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) { continue } for _, session := range member.Sessions() { if session.capabilities.Has(caps.InviteNotify) { session.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname) } } } cnick := inviter.Nick() tnick := invitee.Nick() rb.Add(nil, inviter.server.name, RPL_INVITING, cnick, tnick, chname) invitee.Send(nil, inviter.NickMaskString(), "INVITE", tnick, chname) if invitee.Away() { rb.Add(nil, inviter.server.name, RPL_AWAY, cnick, tnick, invitee.AwayMessage()) } }