roster.go
author Chris Jones <christian.jones@sri.com>
Fri, 28 Dec 2012 17:07:20 -0700
changeset 114 a058e33c1666
parent 113 bee6cc131798
child 116 5c6d6d51d3ba
permissions -rw-r--r--
Updated for the latest revision of the encoding/xml fixes: The context object owned by Encoder and Decoder isn't directly accessible. Also improved the output from the two assert functions to show the info of the caller rather than the assert function itself.

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package xmpp

import (
	"encoding/xml"
	"fmt"
)

// This file contains support for roster management, RFC 3921, Section 7.

var rosterExt Extension = Extension{StanzaHandlers: map[string]func(*xml.Name) interface{}{NsRoster: newRosterQuery}, Start: startRosterFilter}

// Roster query/result
type RosterQuery struct {
	XMLName xml.Name `xml:"jabber:iq:roster query"`
	Item    []RosterItem `xml:"item"`
}

// See RFC 3921, Section 7.1.
type RosterItem struct {
	XMLName      xml.Name `xml:"jabber:iq:roster item"`
	Jid          string   `xml:"jid,attr"`
	Subscription string   `xml:"subscription,attr"`
	Name         string   `xml:"name,attr"`
	Group        []string
}

type rosterClient struct {
	rosterChan   <-chan []RosterItem
	rosterUpdate chan<- RosterItem
}

var (
	rosterClients = make(map[string]rosterClient)
)

// Implicitly becomes part of NewClient's extStanza arg.
func newRosterQuery(name *xml.Name) interface{} {
	return &RosterQuery{}
}

// Synchronously fetch this entity's roster from the server and cache
// that information. This is called once from a fairly deep call stack
// as part of XMPP negotiation.
func fetchRoster(client *Client) error {
	rosterUpdate := rosterClients[client.Uid].rosterUpdate

	iq := &Iq{Header: Header{From: client.Jid.String(), Type: "get",
		Id: <-Id, Nested: []interface{}{RosterQuery{}}}}
	ch := make(chan error)
	f := func(v Stanza) bool {
		defer close(ch)
		iq, ok := v.(*Iq)
		if !ok {
			ch <- fmt.Errorf("response to iq wasn't iq: %s", v)
			return false
		}
		if iq.Type == "error" {
			ch <- iq.Error
			return false
		}
		var rq *RosterQuery
		for _, ele := range iq.Nested {
			if q, ok := ele.(*RosterQuery); ok {
				rq = q
				break
			}
		}
		if rq == nil {
			ch <- fmt.Errorf(
				"Roster query result not query: %v", v)
			return false
		}
		for _, item := range rq.Item {
			rosterUpdate <- item
		}
		ch <- nil
		return false
	}
	client.HandleStanza(iq.Id, f)
	client.Out <- iq
	// Wait for f to complete.
	return <-ch
}

// The roster filter updates the Client's representation of the
// roster, but it lets the relevant stanzas through. This also starts
// the roster feeder, which is the goroutine that provides data on
// client.Roster.
func startRosterFilter(client *Client) {
	out := make(chan Stanza)
	in := client.AddFilter(out)
	go func(in <-chan Stanza, out chan<- Stanza) {
		defer close(out)
		for st := range in {
			maybeUpdateRoster(client, st)
			out <- st
		}
	}(in, out)

	rosterCh := make(chan []RosterItem)
	rosterUpdate := make(chan RosterItem)
	rosterClients[client.Uid] = rosterClient{rosterChan: rosterCh,
		rosterUpdate: rosterUpdate}
	go feedRoster(rosterCh, rosterUpdate)
}

func maybeUpdateRoster(client *Client, st interface{}) {
	iq, ok := st.(*Iq)
	if !ok {
		return
	}

	rosterUpdate := rosterClients[client.Uid].rosterUpdate

	var rq *RosterQuery
	for _, ele := range iq.Nested {
		if q, ok := ele.(*RosterQuery); ok {
			rq = q
			break
		}
	}
	if iq.Type == "set" && rq != nil {
		for _, item := range rq.Item {
			rosterUpdate <- item
		}
		// Send a reply.
		reply := &Iq{Header: Header{To: iq.From, Id: iq.Id,
			Type: "result"}}
		client.Out <- reply
	}
}

func feedRoster(rosterCh chan<- []RosterItem, rosterUpdate <-chan RosterItem) {
	roster := make(map[string]RosterItem)
	snapshot := []RosterItem{}
	for {
		select {
		case newIt := <-rosterUpdate:
			if newIt.Subscription == "remove" {
				delete(roster, newIt.Jid)
			} else {
				roster[newIt.Jid] = newIt
			}
		case rosterCh <- snapshot:
		}
		snapshot = make([]RosterItem, 0, len(roster))
		for _, v := range roster {
			snapshot = append(snapshot, v)
		}
	}
}

// Retrieve a snapshot of the roster for the given Client.
func Roster(client *Client) []RosterItem {
	rosterChan := rosterClients[client.Uid].rosterChan
	return <-rosterChan
}