123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- // Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
- // released under the MIT license
-
- package irc
-
- import (
- "sort"
- "sync"
- "time"
-
- "github.com/ergochat/ergo/irc/datastore"
- "github.com/ergochat/ergo/irc/utils"
- )
-
- type channelManagerEntry struct {
- channel *Channel
- // this is a refcount for joins, so we can avoid a race where we incorrectly
- // think the channel is empty (without holding a lock across the entire Channel.Join()
- // call)
- pendingJoins int
- skeleton string
- }
-
- // ChannelManager keeps track of all the channels on the server,
- // providing synchronization for creation of new channels on first join,
- // cleanup of empty channels on last part, and renames.
- type ChannelManager struct {
- sync.RWMutex // tier 2
- // chans is the main data structure, mapping casefolded name -> *Channel
- chans map[string]*channelManagerEntry
- chansSkeletons utils.HashSet[string]
- purgedChannels map[string]ChannelPurgeRecord // casefolded name to purge record
- server *Server
- }
-
- // NewChannelManager returns a new ChannelManager.
- func (cm *ChannelManager) Initialize(server *Server, config *Config) (err error) {
- cm.chans = make(map[string]*channelManagerEntry)
- cm.chansSkeletons = make(utils.HashSet[string])
- cm.server = server
- return cm.loadRegisteredChannels(config)
- }
-
- func (cm *ChannelManager) loadRegisteredChannels(config *Config) (err error) {
- allChannels, err := FetchAndDeserializeAll[RegisteredChannel](datastore.TableChannels, cm.server.dstore, cm.server.logger)
- if err != nil {
- return
- }
- allPurgeRecords, err := FetchAndDeserializeAll[ChannelPurgeRecord](datastore.TableChannelPurges, cm.server.dstore, cm.server.logger)
- if err != nil {
- return
- }
-
- cm.Lock()
- defer cm.Unlock()
-
- cm.purgedChannels = make(map[string]ChannelPurgeRecord, len(allPurgeRecords))
- for _, purge := range allPurgeRecords {
- cm.purgedChannels[purge.NameCasefolded] = purge
- }
-
- for _, regInfo := range allChannels {
- cfname, err := CasefoldChannel(regInfo.Name)
- if err != nil {
- cm.server.logger.Error("channels", "couldn't casefold registered channel, skipping", regInfo.Name, err.Error())
- continue
- } else {
- cm.server.logger.Debug("channels", "initializing registered channel", regInfo.Name)
- }
- skeleton, err := Skeleton(regInfo.Name)
- if err == nil {
- cm.chansSkeletons.Add(skeleton)
- }
-
- if _, ok := cm.purgedChannels[cfname]; !ok {
- ch := NewChannel(cm.server, regInfo.Name, cfname, true, regInfo)
- cm.chans[cfname] = &channelManagerEntry{
- channel: ch,
- pendingJoins: 0,
- skeleton: skeleton,
- }
- }
- }
-
- return nil
- }
-
- // Get returns an existing channel with name equivalent to `name`, or nil
- func (cm *ChannelManager) Get(name string) (channel *Channel) {
- name, err := CasefoldChannel(name)
- if err != nil {
- return nil
- }
- cm.RLock()
- defer cm.RUnlock()
- entry := cm.chans[name]
- if entry != nil {
- return entry.channel
- }
- return nil
- }
-
- // Join causes `client` to join the channel named `name`, creating it if necessary.
- func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) (err error, forward string) {
- server := client.server
- casefoldedName, err := CasefoldChannel(name)
- skeleton, skerr := Skeleton(name)
- if err != nil || skerr != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
- return errNoSuchChannel, ""
- }
-
- channel, err, newChannel := func() (*Channel, error, bool) {
- var newChannel bool
- cm.Lock()
- defer cm.Unlock()
-
- // check purges first; a registered purged channel will still be present in `chans`
- if _, ok := cm.purgedChannels[casefoldedName]; ok {
- return nil, errChannelPurged, false
- }
- entry := cm.chans[casefoldedName]
- if entry == nil {
- if server.Config().Channels.OpOnlyCreation &&
- !(isSajoin || client.HasRoleCapabs("chanreg")) {
- return nil, errInsufficientPrivs, false
- }
- // enforce confusables
- if cm.chansSkeletons.Has(skeleton) {
- return nil, errConfusableIdentifier, false
- }
- entry = &channelManagerEntry{
- channel: NewChannel(server, name, casefoldedName, false, RegisteredChannel{}),
- pendingJoins: 0,
- }
- cm.chansSkeletons.Add(skeleton)
- entry.skeleton = skeleton
- cm.chans[casefoldedName] = entry
- newChannel = true
- }
- entry.pendingJoins += 1
- return entry.channel, nil, newChannel
- }()
-
- if err != nil {
- return err, ""
- }
-
- err, forward = channel.Join(client, key, isSajoin || newChannel, rb)
-
- cm.maybeCleanup(channel, true)
-
- return
- }
-
- func (cm *ChannelManager) maybeCleanup(channel *Channel, afterJoin bool) {
- cm.Lock()
- defer cm.Unlock()
-
- cfname := channel.NameCasefolded()
-
- entry := cm.chans[cfname]
- if entry == nil || entry.channel != channel {
- return
- }
-
- cm.maybeCleanupInternal(cfname, entry, afterJoin)
- }
-
- func (cm *ChannelManager) maybeCleanupInternal(cfname string, entry *channelManagerEntry, afterJoin bool) {
- if afterJoin {
- entry.pendingJoins -= 1
- }
- if entry.pendingJoins == 0 && entry.channel.IsClean() {
- delete(cm.chans, cfname)
- if entry.skeleton != "" {
- delete(cm.chansSkeletons, entry.skeleton)
- }
- }
- }
-
- // Part parts `client` from the channel named `name`, deleting it if it's empty.
- func (cm *ChannelManager) Part(client *Client, name string, message string, rb *ResponseBuffer) error {
- var channel *Channel
-
- casefoldedName, err := CasefoldChannel(name)
- if err != nil {
- return errNoSuchChannel
- }
-
- cm.RLock()
- entry := cm.chans[casefoldedName]
- if entry != nil {
- channel = entry.channel
- }
- cm.RUnlock()
-
- if channel == nil {
- return errNoSuchChannel
- }
- channel.Part(client, message, rb)
- return nil
- }
-
- func (cm *ChannelManager) Cleanup(channel *Channel) {
- cm.maybeCleanup(channel, false)
- }
-
- func (cm *ChannelManager) SetRegistered(channelName string, account string) (err error) {
- if cm.server.Defcon() <= 4 {
- return errFeatureDisabled
- }
-
- var channel *Channel
- cfname, err := CasefoldChannel(channelName)
- if err != nil {
- return err
- }
-
- var entry *channelManagerEntry
-
- defer func() {
- if err == nil && channel != nil {
- // registration was successful: make the database reflect it
- err = channel.Store(IncludeAllAttrs)
- }
- }()
-
- cm.Lock()
- defer cm.Unlock()
- entry = cm.chans[cfname]
- if entry == nil {
- return errNoSuchChannel
- }
- channel = entry.channel
- err = channel.SetRegistered(account)
- if err != nil {
- return err
- }
- return nil
- }
-
- func (cm *ChannelManager) SetUnregistered(channelName string, account string) (err error) {
- cfname, err := CasefoldChannel(channelName)
- if err != nil {
- return err
- }
-
- var uuid utils.UUID
-
- defer func() {
- if err == nil {
- if delErr := cm.server.dstore.Delete(datastore.TableChannels, uuid); delErr != nil {
- cm.server.logger.Error("datastore", "couldn't delete channel registration", cfname, delErr.Error())
- }
- }
- }()
-
- cm.Lock()
- defer cm.Unlock()
- entry := cm.chans[cfname]
- if entry != nil {
- if entry.channel.Founder() != account {
- return errChannelNotOwnedByAccount
- }
- uuid = entry.channel.UUID()
- entry.channel.SetUnregistered(account) // changes the UUID
- // #1619: if the channel has 0 members and was only being retained
- // because it was registered, clean it up:
- cm.maybeCleanupInternal(cfname, entry, false)
- }
- return nil
- }
-
- // Rename renames a channel (but does not notify the members)
- func (cm *ChannelManager) Rename(name string, newName string) (err error) {
- oldCfname, err := CasefoldChannel(name)
- if err != nil {
- return errNoSuchChannel
- }
-
- newCfname, err := CasefoldChannel(newName)
- if err != nil {
- return errInvalidChannelName
- }
- newSkeleton, err := Skeleton(newName)
- if err != nil {
- return errInvalidChannelName
- }
-
- var channel *Channel
- var info RegisteredChannel
- defer func() {
- if channel != nil && info.Founder != "" {
- channel.MarkDirty(IncludeAllAttrs)
- }
- // always-on clients need to update their saved channel memberships
- for _, member := range channel.Members() {
- member.markDirty(IncludeChannels)
- }
- }()
-
- cm.Lock()
- defer cm.Unlock()
-
- entry := cm.chans[oldCfname]
- if entry == nil {
- return errNoSuchChannel
- }
- channel = entry.channel
- info = channel.ExportRegistration()
- registered := info.Founder != ""
-
- oldSkeleton, err := Skeleton(info.Name)
- if err != nil {
- return errNoSuchChannel // ugh
- }
-
- if newCfname != oldCfname {
- if cm.chans[newCfname] != nil {
- return errChannelNameInUse
- }
- }
-
- if oldSkeleton != newSkeleton {
- if cm.chansSkeletons.Has(newSkeleton) {
- return errConfusableIdentifier
- }
- }
-
- delete(cm.chans, oldCfname)
- if !registered {
- entry.skeleton = newSkeleton
- }
- cm.chans[newCfname] = entry
- delete(cm.chansSkeletons, oldSkeleton)
- cm.chansSkeletons.Add(newSkeleton)
- entry.channel.Rename(newName, newCfname)
- return nil
- }
-
- // Len returns the number of channels
- func (cm *ChannelManager) Len() int {
- cm.RLock()
- defer cm.RUnlock()
- return len(cm.chans)
- }
-
- // Channels returns a slice containing all current channels
- func (cm *ChannelManager) Channels() (result []*Channel) {
- cm.RLock()
- defer cm.RUnlock()
- result = make([]*Channel, 0, len(cm.chans))
- for _, entry := range cm.chans {
- result = append(result, entry.channel)
- }
- return
- }
-
- // ListableChannels returns a slice of all non-purged channels.
- func (cm *ChannelManager) ListableChannels() (result []*Channel) {
- cm.RLock()
- defer cm.RUnlock()
- result = make([]*Channel, 0, len(cm.chans))
- for cfname, entry := range cm.chans {
- if _, ok := cm.purgedChannels[cfname]; !ok {
- result = append(result, entry.channel)
- }
- }
- return
- }
-
- // Purge marks a channel as purged.
- func (cm *ChannelManager) Purge(chname string, record ChannelPurgeRecord) (err error) {
- chname, err = CasefoldChannel(chname)
- if err != nil {
- return errInvalidChannelName
- }
-
- record.NameCasefolded = chname
- record.UUID = utils.GenerateUUIDv4()
-
- channel, err := func() (channel *Channel, err error) {
- cm.Lock()
- defer cm.Unlock()
-
- if _, ok := cm.purgedChannels[chname]; ok {
- return nil, errChannelPurgedAlready
- }
-
- entry := cm.chans[chname]
- // atomically prevent anyone from rejoining
- cm.purgedChannels[chname] = record
- if entry != nil {
- channel = entry.channel
- }
- return
- }()
-
- if err != nil {
- return err
- }
-
- if channel != nil {
- // actually kick everyone off the channel
- channel.Purge("")
- }
-
- var purgeBytes []byte
- if purgeBytes, err = record.Serialize(); err != nil {
- cm.server.logger.Error("internal", "couldn't serialize purge record", channel.Name(), err.Error())
- }
- // TODO we need a better story about error handling for later
- if err = cm.server.dstore.Set(datastore.TableChannelPurges, record.UUID, purgeBytes, time.Time{}); err != nil {
- cm.server.logger.Error("datastore", "couldn't store purge record", chname, err.Error())
- }
-
- return
- }
-
- // IsPurged queries whether a channel is purged.
- func (cm *ChannelManager) IsPurged(chname string) (result bool) {
- chname, err := CasefoldChannel(chname)
- if err != nil {
- return false
- }
-
- cm.RLock()
- _, result = cm.purgedChannels[chname]
- cm.RUnlock()
- return
- }
-
- // Unpurge deletes a channel's purged status.
- func (cm *ChannelManager) Unpurge(chname string) (err error) {
- chname, err = CasefoldChannel(chname)
- if err != nil {
- return errNoSuchChannel
- }
-
- cm.Lock()
- record, found := cm.purgedChannels[chname]
- delete(cm.purgedChannels, chname)
- cm.Unlock()
-
- if !found {
- return errNoSuchChannel
- }
- if err := cm.server.dstore.Delete(datastore.TableChannelPurges, record.UUID); err != nil {
- cm.server.logger.Error("datastore", "couldn't delete purge record", chname, err.Error())
- }
- return nil
- }
-
- func (cm *ChannelManager) ListPurged() (result []string) {
- cm.RLock()
- result = make([]string, 0, len(cm.purgedChannels))
- for c := range cm.purgedChannels {
- result = append(result, c)
- }
- cm.RUnlock()
- sort.Strings(result)
- return
- }
-
- func (cm *ChannelManager) UnfoldName(cfname string) (result string) {
- cm.RLock()
- entry := cm.chans[cfname]
- cm.RUnlock()
- if entry != nil {
- return entry.channel.Name()
- }
- return cfname
- }
-
- func (cm *ChannelManager) LoadPurgeRecord(cfchname string) (record ChannelPurgeRecord, err error) {
- cm.RLock()
- defer cm.RUnlock()
-
- if record, ok := cm.purgedChannels[cfchname]; ok {
- return record, nil
- } else {
- return record, errNoSuchChannel
- }
- }
-
- func (cm *ChannelManager) ChannelsForAccount(account string) (channels []string) {
- cm.RLock()
- defer cm.RUnlock()
-
- for cfname, entry := range cm.chans {
- if entry.channel.Founder() == account {
- channels = append(channels, cfname)
- }
- }
-
- return
- }
-
- // AllChannels returns the uncasefolded names of all registered channels.
- func (cm *ChannelManager) AllRegisteredChannels() (result []string) {
- cm.RLock()
- defer cm.RUnlock()
-
- for cfname, entry := range cm.chans {
- if entry.channel.Founder() != "" {
- result = append(result, cfname)
- }
- }
-
- return
- }
|