// Copyright 2011 Google Inc. All rights reserved. // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. /* Package xmpp provides the means to send and receive instant messages to and from users of XMPP-compatible services. To send a message, m := &xmpp.Message{ To: []string{"kaylee@example.com"}, Body: `Hi! How's the carrot?`, } err := m.Send(c) To receive messages, func init() { xmpp.Handle(handleChat) } func handleChat(c context.Context, m *xmpp.Message) { // ... } */ package xmpp // import "google.golang.org/appengine/xmpp" import ( "errors" "fmt" "net/http" "golang.org/x/net/context" "google.golang.org/appengine" "google.golang.org/appengine/internal" pb "google.golang.org/appengine/internal/xmpp" ) // Message represents an incoming chat message. type Message struct { // Sender is the JID of the sender. // Optional for outgoing messages. Sender string // To is the intended recipients of the message. // Incoming messages will have exactly one element. To []string // Body is the body of the message. Body string // Type is the message type, per RFC 3921. // It defaults to "chat". Type string // RawXML is whether the body contains raw XML. RawXML bool } // Presence represents an outgoing presence update. type Presence struct { // Sender is the JID (optional). Sender string // The intended recipient of the presence update. To string // Type, per RFC 3921 (optional). Defaults to "available". Type string // State of presence (optional). // Valid values: "away", "chat", "xa", "dnd" (RFC 3921). State string // Free text status message (optional). Status string } var ( ErrPresenceUnavailable = errors.New("xmpp: presence unavailable") ErrInvalidJID = errors.New("xmpp: invalid JID") ) // Handle arranges for f to be called for incoming XMPP messages. // Only messages of type "chat" or "normal" will be handled. func Handle(f func(c context.Context, m *Message)) { http.HandleFunc("/_ah/xmpp/message/chat/", func(_ http.ResponseWriter, r *http.Request) { f(appengine.NewContext(r), &Message{ Sender: r.FormValue("from"), To: []string{r.FormValue("to")}, Body: r.FormValue("body"), }) }) } // Send sends a message. // If any failures occur with specific recipients, the error will be an appengine.MultiError. func (m *Message) Send(c context.Context) error { req := &pb.XmppMessageRequest{ Jid: m.To, Body: &m.Body, RawXml: &m.RawXML, } if m.Type != "" && m.Type != "chat" { req.Type = &m.Type } if m.Sender != "" { req.FromJid = &m.Sender } res := &pb.XmppMessageResponse{} if err := internal.Call(c, "xmpp", "SendMessage", req, res); err != nil { return err } if len(res.Status) != len(req.Jid) { return fmt.Errorf("xmpp: sent message to %d JIDs, but only got %d statuses back", len(req.Jid), len(res.Status)) } me, any := make(appengine.MultiError, len(req.Jid)), false for i, st := range res.Status { if st != pb.XmppMessageResponse_NO_ERROR { me[i] = errors.New(st.String()) any = true } } if any { return me } return nil } // Invite sends an invitation. If the from address is an empty string // the default (yourapp@appspot.com/bot) will be used. func Invite(c context.Context, to, from string) error { req := &pb.XmppInviteRequest{ Jid: &to, } if from != "" { req.FromJid = &from } res := &pb.XmppInviteResponse{} return internal.Call(c, "xmpp", "SendInvite", req, res) } // Send sends a presence update. func (p *Presence) Send(c context.Context) error { req := &pb.XmppSendPresenceRequest{ Jid: &p.To, } if p.State != "" { req.Show = &p.State } if p.Type != "" { req.Type = &p.Type } if p.Sender != "" { req.FromJid = &p.Sender } if p.Status != "" { req.Status = &p.Status } res := &pb.XmppSendPresenceResponse{} return internal.Call(c, "xmpp", "SendPresence", req, res) } var presenceMap = map[pb.PresenceResponse_SHOW]string{ pb.PresenceResponse_NORMAL: "", pb.PresenceResponse_AWAY: "away", pb.PresenceResponse_DO_NOT_DISTURB: "dnd", pb.PresenceResponse_CHAT: "chat", pb.PresenceResponse_EXTENDED_AWAY: "xa", } // GetPresence retrieves a user's presence. // If the from address is an empty string the default // (yourapp@appspot.com/bot) will be used. // Possible return values are "", "away", "dnd", "chat", "xa". // ErrPresenceUnavailable is returned if the presence is unavailable. func GetPresence(c context.Context, to string, from string) (string, error) { req := &pb.PresenceRequest{ Jid: &to, } if from != "" { req.FromJid = &from } res := &pb.PresenceResponse{} if err := internal.Call(c, "xmpp", "GetPresence", req, res); err != nil { return "", err } if !*res.IsAvailable || res.Presence == nil { return "", ErrPresenceUnavailable } presence, ok := presenceMap[*res.Presence] if ok { return presence, nil } return "", fmt.Errorf("xmpp: unknown presence %v", *res.Presence) } // GetPresenceMulti retrieves multiple users' presence. // If the from address is an empty string the default // (yourapp@appspot.com/bot) will be used. // Possible return values are "", "away", "dnd", "chat", "xa". // If any presence is unavailable, an appengine.MultiError is returned func GetPresenceMulti(c context.Context, to []string, from string) ([]string, error) { req := &pb.BulkPresenceRequest{ Jid: to, } if from != "" { req.FromJid = &from } res := &pb.BulkPresenceResponse{} if err := internal.Call(c, "xmpp", "BulkGetPresence", req, res); err != nil { return nil, err } presences := make([]string, 0, len(res.PresenceResponse)) errs := appengine.MultiError{} addResult := func(presence string, err error) { presences = append(presences, presence) errs = append(errs, err) } anyErr := false for _, subres := range res.PresenceResponse { if !subres.GetValid() { anyErr = true addResult("", ErrInvalidJID) continue } if !*subres.IsAvailable || subres.Presence == nil { anyErr = true addResult("", ErrPresenceUnavailable) continue } presence, ok := presenceMap[*subres.Presence] if ok { addResult(presence, nil) } else { anyErr = true addResult("", fmt.Errorf("xmpp: unknown presence %q", *subres.Presence)) } } if anyErr { return presences, errs } return presences, nil } func init() { internal.RegisterErrorCodeMap("xmpp", pb.XmppServiceError_ErrorCode_name) }