re-implement Client.Whowas(); implement Client.Who()

This commit is contained in:
Liam Stanley 2016-12-14 01:49:43 -05:00
parent 6f89a46cc5
commit fea3c94367
2 changed files with 190 additions and 1 deletions

188
client.go

@ -13,6 +13,7 @@ import (
"log"
"net"
"strings"
"sync"
"time"
)
@ -611,3 +612,190 @@ func (c *Client) SendRawf(format string, a ...interface{}) error {
func (c *Client) Topic(channel, message string) error {
return c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message})
}
// ErrCallbackTimedout is used when we need to wait for temporary callbacks.
type ErrCallbackTimedout struct {
// ID is the identified of the callback in the callback stack.
ID string
// Timeout is the time that past before the callback timed out.
Timeout time.Duration
}
func (e *ErrCallbackTimedout) Error() string {
return "callback [" + e.ID + "] timed out while waiting for response from the server: " + e.Timeout.String()
}
// Who runs a WHO query to give details about an active user. Can only be
// used if the calling callback was added as a background callback. Will
// lock otherwise.
func (c *Client) Who(nick string) (*User, error) {
if !IsValidNick(nick) {
return nil, &ErrInvalidTarget{Target: nick}
}
if !c.IsConnected() {
return nil, ErrNotConnected
}
var user *User
whoDone := make(chan struct{})
whoHandler := func(c *Client, e Event) {
var eIdent, eHost, eNick string
// Assume WHOX related.
if e.Command == RPL_WHOSPCRPL {
if len(e.Params) != 6 {
// Assume there was some form of error or invalid WHOX response.
return
}
if e.Params[1] != "2" {
// We should always be sending 2, and we should receive 2. If
// this is anything but, then we didn't send the request and we
// can ignore it.
return
}
eIdent, eHost, eNick = e.Params[3], e.Params[4], e.Params[5]
} else {
eIdent, eHost, eNick = e.Params[2], e.Params[3], e.Params[5]
}
if strings.ToLower(eNick) != strings.ToLower(nick) {
// Not the same user we're querying for.
return
}
user = &User{
Nick: eNick,
Ident: eIdent,
Host: eHost,
Name: e.Trailing,
}
whoDone <- struct{}{}
}
// Add a callback for both the standard WHO as well as a WHOX response.
// Use register rather than sregister, as we're already in a lock.
whoCb := c.Callbacks.AddBg(RPL_WHOREPLY, whoHandler)
whoXCb := c.Callbacks.AddBg(RPL_WHOSPCRPL, whoHandler)
// Initiate the event so we can catch it.
err := c.Send(&Event{Command: WHO, Params: []string{nick, "%tcuhnr,2"}})
if err != nil {
return nil, err
}
// Wait for everything to finish. Give the server 2 seconds to respond.
select {
case <-whoDone:
close(whoDone)
case <-time.After(time.Second * 2):
// Remove callbacks and return. Took too long.
c.Callbacks.Remove(whoCb)
c.Callbacks.Remove(whoXCb)
return nil, &ErrCallbackTimedout{
ID: whoCb + " + " + whoXCb,
Timeout: time.Second * 2,
}
}
c.Callbacks.Remove(whoCb)
c.Callbacks.Remove(whoXCb)
return user, nil
}
// Whowas sends and waits for a response to a WHOWAS query to the server.
// Returns the list of users form the WHOWAS query. Can only be used if the
// calling callback was added as a background callback. Will lock otherwise.
func (c *Client) Whowas(nick string) ([]*User, error) {
if !IsValidNick(nick) {
return nil, &ErrInvalidTarget{Target: nick}
}
if !c.IsConnected() {
return nil, ErrNotConnected
}
var mu sync.Mutex
var events []*Event
whoDone := make(chan struct{})
// One callback needs to be added to collect the WHOWAS lines.
// <nick> <user> <host> * :<real_name>
whoCb := c.Callbacks.AddBg(RPL_WHOWASUSER, func(c *Client, e Event) {
if len(e.Params) != 5 {
return
}
// First check and make sure that this WHOWAS is for us.
if e.Params[1] != nick {
return
}
mu.Lock()
events = append(events, &e)
mu.Unlock()
})
// One more callback needs to be added to let us know when WHOWAS has
// finished.
// <nick> :<info>
whoDoneCb := c.Callbacks.AddBg(RPL_ENDOFWHOWAS, func(c *Client, e Event) {
if len(e.Params) != 2 {
return
}
// First check and make sure that this WHOWAS is for us.
if e.Params[1] != nick {
return
}
mu.Lock()
whoDone <- struct{}{}
mu.Unlock()
})
// Send the WHOWAS query.
err := c.Send(&Event{Command: WHOWAS, Params: []string{nick, "10"}})
if err != nil {
return nil, err
}
// Wait for everything to finish. Give the server 2 seconds to respond.
select {
case <-whoDone:
close(whoDone)
case <-time.After(time.Second * 2):
// Remove callbacks and return. Took too long.
c.Callbacks.Remove(whoCb)
c.Callbacks.Remove(whoDoneCb)
return nil, &ErrCallbackTimedout{
ID: whoCb + " + " + whoDoneCb,
Timeout: time.Second * 2,
}
}
// Remove the temporary callbacks to ensure that nothing else is
// received.
c.Callbacks.Remove(whoCb)
c.Callbacks.Remove(whoDoneCb)
var users []*User
for i := 0; i < len(events); i++ {
users = append(users, &User{
Nick: events[i].Params[1],
Ident: events[i].Params[2],
Host: events[i].Params[3],
Name: events[i].Trailing,
})
}
return users, nil
}

@ -57,11 +57,12 @@ type User struct {
// be empty.
Name string
// FirstSeen represents the first time that the user was seen by the
// client for the given channel.
// client for the given channel. Only usable if from state, not in past.
FirstSeen time.Time
// LastActive represents the last time that we saw the user active,
// which could be during nickname change, message, channel join, etc.
// Only usable if from state, not in past.
LastActive time.Time
}