1// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net> 2// released under the MIT license 3 4package irc 5 6import ( 7 "encoding/json" 8 "fmt" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/tidwall/buntdb" 14 15 "github.com/ergochat/ergo/irc/modes" 16 "github.com/ergochat/ergo/irc/utils" 17) 18 19// this is exclusively the *persistence* layer for channel registration; 20// channel creation/tracking/destruction is in channelmanager.go 21 22const ( 23 keyChannelExists = "channel.exists %s" 24 keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped 25 keyChannelRegTime = "channel.registered.time %s" 26 keyChannelFounder = "channel.founder %s" 27 keyChannelTopic = "channel.topic %s" 28 keyChannelTopicSetBy = "channel.topic.setby %s" 29 keyChannelTopicSetTime = "channel.topic.settime %s" 30 keyChannelBanlist = "channel.banlist %s" 31 keyChannelExceptlist = "channel.exceptlist %s" 32 keyChannelInvitelist = "channel.invitelist %s" 33 keyChannelPassword = "channel.key %s" 34 keyChannelModes = "channel.modes %s" 35 keyChannelAccountToUMode = "channel.accounttoumode %s" 36 keyChannelUserLimit = "channel.userlimit %s" 37 keyChannelSettings = "channel.settings %s" 38 keyChannelForward = "channel.forward %s" 39 40 keyChannelPurged = "channel.purged %s" 41) 42 43var ( 44 channelKeyStrings = []string{ 45 keyChannelExists, 46 keyChannelName, 47 keyChannelRegTime, 48 keyChannelFounder, 49 keyChannelTopic, 50 keyChannelTopicSetBy, 51 keyChannelTopicSetTime, 52 keyChannelBanlist, 53 keyChannelExceptlist, 54 keyChannelInvitelist, 55 keyChannelPassword, 56 keyChannelModes, 57 keyChannelAccountToUMode, 58 keyChannelUserLimit, 59 keyChannelSettings, 60 keyChannelForward, 61 } 62) 63 64// these are bit flags indicating what part of the channel status is "dirty" 65// and needs to be read from memory and written to the db 66const ( 67 IncludeInitial uint = 1 << iota 68 IncludeTopic 69 IncludeModes 70 IncludeLists 71 IncludeSettings 72) 73 74// this is an OR of all possible flags 75const ( 76 IncludeAllAttrs = ^uint(0) 77) 78 79// RegisteredChannel holds details about a given registered channel. 80type RegisteredChannel struct { 81 // Name of the channel. 82 Name string 83 // Casefolded name of the channel. 84 NameCasefolded string 85 // RegisteredAt represents the time that the channel was registered. 86 RegisteredAt time.Time 87 // Founder indicates the founder of the channel. 88 Founder string 89 // Topic represents the channel topic. 90 Topic string 91 // TopicSetBy represents the host that set the topic. 92 TopicSetBy string 93 // TopicSetTime represents the time the topic was set. 94 TopicSetTime time.Time 95 // Modes represents the channel modes 96 Modes []modes.Mode 97 // Key represents the channel key / password 98 Key string 99 // Forward is the forwarding/overflow (+f) channel 100 Forward string 101 // UserLimit is the user limit (0 for no limit) 102 UserLimit int 103 // AccountToUMode maps user accounts to their persistent channel modes (e.g., +q, +h) 104 AccountToUMode map[string]modes.Mode 105 // Bans represents the bans set on the channel. 106 Bans map[string]MaskInfo 107 // Excepts represents the exceptions set on the channel. 108 Excepts map[string]MaskInfo 109 // Invites represents the invite exceptions set on the channel. 110 Invites map[string]MaskInfo 111 // Settings are the chanserv-modifiable settings 112 Settings ChannelSettings 113} 114 115type ChannelPurgeRecord struct { 116 Oper string 117 PurgedAt time.Time 118 Reason string 119} 120 121// ChannelRegistry manages registered channels. 122type ChannelRegistry struct { 123 server *Server 124} 125 126// NewChannelRegistry returns a new ChannelRegistry. 127func (reg *ChannelRegistry) Initialize(server *Server) { 128 reg.server = server 129} 130 131// AllChannels returns the uncasefolded names of all registered channels. 132func (reg *ChannelRegistry) AllChannels() (result []string) { 133 prefix := fmt.Sprintf(keyChannelName, "") 134 reg.server.store.View(func(tx *buntdb.Tx) error { 135 return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 136 if !strings.HasPrefix(key, prefix) { 137 return false 138 } 139 result = append(result, value) 140 return true 141 }) 142 }) 143 144 return 145} 146 147// PurgedChannels returns the set of all casefolded channel names that have been purged 148func (reg *ChannelRegistry) PurgedChannels() (result utils.StringSet) { 149 result = make(utils.StringSet) 150 151 prefix := fmt.Sprintf(keyChannelPurged, "") 152 reg.server.store.View(func(tx *buntdb.Tx) error { 153 return tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool { 154 if !strings.HasPrefix(key, prefix) { 155 return false 156 } 157 channel := strings.TrimPrefix(key, prefix) 158 result.Add(channel) 159 return true 160 }) 161 }) 162 return 163} 164 165// StoreChannel obtains a consistent view of a channel, then persists it to the store. 166func (reg *ChannelRegistry) StoreChannel(info RegisteredChannel, includeFlags uint) (err error) { 167 if !reg.server.ChannelRegistrationEnabled() { 168 return 169 } 170 171 if info.Founder == "" { 172 // sanity check, don't try to store an unregistered channel 173 return 174 } 175 176 reg.server.store.Update(func(tx *buntdb.Tx) error { 177 reg.saveChannel(tx, info, includeFlags) 178 return nil 179 }) 180 181 return nil 182} 183 184// LoadChannel loads a channel from the store. 185func (reg *ChannelRegistry) LoadChannel(nameCasefolded string) (info RegisteredChannel, err error) { 186 if !reg.server.ChannelRegistrationEnabled() { 187 err = errFeatureDisabled 188 return 189 } 190 191 channelKey := nameCasefolded 192 // nice to have: do all JSON (de)serialization outside of the buntdb transaction 193 err = reg.server.store.View(func(tx *buntdb.Tx) error { 194 _, dberr := tx.Get(fmt.Sprintf(keyChannelExists, channelKey)) 195 if dberr == buntdb.ErrNotFound { 196 // chan does not already exist, return 197 return errNoSuchChannel 198 } 199 200 // channel exists, load it 201 name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey)) 202 regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey)) 203 regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) 204 founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey)) 205 topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey)) 206 topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey)) 207 var topicSetTime time.Time 208 topicSetTimeStr, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey)) 209 if topicSetTimeInt, topicSetTimeErr := strconv.ParseInt(topicSetTimeStr, 10, 64); topicSetTimeErr == nil { 210 topicSetTime = time.Unix(0, topicSetTimeInt).UTC() 211 } 212 password, _ := tx.Get(fmt.Sprintf(keyChannelPassword, channelKey)) 213 modeString, _ := tx.Get(fmt.Sprintf(keyChannelModes, channelKey)) 214 userLimitString, _ := tx.Get(fmt.Sprintf(keyChannelUserLimit, channelKey)) 215 forward, _ := tx.Get(fmt.Sprintf(keyChannelForward, channelKey)) 216 banlistString, _ := tx.Get(fmt.Sprintf(keyChannelBanlist, channelKey)) 217 exceptlistString, _ := tx.Get(fmt.Sprintf(keyChannelExceptlist, channelKey)) 218 invitelistString, _ := tx.Get(fmt.Sprintf(keyChannelInvitelist, channelKey)) 219 accountToUModeString, _ := tx.Get(fmt.Sprintf(keyChannelAccountToUMode, channelKey)) 220 settingsString, _ := tx.Get(fmt.Sprintf(keyChannelSettings, channelKey)) 221 222 modeSlice := make([]modes.Mode, len(modeString)) 223 for i, mode := range modeString { 224 modeSlice[i] = modes.Mode(mode) 225 } 226 227 userLimit, _ := strconv.Atoi(userLimitString) 228 229 var banlist map[string]MaskInfo 230 _ = json.Unmarshal([]byte(banlistString), &banlist) 231 var exceptlist map[string]MaskInfo 232 _ = json.Unmarshal([]byte(exceptlistString), &exceptlist) 233 var invitelist map[string]MaskInfo 234 _ = json.Unmarshal([]byte(invitelistString), &invitelist) 235 accountToUMode := make(map[string]modes.Mode) 236 _ = json.Unmarshal([]byte(accountToUModeString), &accountToUMode) 237 238 var settings ChannelSettings 239 _ = json.Unmarshal([]byte(settingsString), &settings) 240 241 info = RegisteredChannel{ 242 Name: name, 243 NameCasefolded: nameCasefolded, 244 RegisteredAt: time.Unix(0, regTimeInt).UTC(), 245 Founder: founder, 246 Topic: topic, 247 TopicSetBy: topicSetBy, 248 TopicSetTime: topicSetTime, 249 Key: password, 250 Modes: modeSlice, 251 Bans: banlist, 252 Excepts: exceptlist, 253 Invites: invitelist, 254 AccountToUMode: accountToUMode, 255 UserLimit: int(userLimit), 256 Settings: settings, 257 Forward: forward, 258 } 259 return nil 260 }) 261 262 return 263} 264 265// Delete deletes a channel corresponding to `info`. If no such channel 266// is present in the database, no error is returned. 267func (reg *ChannelRegistry) Delete(info RegisteredChannel) (err error) { 268 if !reg.server.ChannelRegistrationEnabled() { 269 return 270 } 271 272 reg.server.store.Update(func(tx *buntdb.Tx) error { 273 reg.deleteChannel(tx, info.NameCasefolded, info) 274 return nil 275 }) 276 return nil 277} 278 279// delete a channel, unless it was overwritten by another registration of the same channel 280func (reg *ChannelRegistry) deleteChannel(tx *buntdb.Tx, key string, info RegisteredChannel) { 281 _, err := tx.Get(fmt.Sprintf(keyChannelExists, key)) 282 if err == nil { 283 regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, key)) 284 regTimeInt, _ := strconv.ParseInt(regTime, 10, 64) 285 registeredAt := time.Unix(0, regTimeInt).UTC() 286 founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, key)) 287 288 // to see if we're deleting the right channel, confirm the founder and the registration time 289 if founder == info.Founder && registeredAt.Equal(info.RegisteredAt) { 290 for _, keyFmt := range channelKeyStrings { 291 tx.Delete(fmt.Sprintf(keyFmt, key)) 292 } 293 294 // remove this channel from the client's list of registered channels 295 channelsKey := fmt.Sprintf(keyAccountChannels, info.Founder) 296 channelsStr, err := tx.Get(channelsKey) 297 if err == buntdb.ErrNotFound { 298 return 299 } 300 registeredChannels := unmarshalRegisteredChannels(channelsStr) 301 var nowRegisteredChannels []string 302 for _, channel := range registeredChannels { 303 if channel != key { 304 nowRegisteredChannels = append(nowRegisteredChannels, channel) 305 } 306 } 307 tx.Set(channelsKey, strings.Join(nowRegisteredChannels, ","), nil) 308 } 309 } 310} 311 312func (reg *ChannelRegistry) updateAccountToChannelMapping(tx *buntdb.Tx, channelInfo RegisteredChannel) { 313 channelKey := channelInfo.NameCasefolded 314 chanFounderKey := fmt.Sprintf(keyChannelFounder, channelKey) 315 founder, existsErr := tx.Get(chanFounderKey) 316 if existsErr == buntdb.ErrNotFound || founder != channelInfo.Founder { 317 // add to new founder's list 318 accountChannelsKey := fmt.Sprintf(keyAccountChannels, channelInfo.Founder) 319 alreadyChannels, _ := tx.Get(accountChannelsKey) 320 newChannels := channelKey // this is the casefolded channel name 321 if alreadyChannels != "" { 322 newChannels = fmt.Sprintf("%s,%s", alreadyChannels, newChannels) 323 } 324 tx.Set(accountChannelsKey, newChannels, nil) 325 } 326 if existsErr == nil && founder != channelInfo.Founder { 327 // remove from old founder's list 328 accountChannelsKey := fmt.Sprintf(keyAccountChannels, founder) 329 alreadyChannelsRaw, _ := tx.Get(accountChannelsKey) 330 var newChannels []string 331 if alreadyChannelsRaw != "" { 332 for _, chname := range strings.Split(alreadyChannelsRaw, ",") { 333 if chname != channelInfo.NameCasefolded { 334 newChannels = append(newChannels, chname) 335 } 336 } 337 } 338 tx.Set(accountChannelsKey, strings.Join(newChannels, ","), nil) 339 } 340} 341 342// saveChannel saves a channel to the store. 343func (reg *ChannelRegistry) saveChannel(tx *buntdb.Tx, channelInfo RegisteredChannel, includeFlags uint) { 344 channelKey := channelInfo.NameCasefolded 345 // maintain the mapping of account -> registered channels 346 reg.updateAccountToChannelMapping(tx, channelInfo) 347 348 if includeFlags&IncludeInitial != 0 { 349 tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil) 350 tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil) 351 tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.UnixNano(), 10), nil) 352 tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil) 353 } 354 355 if includeFlags&IncludeTopic != 0 { 356 tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil) 357 var topicSetTimeStr string 358 if !channelInfo.TopicSetTime.IsZero() { 359 topicSetTimeStr = strconv.FormatInt(channelInfo.TopicSetTime.UnixNano(), 10) 360 } 361 tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), topicSetTimeStr, nil) 362 tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil) 363 } 364 365 if includeFlags&IncludeModes != 0 { 366 tx.Set(fmt.Sprintf(keyChannelPassword, channelKey), channelInfo.Key, nil) 367 modeString := modes.Modes(channelInfo.Modes).String() 368 tx.Set(fmt.Sprintf(keyChannelModes, channelKey), modeString, nil) 369 tx.Set(fmt.Sprintf(keyChannelUserLimit, channelKey), strconv.Itoa(channelInfo.UserLimit), nil) 370 tx.Set(fmt.Sprintf(keyChannelForward, channelKey), channelInfo.Forward, nil) 371 } 372 373 if includeFlags&IncludeLists != 0 { 374 banlistString, _ := json.Marshal(channelInfo.Bans) 375 tx.Set(fmt.Sprintf(keyChannelBanlist, channelKey), string(banlistString), nil) 376 exceptlistString, _ := json.Marshal(channelInfo.Excepts) 377 tx.Set(fmt.Sprintf(keyChannelExceptlist, channelKey), string(exceptlistString), nil) 378 invitelistString, _ := json.Marshal(channelInfo.Invites) 379 tx.Set(fmt.Sprintf(keyChannelInvitelist, channelKey), string(invitelistString), nil) 380 accountToUModeString, _ := json.Marshal(channelInfo.AccountToUMode) 381 tx.Set(fmt.Sprintf(keyChannelAccountToUMode, channelKey), string(accountToUModeString), nil) 382 } 383 384 if includeFlags&IncludeSettings != 0 { 385 settingsString, _ := json.Marshal(channelInfo.Settings) 386 tx.Set(fmt.Sprintf(keyChannelSettings, channelKey), string(settingsString), nil) 387 } 388} 389 390// PurgeChannel records a channel purge. 391func (reg *ChannelRegistry) PurgeChannel(chname string, record ChannelPurgeRecord) (err error) { 392 serialized, err := json.Marshal(record) 393 if err != nil { 394 return err 395 } 396 serializedStr := string(serialized) 397 key := fmt.Sprintf(keyChannelPurged, chname) 398 399 return reg.server.store.Update(func(tx *buntdb.Tx) error { 400 tx.Set(key, serializedStr, nil) 401 return nil 402 }) 403} 404 405// LoadPurgeRecord retrieves information about whether and how a channel was purged. 406func (reg *ChannelRegistry) LoadPurgeRecord(chname string) (record ChannelPurgeRecord, err error) { 407 var rawRecord string 408 key := fmt.Sprintf(keyChannelPurged, chname) 409 reg.server.store.View(func(tx *buntdb.Tx) error { 410 rawRecord, _ = tx.Get(key) 411 return nil 412 }) 413 if rawRecord == "" { 414 err = errNoSuchChannel 415 return 416 } 417 err = json.Unmarshal([]byte(rawRecord), &record) 418 if err != nil { 419 reg.server.logger.Error("internal", "corrupt purge record", chname, err.Error()) 420 err = errNoSuchChannel 421 return 422 } 423 return 424} 425 426// UnpurgeChannel deletes the record of a channel purge. 427func (reg *ChannelRegistry) UnpurgeChannel(chname string) (err error) { 428 key := fmt.Sprintf(keyChannelPurged, chname) 429 return reg.server.store.Update(func(tx *buntdb.Tx) error { 430 tx.Delete(key) 431 return nil 432 }) 433} 434