mirror of
https://github.com/ergochat/ergo.git
synced 2025-01-22 02:04:10 +01:00
Merge pull request #1419 from slingamn/alwayson_channelmodes.1
fix #1345
This commit is contained in:
commit
7e56f62aed
@ -38,11 +38,13 @@ const (
|
|||||||
keyAccountVHost = "account.vhost %s"
|
keyAccountVHost = "account.vhost %s"
|
||||||
keyCertToAccount = "account.creds.certfp %s"
|
keyCertToAccount = "account.creds.certfp %s"
|
||||||
keyAccountChannels = "account.channels %s" // channels registered to the account
|
keyAccountChannels = "account.channels %s" // channels registered to the account
|
||||||
keyAccountJoinedChannels = "account.joinedto %s" // channels a persistent client has joined
|
|
||||||
keyAccountLastSeen = "account.lastseen %s"
|
keyAccountLastSeen = "account.lastseen %s"
|
||||||
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
keyAccountModes = "account.modes %s" // user modes for the always-on client as a string
|
||||||
keyAccountRealname = "account.realname %s" // client realname stored as string
|
keyAccountRealname = "account.realname %s" // client realname stored as string
|
||||||
keyAccountSuspended = "account.suspended %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
|
maxCertfpsPerAccount = 5
|
||||||
)
|
)
|
||||||
@ -542,24 +544,34 @@ func (am *AccountManager) setPassword(account string, password string, hasPrivs
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) saveChannels(account string, channels []string) {
|
func (am *AccountManager) saveChannels(account string, channelToModes map[string]string) {
|
||||||
channelsStr := strings.Join(channels, ",")
|
j, err := json.Marshal(channelToModes)
|
||||||
key := fmt.Sprintf(keyAccountJoinedChannels, account)
|
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 {
|
am.server.store.Update(func(tx *buntdb.Tx) error {
|
||||||
tx.Set(key, channelsStr, nil)
|
tx.Set(key, jStr, nil)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (am *AccountManager) loadChannels(account string) (channels []string) {
|
func (am *AccountManager) loadChannels(account string) (channelToModes map[string]string) {
|
||||||
key := fmt.Sprintf(keyAccountJoinedChannels, account)
|
key := fmt.Sprintf(keyAccountChannelToModes, account)
|
||||||
var channelsStr string
|
var channelsStr string
|
||||||
am.server.store.View(func(tx *buntdb.Tx) error {
|
am.server.store.View(func(tx *buntdb.Tx) error {
|
||||||
channelsStr, _ = tx.Get(key)
|
channelsStr, _ = tx.Get(key)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if channelsStr != "" {
|
if channelsStr == "" {
|
||||||
return strings.Split(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
|
return
|
||||||
}
|
}
|
||||||
@ -1454,7 +1466,7 @@ func (am *AccountManager) Unregister(account string, erase bool) error {
|
|||||||
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
|
||||||
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
|
||||||
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
|
||||||
joinedChannelsKey := fmt.Sprintf(keyAccountJoinedChannels, casefoldedAccount)
|
joinedChannelsKey := fmt.Sprintf(keyAccountChannelToModes, casefoldedAccount)
|
||||||
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
lastSeenKey := fmt.Sprintf(keyAccountLastSeen, casefoldedAccount)
|
||||||
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
unregisteredKey := fmt.Sprintf(keyAccountUnregistered, casefoldedAccount)
|
||||||
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
modesKey := fmt.Sprintf(keyAccountModes, casefoldedAccount)
|
||||||
|
@ -553,6 +553,30 @@ func (channel *Channel) ClientStatus(client *Client) (present bool, cModes modes
|
|||||||
return present, modes.AllModes()
|
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 {
|
func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool {
|
||||||
channel.stateMutex.RLock()
|
channel.stateMutex.RLock()
|
||||||
founder := channel.registeredFounder
|
founder := channel.registeredFounder
|
||||||
@ -1383,6 +1407,9 @@ func (channel *Channel) applyModeToMember(client *Client, change modes.ModeChang
|
|||||||
if !exists {
|
if !exists {
|
||||||
rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.Nick(), channel.Name(), client.t("They aren't on that channel"))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,7 +404,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
|||||||
client.run(session)
|
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()
|
now := time.Now().UTC()
|
||||||
config := server.Config()
|
config := server.Config()
|
||||||
if lastSeen == nil && account.Settings.AutoreplayMissed {
|
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:
|
// XXX set this last to avoid confusing SetNick:
|
||||||
client.registered = true
|
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
|
// 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
|
// this is *probably* ok as long as the persisted memberships are accurate
|
||||||
server.channels.Join(client, chname, "", true, nil)
|
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) {
|
if persistenceEnabled(config.Accounts.Multiclient.AutoAway, client.accountSettings.AutoAway) {
|
||||||
@ -1967,11 +1972,12 @@ func (client *Client) performWrite(additionalDirtyBits uint) {
|
|||||||
|
|
||||||
if (dirtyBits & IncludeChannels) != 0 {
|
if (dirtyBits & IncludeChannels) != 0 {
|
||||||
channels := client.Channels()
|
channels := client.Channels()
|
||||||
channelNames := make([]string, len(channels))
|
channelToModes := make(map[string]string, len(channels))
|
||||||
for i, channel := range channels {
|
for _, channel := range channels {
|
||||||
channelNames[i] = channel.Name()
|
chname, modes := channel.nameAndModes(client)
|
||||||
|
channelToModes[chname] = modes
|
||||||
}
|
}
|
||||||
client.server.accounts.saveChannels(account, channelNames)
|
client.server.accounts.saveChannels(account, channelToModes)
|
||||||
}
|
}
|
||||||
if (dirtyBits & IncludeLastSeen) != 0 {
|
if (dirtyBits & IncludeLastSeen) != 0 {
|
||||||
client.server.accounts.saveLastSeen(account, client.copyLastSeen())
|
client.server.accounts.saveLastSeen(account, client.copyLastSeen())
|
||||||
|
@ -24,7 +24,7 @@ const (
|
|||||||
// 'version' of the database schema
|
// 'version' of the database schema
|
||||||
keySchemaVersion = "db.version"
|
keySchemaVersion = "db.version"
|
||||||
// latest schema of the db
|
// latest schema of the db
|
||||||
latestDbSchema = 18
|
latestDbSchema = 19
|
||||||
|
|
||||||
keyCloakSecret = "crypto.cloak_secret"
|
keyCloakSecret = "crypto.cloak_secret"
|
||||||
)
|
)
|
||||||
@ -903,6 +903,66 @@ func schemaChangeV17ToV18(config *Config, tx *buntdb.Tx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #1345: persist the channel-user modes of always-on clients
|
||||||
|
func schemaChangeV18To19(config *Config, tx *buntdb.Tx) error {
|
||||||
|
channelToAmodesCache := make(map[string]map[string]modes.Mode)
|
||||||
|
joinedto := "account.joinedto "
|
||||||
|
var accounts []string
|
||||||
|
var channels [][]string
|
||||||
|
tx.AscendGreaterOrEqual("", joinedto, func(key, value string) bool {
|
||||||
|
if !strings.HasPrefix(key, joinedto) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
accounts = append(accounts, strings.TrimPrefix(key, joinedto))
|
||||||
|
var ch []string
|
||||||
|
if value != "" {
|
||||||
|
ch = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
channels = append(channels, ch)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := 0; i < len(accounts); i++ {
|
||||||
|
account := accounts[i]
|
||||||
|
channels := channels[i]
|
||||||
|
tx.Delete(joinedto + account)
|
||||||
|
newValue := make(map[string]string, len(channels))
|
||||||
|
for _, channel := range channels {
|
||||||
|
chcfname, err := CasefoldChannel(channel)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// get amodes from the channelToAmodesCache, fill if necessary
|
||||||
|
amodes, ok := channelToAmodesCache[chcfname]
|
||||||
|
if !ok {
|
||||||
|
amodeStr, _ := tx.Get("channel.accounttoumode " + chcfname)
|
||||||
|
if amodeStr != "" {
|
||||||
|
jErr := json.Unmarshal([]byte(amodeStr), &amodes)
|
||||||
|
if jErr != nil {
|
||||||
|
log.Printf("error retrieving amodes for %s: %v\n", channel, jErr)
|
||||||
|
amodes = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// setting/using the nil value here is ok
|
||||||
|
channelToAmodesCache[chcfname] = amodes
|
||||||
|
}
|
||||||
|
if mode, ok := amodes[account]; ok {
|
||||||
|
newValue[channel] = string(mode)
|
||||||
|
} else {
|
||||||
|
newValue[channel] = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newValueBytes, jErr := json.Marshal(newValue)
|
||||||
|
if jErr != nil {
|
||||||
|
log.Printf("couldn't serialize new mode values for v19: %v\n", jErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tx.Set("account.channeltomodes "+account, string(newValueBytes), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
func getSchemaChange(initialVersion int) (result SchemaChange, ok bool) {
|
||||||
for _, change := range allChanges {
|
for _, change := range allChanges {
|
||||||
if initialVersion == change.InitialVersion {
|
if initialVersion == change.InitialVersion {
|
||||||
@ -998,4 +1058,9 @@ var allChanges = []SchemaChange{
|
|||||||
TargetVersion: 18,
|
TargetVersion: 18,
|
||||||
Changer: schemaChangeV17ToV18,
|
Changer: schemaChangeV17ToV18,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
InitialVersion: 18,
|
||||||
|
TargetVersion: 19,
|
||||||
|
Changer: schemaChangeV18To19,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@ -211,9 +211,9 @@ func (client *Client) SetAway(away bool, awayMessage string) (changed bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) AlwaysOn() (alwaysOn bool) {
|
func (client *Client) AlwaysOn() (alwaysOn bool) {
|
||||||
client.stateMutex.Lock()
|
client.stateMutex.RLock()
|
||||||
alwaysOn = client.registered && client.alwaysOn
|
alwaysOn = client.registered && client.alwaysOn
|
||||||
client.stateMutex.Unlock()
|
client.stateMutex.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ const (
|
|||||||
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
// XXX instead of referencing, e.g., keyAccountExists, we should write in the string literal
|
||||||
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
// (to ensure that no matter what code changes happen elsewhere, we're still producing a
|
||||||
// db of the hardcoded version)
|
// db of the hardcoded version)
|
||||||
importDBSchemaVersion = 18
|
importDBSchemaVersion = 19
|
||||||
)
|
)
|
||||||
|
|
||||||
type userImport struct {
|
type userImport struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user