mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-23 19:22:41 +01:00
434 lines
14 KiB
Go
434 lines
14 KiB
Go
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
|
|
// 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.HashSet[string]) {
|
|
result = make(utils.HashSet[string])
|
|
|
|
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
|
|
})
|
|
}
|