// Copyright (c) 2016-2017 Daniel Oaks // released under the MIT license package irc import ( "encoding/json" "fmt" "strconv" "strings" "time" "github.com/tidwall/buntdb" "github.com/ergochat/ergo/irc/modes" "github.com/ergochat/ergo/irc/utils" ) // this is exclusively the *persistence* layer for channel registration; // channel creation/tracking/destruction is in channelmanager.go const ( keyChannelExists = "channel.exists %s" keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped keyChannelRegTime = "channel.registered.time %s" keyChannelFounder = "channel.founder %s" keyChannelTopic = "channel.topic %s" keyChannelTopicSetBy = "channel.topic.setby %s" keyChannelTopicSetTime = "channel.topic.settime %s" keyChannelBanlist = "channel.banlist %s" keyChannelExceptlist = "channel.exceptlist %s" keyChannelInvitelist = "channel.invitelist %s" keyChannelPassword = "channel.key %s" keyChannelModes = "channel.modes %s" keyChannelAccountToUMode = "channel.accounttoumode %s" keyChannelUserLimit = "channel.userlimit %s" keyChannelSettings = "channel.settings %s" keyChannelForward = "channel.forward %s" keyChannelPurged = "channel.purged %s" ) var ( channelKeyStrings = []string{ keyChannelExists, keyChannelName, keyChannelRegTime, keyChannelFounder, keyChannelTopic, keyChannelTopicSetBy, keyChannelTopicSetTime, keyChannelBanlist, keyChannelExceptlist, keyChannelInvitelist, keyChannelPassword, keyChannelModes, keyChannelAccountToUMode, keyChannelUserLimit, keyChannelSettings, keyChannelForward, } ) // these are bit flags indicating what part of the channel status is "dirty" // and needs to be read from memory and written to the db const ( IncludeInitial uint = 1 << iota IncludeTopic IncludeModes IncludeLists IncludeSettings ) // this is an OR of all possible flags const ( IncludeAllAttrs = ^uint(0) ) // RegisteredChannel holds details about a given registered channel. type RegisteredChannel struct { // Name of the channel. Name string // Casefolded name of the channel. NameCasefolded string // RegisteredAt represents the time that the channel was registered. RegisteredAt time.Time // Founder indicates the founder of the channel. Founder string // Topic represents the channel topic. Topic string // TopicSetBy represents the host that set the topic. TopicSetBy string // TopicSetTime represents the time the topic was set. TopicSetTime time.Time // Modes represents the channel modes Modes []modes.Mode // Key represents the channel key / password Key string // Forward is the forwarding/overflow (+f) channel Forward string // UserLimit is the user limit (0 for no limit) UserLimit int // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) AccountToUMode map[string]modes.Mode // Bans represents the bans set on the channel. Bans map[string]MaskInfo // Excepts represents the exceptions set on the channel. Excepts map[string]MaskInfo // Invites represents the invite exceptions set on the channel. Invites map[string]MaskInfo // Settings are the chanserv-modifiable settings Settings ChannelSettings } type ChannelPurgeRecord struct { Oper string PurgedAt time.Time Reason string } // ChannelRegistry manages registered channels. type ChannelRegistry struct { server *Server } // NewChannelRegistry returns a new ChannelRegistry. func (reg *ChannelRegistry) Initialize(server *Server) { reg.server = server } // AllChannels returns the uncasefolded names of all registered channels. func (reg *ChannelRegistry) AllChannels() (result []string) { prefix := fmt.Sprintf(keyChannelName, "") reg.server.store.View(func(tx *buntdb.Tx) error { return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { if !strings.HasPrefix(key, prefix) { return false } result = append(result, value) return true }) }) return } // PurgedChannels returns the set of all casefolded channel names that have been purged func (reg *ChannelRegistry) PurgedChannels() (result utils.StringSet) { result = make(utils.StringSet) prefix := fmt.Sprintf(keyChannelPurged, "") reg.server.store.View(func(tx *buntdb.Tx) error { return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { if !strings.HasPrefix(key, prefix) { return false } channel := strings.TrimPrefix(key, prefix) result.Add(channel) return true }) }) return } // StoreChannel obtains a consistent view of a channel, then persists it to the store. func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) { if !reg.server.ChannelRegistrationEnabled() { return } if info.Founder == "" { // sanity check, don't try to store an unregistered channel return } reg.server.store.Update(func(tx *buntdb.Tx) error { reg.saveChannel(tx, info, includeFlags) return nil }) return nil } // LoadChannel loads a channel from the store. func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) { if !reg.server.ChannelRegistrationEnabled() { err = errFeatureDisabled return } channelKey := nameCasefolded // nice to have: do all JSON (de)serialization outside of the buntdb transaction err = reg.server.store.View(func(tx *buntdb.Tx) error { _, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey)) if dberr == buntdb.ErrNotFound { // chan does not already exist, return return errNoSuchChannel } // channel exists, load it name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey)) regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey)) regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) var topicSetTime time.Time topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil { topicSetTime = time.Unix(0, topicSetTimeInt).UTC() } password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey)) modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey)) userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey)) forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey)) banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey)) settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey)) modeSlice := make([]modes.Mode, len(modeString)) for i, mode := range modeString { modeSlice[i] = modes.Mode(mode) } userLimit, _ := strconv.Atoi(userLimitString) var banlist map[string]MaskInfo _ = json.Unmarshal([]byte(banlistString), &banlist) var exceptlist map[string]MaskInfo _ = json.Unmarshal([]byte(exceptlistString), &exceptlist) var invitelist map[string]MaskInfo _ = json.Unmarshal([]byte(invitelistString), &invitelist) accountToUMode := make(map[string]modes.Mode) _ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode) var settings ChannelSettings _ = json.Unmarshal([]byte(settingsString), &settings) info = RegisteredChannel{ Name: name, NameCasefolded: nameCasefolded, RegisteredAt: time.Unix(0, regTimeInt).UTC(), Founder: founder, Topic: topic, TopicSetBy: topicSetBy, TopicSetTime: topicSetTime, Key: password, Modes: modeSlice, Bans: banlist, Excepts: exceptlist, Invites: invitelist, AccountToUMode: accountToUMode, UserLimit: int(userLimit), Settings: settings, Forward: forward, } return nil }) return } // Delete deletes a channel corresponding to `info`. If no such channel // is present in the database, no error is returned. func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) { if !reg.server.ChannelRegistrationEnabled() { return } reg.server.store.Update(func(tx *buntdb.Tx) error { reg.deleteChannel(tx, info.NameCasefolded, info) return nil }) return nil } // delete a channel, unless it was overwritten by another registration of the same channel func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) { _, err := tx.Get(fmt.Sprintf(keyChannelExists, key)) if err == nil { regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key)) regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) registeredAt := time.Unix(0, regTimeInt).UTC() founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key)) // to see if we're deleting the right channel, confirm the founder and the registration time if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) { for _, keyFmt := range channelKeyStrings { tx.Delete(fmt.Sprintf(keyFmt, key)) } // remove this channel from the client's list of registered channels channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder) channelsStr, err := tx.Get(channelsKey) if err == buntdb.ErrNotFound { return } registeredChannels := unmarshalRegisteredChannels(channelsStr) var nowRegisteredChannels []string for _, channel := range registeredChannels { if channel != key { nowRegisteredChannels = append(nowRegisteredChannels, channel) } } tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil) } } } func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) { channelKey := channelInfo.NameCasefolded chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey) founder, existsErr := tx.Get(chanFounderKey) if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder { // add to new founder's list accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) alreadyChannels, _ := tx.Get(accountChannelsKey) newChannels := channelKey // this is the casefolded channel name if alreadyChannels != "" { newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels) } tx.Set(accountChannelsKey, newChannels, nil) } if existsErr == nil && founder != channelInfo.Founder { // remove from old founder's list accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder) alreadyChannelsRaw, _ := tx.Get(accountChannelsKey) var newChannels []string if alreadyChannelsRaw != "" { for _, chname := range strings.Split(alreadyChannelsRaw, ",") { if chname != channelInfo.NameCasefolded { newChannels = append(newChannels, chname) } } } tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil) } } // saveChannel saves a channel to the store. func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) { channelKey := channelInfo.NameCasefolded // maintain the mapping of account -> registered channels reg.updateAccountToChannelMapping(tx, channelInfo) if includeFlags&IncludeInitial != 0 { tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil) tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) } if includeFlags&IncludeTopic != 0 { tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil) var topicSetTimeStr string if !channelInfo.TopicSetTime.IsZero() { topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10) } tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil) tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) } if includeFlags&IncludeModes != 0 { tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil) modeString := modes.Modes(channelInfo.Modes).String() tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil) tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil) tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil) } if includeFlags&IncludeLists != 0 { banlistString, _ := json.Marshal(channelInfo.Bans) tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) exceptlistString, _ := json.Marshal(channelInfo.Excepts) tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) invitelistString, _ := json.Marshal(channelInfo.Invites) tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode) tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil) } if includeFlags&IncludeSettings != 0 { settingsString, _ := json.Marshal(channelInfo.Settings) tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil) } } // PurgeChannel records a channel purge. func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) { serialized, err := json.Marshal(record) if err != nil { return err } serializedStr := string(serialized) key := fmt.Sprintf(keyChannelPurged, chname) return reg.server.store.Update(func(tx *buntdb.Tx) error { tx.Set(key, serializedStr, nil) return nil }) } // LoadPurgeRecord retrieves information about whether and how a channel was purged. func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) { var rawRecord string key := fmt.Sprintf(keyChannelPurged, chname) reg.server.store.View(func(tx *buntdb.Tx) error { rawRecord, _ = tx.Get(key) return nil }) if rawRecord == "" { err = errNoSuchChannel return } err = json.Unmarshal([]byte(rawRecord), &record) if err != nil { reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error()) err = errNoSuchChannel return } return } // UnpurgeChannel deletes the record of a channel purge. func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) { key := fmt.Sprintf(keyChannelPurged, chname) return reg.server.store.Update(func(tx *buntdb.Tx) error { tx.Delete(key) return nil }) }