diff --git a/irc/accounts.go b/irc/accounts.go index 41b335c1..279f2986 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -38,11 +38,13 @@ const ( keyAccountVHost = "account.vhost %s" keyCertToAccount = "account.creds.certfp %s" keyAccountChannels = "account.channels %s" // channels registered to the account - keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined keyAccountLastSeen = "account.lastseen %s" keyAccountModes = "account.modes %s" // user modes for the always-on client as a string keyAccountRealname = "account.realname %s" // client realname stored as string keyAccountSuspended = "account.suspended %s" // client realname stored as string + // for an always-on client, a map of channel names they're in to their current modes + // (not to be confused with their amodes, which a non-always-on client can have): + keyAccountChannelToModes = "account.channeltomodes %s" maxCertfpsPerAccount = 5 ) @@ -542,24 +544,34 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs return err } -func (am *AccountManager) saveChannels(account string, channels []string) { - channelsStr := strings.Join(channels, ",") - key := fmt.Sprintf(keyAccountJoinedChannels, account) +func (am *AccountManager) saveChannels(account string, channelToModes map[string]string) { + j, err := json.Marshal(channelToModes) + if err != nil { + am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error()) + return + } + jStr := string(j) + key := fmt.Sprintf(keyAccountChannelToModes, account) am.server.store.Update(func(tx *buntdb.Tx) error { - tx.Set(key, channelsStr, nil) + tx.Set(key, jStr, nil) return nil }) } -func (am *AccountManager) loadChannels(account string) (channels []string) { - key := fmt.Sprintf(keyAccountJoinedChannels, account) +func (am *AccountManager) loadChannels(account string) (channelToModes map[string]string) { + key := fmt.Sprintf(keyAccountChannelToModes, account) var channelsStr string am.server.store.View(func(tx *buntdb.Tx) error { channelsStr, _ = tx.Get(key) return nil }) - if channelsStr != "" { - return strings.Split(channelsStr, ",") + if channelsStr == "" { + return nil + } + err := json.Unmarshal([]byte(channelsStr), &channelToModes) + if err != nil { + am.server.logger.Error("internal", "couldn't marshal channel-to-modes", account, err.Error()) + return nil } return } @@ -1454,7 +1466,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error { settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount) vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount) channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount) - joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount) + joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount) lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount) unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount) modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount) diff --git a/irc/channel.go b/irc/channel.go index ae53b558..ed8c05f8 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -553,6 +553,30 @@ func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes return present, modes.AllModes() } +// helper for persisting channel-user modes for always-on clients; +// return the channel name and all channel-user modes for a client +func (channel *Channel) nameAndModes(client *Client) (chname string, modeStr string) { + channel.stateMutex.RLock() + defer channel.stateMutex.RUnlock() + chname = channel.name + modeStr = channel.members[client].String() + return +} + +// overwrite any existing channel-user modes with the stored ones +func (channel *Channel) setModesForClient(client *Client, modeStr string) { + newModes := modes.NewModeSet() + for _, mode := range modeStr { + newModes.SetMode(modes.Mode(mode), true) + } + channel.stateMutex.Lock() + defer channel.stateMutex.Unlock() + if _, ok := channel.members[client]; !ok { + return + } + channel.members[client] = newModes +} + func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool { channel.stateMutex.RLock() founder := channel.registeredFounder @@ -1383,6 +1407,9 @@ func (channel *Channel) applyModeToMember(client *Client, change modes.ModeChang if !exists { rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel")) } + if applied { + target.markDirty(IncludeChannels) + } return } diff --git a/irc/client.go b/irc/client.go index 8f428f49..bc00f7a1 100644 --- a/irc/client.go +++ b/irc/client.go @@ -404,7 +404,7 @@ func (server *Server) RunClient(conn IRCConn) { client.run(session) } -func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen map[string]time.Time, uModes modes.Modes, realname string) { +func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToModes map[string]string, lastSeen map[string]time.Time, uModes modes.Modes, realname string) { now := time.Now().UTC() config := server.Config() if lastSeen == nil && account.Settings.AutoreplayMissed { @@ -463,10 +463,15 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, // XXX set this last to avoid confusing SetNick: client.registered = true - for _, chname := range chnames { + for chname, modeStr := range channelToModes { // XXX we're using isSajoin=true, to make these joins succeed even without channel key // this is *probably* ok as long as the persisted memberships are accurate server.channels.Join(client, chname, "", true, nil) + if channel := server.channels.Get(chname); channel != nil { + channel.setModesForClient(client, modeStr) + } else { + server.logger.Error("internal", "could not create channel", chname) + } } if persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) { @@ -1967,11 +1972,12 @@ func (client *Client) performWrite(additionalDirtyBits uint) { if (dirtyBits & IncludeChannels) != 0 { channels := client.Channels() - channelNames := make([]string, len(channels)) - for i, channel := range channels { - channelNames[i] = channel.Name() + channelToModes := make(map[string]string, len(channels)) + for _, channel := range channels { + chname, modes := channel.nameAndModes(client) + channelToModes[chname] = modes } - client.server.accounts.saveChannels(account, channelNames) + client.server.accounts.saveChannels(account, channelToModes) } if (dirtyBits & IncludeLastSeen) != 0 { client.server.accounts.saveLastSeen(account, client.copyLastSeen()) diff --git a/irc/getters.go b/irc/getters.go index bbc6cf4f..cd3a939e 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -211,9 +211,9 @@ func (client *Client) SetAway(away bool, awayMessage string) (changed bool) { } func (client *Client) AlwaysOn() (alwaysOn bool) { - client.stateMutex.Lock() + client.stateMutex.RLock() alwaysOn = client.registered && client.alwaysOn - client.stateMutex.Unlock() + client.stateMutex.RUnlock() return }