Added roster retrieval to StartSession().
authorChris Jones <chris@cjones.org>
Fri, 30 Dec 2011 17:16:37 -0700
changeset 33 571713f49494
parent 32 4e68d8f89dc3
child 34 7b1f924c75e2
Added roster retrieval to StartSession().
examples/interact.go
stream.go
structs.go
xmpp.go
--- a/examples/interact.go	Thu Dec 29 11:25:26 2011 -0700
+++ b/examples/interact.go	Fri Dec 30 17:16:37 2011 -0700
@@ -30,10 +30,15 @@
 	}
 	defer c.Close()
 
-	err = c.StartSession(&xmpp.Presence{})
+	err = c.StartSession(true, &xmpp.Presence{})
 	if err != nil {
 		log.Fatalf("StartSession: %v", err)
 	}
+	roster := c.Roster()
+	fmt.Printf("%d roster entries:\n", len(roster))
+	for jid, entry := range(roster) {
+		fmt.Printf("%s: %v\n", jid, entry)
+	}
 
 	go func(ch <-chan xmpp.Stanza) {
 		for obj := range ch {
--- a/stream.go	Thu Dec 29 11:25:26 2011 -0700
+++ b/stream.go	Fri Dec 30 17:16:37 2011 -0700
@@ -551,7 +551,9 @@
 // Register a callback to handle the next XMPP stanza (iq, message, or
 // presence) with a given id. The provided function will not be called
 // more than once. If it returns false, the stanza will not be made
-// available on the normal Client.In channel.
+// available on the normal Client.In channel. The stanza handler
+// must not read from that channel, as deliveries on it cannot proceed
+// until the handler returns true or false.
 func (cl *Client) HandleStanza(id string, f func(Stanza) bool) {
 	h := &stanzaHandler{id: id, f: f}
 	cl.handlers <- h
--- a/structs.go	Thu Dec 29 11:25:26 2011 -0700
+++ b/structs.go	Fri Dec 30 17:16:37 2011 -0700
@@ -138,10 +138,28 @@
 	Lang string `xml:"attr"`
 	Error *Error
 	Any *Generic
+	Query *RosterQuery
 }
 var _ xml.Marshaler = &Iq{}
 var _ Stanza = &Iq{}
 
+// Roster query/result
+type RosterQuery struct {
+	// Should always be query in the NsRoster namespace
+	XMLName xml.Name
+	Item []RosterItem
+}
+
+// See RFC 3921, Section 7.1.
+type RosterItem struct {
+	// Should always be "item"
+	XMLName xml.Name
+	Jid string `xml:"attr"`
+	Subscription string `xml:"attr"`
+	Name string `xml:"attr"`
+	Group []string
+}
+
 // Describes an XMPP stanza error. See RFC 3920, Section 9.3.
 type Error struct {
 	// The error type attribute.
@@ -264,6 +282,8 @@
 		u.XMLName.Local)
 }
 
+// BUG(cjyar) This is fragile. We should find a way to use go's native
+// XML marshaling.
 func marshalXML(st Stanza) ([]byte, os.Error) {
 	buf := bytes.NewBuffer(nil)
 	buf.WriteString("<")
@@ -291,6 +311,9 @@
 	if st.XChild() != nil {
 		xml.Marshal(buf, st.XChild())
 	}
+	if iq, ok := st.(*Iq) ; ok && iq.Query != nil {
+		xml.Marshal(buf, iq.Query)
+	}
 	buf.WriteString("</")
 	buf.WriteString(st.XName())
 	buf.WriteString(">")
--- a/xmpp.go	Thu Dec 29 11:25:26 2011 -0700
+++ b/xmpp.go	Fri Dec 30 17:16:37 2011 -0700
@@ -24,6 +24,7 @@
 	// Version of RFC 3920 that we implement.
 	Version = "1.0"
 
+	// BUG(cjyar) These should be public.
 	// Various XML namespaces.
 	nsStreams = "urn:ietf:params:xml:ns:xmpp-streams"
 	nsStream = "http://etherx.jabber.org/streams"
@@ -31,6 +32,7 @@
 	nsSASL = "urn:ietf:params:xml:ns:xmpp-sasl"
 	nsBind = "urn:ietf:params:xml:ns:xmpp-bind"
 	nsSession = "urn:ietf:params:xml:ns:xmpp-session"
+	NsRoster = "jabber:iq:roster"
 
 	// DNS SRV names
 	serverSrv = "xmpp-server"
@@ -53,8 +55,6 @@
 	socketSync sync.WaitGroup
 	saslExpected string
 	authDone bool
-	idMutex sync.Mutex
-	nextId int64
 	handlers chan *stanzaHandler
 	inputControl chan int
 	// This channel may be used as a convenient way to generate a
@@ -73,6 +73,7 @@
 	// connection process. It should not be updated once
 	// StartSession() returns.
 	Features *Features
+	roster map[string] *RosterItem
 }
 var _ io.Closer = &Client{}
 
@@ -114,7 +115,7 @@
 	cl.password = password
 	cl.Jid = *jid
 	cl.socket = tcp
-	cl.handlers = make(chan *stanzaHandler, 1)
+	cl.handlers = make(chan *stanzaHandler, 100)
 	cl.inputControl = make(chan int)
 	idCh := make(chan string)
 	cl.Id = idCh
@@ -248,12 +249,12 @@
 	cl.inputControl <- 1
 }
 
-// Start an XMPP session. This should typically be done immediately
-// after creating the new Client. Once the session has been
-// established, pr will be sent as an initial presence; nil means
-// don't send initial presence. The initial presence can be a
-// newly-initialized Presence struct. See RFC 3921, Section 3.
-func (cl *Client) StartSession(pr *Presence) os.Error {
+// Start an XMPP session. A typical XMPP client should call this
+// immediately after creating the Client in order to start the
+// session, retrieve the roster, and broadcast an initial
+// presence. The presence can be as simple as a newly-initialized
+// Presence struct.  See RFC 3921, Section 3.
+func (cl *Client) StartSession(getRoster bool, pr *Presence) os.Error {
 	id := <- cl.Id
 	iq := &Iq{To: cl.Jid.Domain, Id: id, Type: "set", Any:
 		&Generic{XMLName: xml.Name{Space: nsSession, Local:
@@ -265,14 +266,75 @@
 			ch <- st.XError()
 			return false
 		}
-		if pr != nil {
-			cl.Out <- pr
-		}
 		ch <- nil
 		return false
 	}
 	cl.HandleStanza(id, f)
 	cl.Out <- iq
+
 	// Now wait until the callback is called.
-	return <-ch
+	if err := <- ch ; err != nil {
+		return err
+	}
+	if getRoster {
+		err := cl.fetchRoster()
+		if err != nil {
+			return err
+		}
+	}
+	if pr != nil {
+		cl.Out <- pr
+	}
+	return nil
 }
+
+// Synchronously fetch this entity's roster from the server and cache
+// that information.
+func (cl *Client) fetchRoster() os.Error {
+	iq := &Iq{From: cl.Jid.String(), Id: <- cl.Id, Type: "get",
+		Query: &RosterQuery{XMLName: xml.Name{Local: "query",
+			Space: NsRoster}}}
+	ch := make(chan os.Error)
+	f := func(st Stanza) bool {
+		iq, ok := st.(*Iq)
+		if !ok {
+			ch <- os.NewError(fmt.Sprintf(
+				"Roster query result not iq: %v", st))
+			return false
+		}
+		if iq.Type == "error" {
+			ch <- iq.Error
+			return false
+		}
+		q := iq.Query
+		if q == nil {
+			ch <- os.NewError(fmt.Sprintf(
+				"Roster query result nil query: %v",
+				iq))
+			return false
+		}
+		cl.roster = make(map[string] *RosterItem, len(q.Item))
+		for _, item := range(q.Item) {
+			cl.roster[item.Jid] = &item
+		}
+		ch <- nil
+		return false
+	}
+	cl.HandleStanza(iq.Id, f)
+	cl.Out <- iq
+	// Wait for f to complete.
+	return <- ch
+}
+
+// BUG(cjyar) The roster isn't actually updated when things change.
+
+// Returns the current roster of other entities which this one has a
+// relationship with. Changes to the roster will be signaled by an
+// appropriate Iq appearing on Client.In. See RFC 3921, Section 7.4.
+func (cl *Client) Roster() map[string] *RosterItem {
+	r := make(map[string] *RosterItem)
+	for key, val := range(cl.roster) {
+		r[key] = val
+	}
+	return r
+}