diff --git a/handlers.go b/handlers.go index ea8acc7..e0df547 100644 --- a/handlers.go +++ b/handlers.go @@ -27,6 +27,7 @@ func (c *Client) registerHandlers() { c.Callbacks.register(true, KICK, CallbackFunc(handleKICK)) c.Callbacks.register(true, QUIT, CallbackFunc(handleQUIT)) c.Callbacks.register(true, NICK, CallbackFunc(handleNICK)) + c.Callbacks.register(true, RPL_NAMREPLY, CallbackFunc(handleNAMES)) // WHO/WHOX responses. c.Callbacks.register(true, RPL_WHOREPLY, CallbackFunc(handleWHO)) @@ -310,3 +311,28 @@ func handleMOTD(c *Client, e Event) { c.state.mu.Unlock() } + +func handleNAMES(c *Client, e Event) { + if len(e.Params) < 1 || !IsValidChannel(e.Params[len(e.Params)-1]) { + return + } + + parts := strings.Split(e.Trailing, " ") + + c.state.mu.Lock() + for i := 0; i < len(parts); i++ { + modes, nick, ok := parseUserModes(parts[i]) + if !ok { + continue + } + + user := c.state.createUserIfNotExists(e.Params[len(e.Params)-1], nick) + if user == nil { + continue + } + + // Don't append modes, overwrite them. + user.Modes.setModes(modes, false) + } + c.state.mu.Unlock() +} diff --git a/state.go b/state.go index bf7f953..be22ce8 100644 --- a/state.go +++ b/state.go @@ -74,6 +74,12 @@ type User struct { // Only usable if from state, not in past. LastActive time.Time + // Modes are the user modes applied to this user that affect the given + // channel. This supports non-rfc style modes like Admin, Owner, and HalfOp. + // If you want to easily check if a user has permissions equal or greater + // than OP, use Modes.IsAdmin(). + Modes UserModes + // Extras are things added on by additional tracking methods, which may // or may not work on the IRC server in mention. Extras struct { @@ -317,3 +323,100 @@ func (s *state) getUsers(matchType, toMatch string) []*User { return users } + +// UserModes contains all channel-based user permissions. The minimum op, and +// voice should be supported on all networks. This also supports non-rfc +// Owner, Admin, and HalfOp, if the network has support for it. +type UserModes struct { + // Owner (non-rfc) indicates that the user has full permissions to the + // channel. More than one user can have owner permission. + Owner bool + // Admin (non-rfc) is commonly given to users that are trusted enough + // to manage channel permissions, as well as higher level service settings. + Admin bool + // Op is commonly given to trusted users who can manage a given channel + // by kicking, and banning users. + Op bool + // HalfOp (non-rfc) is commonly used to give users permissions like the + // ability to kick, without giving them greater abilities to ban all users. + HalfOp bool + // Voice indicates the user has voice permissions, commonly given to known + // users, wih very light trust, or to indicate a user is active. + Voice bool +} + +// IsAdmin indicates that the user has banning abilities, and are likely a +// very trustable user (e.g. op+). +func (m UserModes) IsAdmin() bool { + if m.Owner || m.Admin || m.Op { + return true + } + + return false +} + +// IsAdmin indicates that the user at least has modes set upon them, higher +// than a regular joining user. +func (m UserModes) IsTrusted() bool { + if m.IsAdmin() || m.HalfOp || m.Voice { + return true + } + + return false +} + +// reset resets the modes of a user. +func (m *UserModes) reset() { + m.Owner = false + m.Admin = false + m.Op = false + m.HalfOp = false + m.Voice = false +} + +// setMode translates raw mode characters into proper permissions. Only use +// this function when you have a session lock. +func (m *UserModes) setModes(modes string, append bool) { + if !append { + m.reset() + } + + for i := 0; i < len(modes); i++ { + switch string(modes[i]) { + case OwnerPrefix: + m.Owner = true + case AdminPrefix: + m.Admin = true + case OperatorPrefix: + m.Op = true + case HalfOperatorPrefix: + m.HalfOp = true + case VoicePrefix: + m.Voice = true + } + } +} + +// parseUserModes parses a raw mode line, like "@user" or "@+user". +func parseUserModes(raw string) (modes, nick string, success bool) { + for i := 0; i < len(raw); i++ { + char := string(raw[i]) + + if char == OwnerPrefix || char == AdminPrefix || char == HalfOperatorPrefix || + char == OperatorPrefix || char == VoicePrefix { + modes += char + continue + } + + // Assume we've gotten to the nickname part. + if !IsValidNick(raw[i:]) { + return modes, nick, false + } + + nick = raw[i:] + + return modes, nick, true + } + + return +}