Mirror of github.com/gissleh/irc
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

331 lines
7.3 KiB

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)
})
}