package list import ( "sort" "strings" "sync" "github.com/gissleh/irc/isupport" ) // The List of users in a channel. It has all operations one would perform on // users, like adding/removing modes and changing nicks. type List struct { mutex sync.RWMutex isupport *isupport.ISupport users []*User index map[string]*User autosort bool } // New creates a new list with the ISupport. The list can be reused between connections since the // ISupport is simply cleared and repopulated, but it should be cleared. func New(isupport *isupport.ISupport) *List { return &List{ isupport: isupport, users: make([]*User, 0, 64), index: make(map[string]*User, 64), autosort: true, } } // InsertFromNamesToken inserts using a NAMES token to get the nick, user, host and prefixes. // The format is `"@+Nick@user!hostmask.example.com"` func (list *List) InsertFromNamesToken(namestoken string) (ok bool) { user := User{} // Parse prefixes and modes. @ and ! (It's IRCHighWay if you were wondering) are both // mode prefixes and that just makes a mess if leave them for last. It also supports // `multi-prefix` for i, ch := range namestoken { mode := list.isupport.Mode(ch) if mode == 0 { if i != 0 { namestoken = namestoken[i:] } break } user.Prefixes += string(ch) user.Modes += string(mode) } // Get the nick split := strings.Split(namestoken, "!") user.Nick = split[0] // Support `userhost-in-names` if len(split) == 2 { userhost := strings.Split(split[1], "@") if len(userhost) == 2 { user.User = userhost[0] user.Host = userhost[1] } } ok = list.Insert(user) if !ok { // Patch the user's modes and prefixes since this is up to date information. list.mutex.Lock() for _, existing := range list.users { if existing.Nick == user.Nick { existing.Modes = user.Modes existing.Prefixes = user.Prefixes existing.updatePrefixedNick() break } } list.mutex.Unlock() } return ok } // Insert a user. Modes and prefixes will be cleaned up before insertion. func (list *List) Insert(user User) (ok bool) { if len(user.Modes) > 0 { // IRCv3 promises they'll be ordered by rank in WHO and NAMES replies, // but one can never be too sure with IRC. user.Modes = list.isupport.SortModes(user.Modes) if len(user.Prefixes) < len(user.Modes) { user.Prefixes = list.isupport.Prefixes(user.Modes) } else { user.Prefixes = list.isupport.SortPrefixes(user.Prefixes) } user.updatePrefixedNick() } else { user.Prefixes = "" user.updatePrefixedNick() } list.mutex.Lock() defer list.mutex.Unlock() if list.index[strings.ToLower(user.Nick)] != nil { return false } list.users = append(list.users, &user) list.index[strings.ToLower(user.Nick)] = &user if list.autosort { list.sort() } return true } // AddMode adds a mode to a user. Redundant modes will be ignored. It returns true if // the user can be found, even if the mode was redundant. func (list *List) AddMode(nick string, mode rune) (ok bool) { if !list.isupport.IsPermissionMode(mode) { return false } list.mutex.RLock() defer list.mutex.RUnlock() user := list.index[strings.ToLower(nick)] if user == nil { return false } if strings.ContainsRune(user.Modes, mode) { return true } prevHighest := user.HighestMode() user.Modes = list.isupport.SortModes(user.Modes + string(mode)) user.Prefixes = list.isupport.Prefixes(user.Modes) user.updatePrefixedNick() // Only sort if the new mode changed the highest mode. if list.autosort && prevHighest != user.HighestMode() { list.sort() } return true } // RemoveMode adds a mode to a user. It returns true if // the user can be found, even if the mode was not there. func (list *List) RemoveMode(nick string, mode rune) (ok bool) { if !list.isupport.IsPermissionMode(mode) { return false } list.mutex.RLock() defer list.mutex.RUnlock() user := list.index[strings.ToLower(nick)] if user == nil { return false } if !strings.ContainsRune(user.Modes, mode) { return true } prevHighest := user.HighestMode() user.Modes = strings.Replace(user.Modes, string(mode), "", 1) user.Prefixes = strings.Replace(user.Prefixes, string(list.isupport.Prefix(mode)), "", 1) user.updatePrefixedNick() // Only sort if the new mode changed the highest mode. if list.autosort && prevHighest != user.HighestMode() { list.sort() } return true } // Rename renames a user. It will return true if user by `from` exists, or if user by `to` does not exist. func (list *List) Rename(from, to string) (ok bool) { fromKey := strings.ToLower(from) toKey := strings.ToLower(to) list.mutex.Lock() defer list.mutex.Unlock() // Sanitiy check user := list.index[fromKey] if user == nil { return false } if from == to { return true } existing := list.index[toKey] if existing != nil { return false } user.Nick = to user.updatePrefixedNick() delete(list.index, fromKey) list.index[toKey] = user if list.autosort { list.sort() } return true } // Remove a user from the userlist. func (list *List) Remove(nick string) (ok bool) { list.mutex.Lock() defer list.mutex.Unlock() user := list.index[strings.ToLower(nick)] if user == nil { return false } for i := range list.users { if list.users[i] == user { list.users = append(list.users[:i], list.users[i+1:]...) break } } delete(list.index, strings.ToLower(nick)) return true } // User gets a copy of the user by nick, or an empty user if there is none. func (list *List) User(nick string) (u User, ok bool) { list.mutex.RLock() defer list.mutex.RUnlock() user := list.index[strings.ToLower(nick)] if user == nil { return User{}, false } return *user, true } // Users gets a copy of the users in the list's current state. func (list *List) Users() []User { result := make([]User, len(list.users)) list.mutex.RLock() for i := range list.users { result[i] = *list.users[i] } list.mutex.RUnlock() return result } // Patch allows editing a limited subset of the user's properties. func (list *List) Patch(nick string, patch UserPatch) (ok bool) { list.mutex.Lock() defer list.mutex.Unlock() for _, user := range list.users { if strings.EqualFold(nick, user.Nick) { if patch.Account != "" || patch.ClearAccount { user.Account = patch.Account } if patch.Away != "" || patch.ClearAway { user.Away = patch.Away } if patch.User != "" { user.User = patch.User } if patch.Host != "" { user.Host = patch.Host } return true } } return false } // SetAutoSort enables or disables automatic sorting, which by default is enabled. // Dislabing it makes sense when doing a massive operation. Enabling it will trigger // a sort. func (list *List) SetAutoSort(autosort bool) { list.mutex.Lock() list.autosort = autosort list.sort() list.mutex.Unlock() } // Clear removes all users in a list. func (list *List) Clear() { list.mutex.Lock() list.users = list.users[:0] for key := range list.index { delete(list.index, key) } list.mutex.Unlock() } // Immutable gets an immutable version of the list. func (list *List) Immutable() Immutable { return Immutable{list: list} } func (list *List) sort() { sort.Slice(list.users, func(i, j int) bool { a := list.users[i] b := list.users[j] aMode := a.HighestMode() bMode := b.HighestMode() if aMode != bMode { return list.isupport.IsModeHigher(aMode, bMode) } return strings.ToLower(a.Nick) < strings.ToLower(b.Nick) }) }