package xmpp import ( "time" //"os" "strings" "fmt" "crypto/tls" log "github.com/sirupsen/logrus" gxmpp "github.com/mattn/go-xmpp" . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" ) // User id format: username@server (= JID) // OR: nickname@room_name@muc_server // Room id format: room_name@muc_server (= MUC ID) type XMPP struct { handler Handler connectorLoopNum int connected bool timeout int server string port int ssl bool jid string jid_localpart string password string nickname string conn *gxmpp.Client isMUC map[string]bool } func (xm *XMPP) SetHandler(h Handler) { xm.handler = h } func(xm *XMPP) Protocol() string { return "xmpp" } func (xm *XMPP) Configure(c Configuration) error { if xm.conn != nil { xm.Close() } // Parse and validate configuration var err error xm.server, err = c.GetString("server") if err != nil { return err } xm.port, err = c.GetInt("port", 5222) if err != nil { return err } xm.ssl, err = c.GetBool("ssl", true) if err != nil { return err } xm.jid, err = c.GetString("jid") if err != nil { return err } jid_parts := strings.Split(xm.jid, "@") if len(jid_parts) != 2 { return fmt.Errorf("Invalid JID: %s", xm.jid) } if jid_parts[1] != xm.server { return fmt.Errorf("JID %s not on server %s", xm.jid, xm.server) } xm.jid_localpart = jid_parts[0] xm.nickname = xm.jid_localpart xm.password, err = c.GetString("password") if err != nil { return err } // Try to connect if xm.isMUC == nil { xm.isMUC = make(map[string]bool) } xm.connectorLoopNum += 1 go xm.connectLoop(xm.connectorLoopNum) for i := 0; i < 42; i++ { time.Sleep(time.Duration(1)*time.Second) if xm.connected { return nil } } return fmt.Errorf("Failed to connect after 42s attempting") } func (xm *XMPP) connectLoop(num int) { xm.timeout = 10 for { if xm.connectorLoopNum != num { return } tc := &tls.Config{ ServerName: strings.Split(xm.jid, "@")[1], InsecureSkipVerify: true, } options := gxmpp.Options{ Host: xm.server, User: xm.jid, Password: xm.password, NoTLS: true, StartTLS: xm.ssl, Session: true, TLSConfig: tc, } var err error xm.conn, err = options.NewClient() if err != nil { xm.connected = false fmt.Printf("XMPP failed to connect / disconnected: %s\n", err) fmt.Printf("Retrying in %ds\n", xm.timeout) time.Sleep(time.Duration(xm.timeout) * time.Second) xm.timeout *= 2 if xm.timeout > 600 { xm.timeout = 600 } } else { xm.connected = true xm.timeout = 10 err = xm.handleXMPP() if err != nil { xm.connected = false fmt.Printf("XMPP disconnected: %s\n", err) fmt.Printf("Reconnecting.\n") } } } } func (xm *XMPP) xmppKeepAlive() chan bool { done := make(chan bool) go func() { ticker := time.NewTicker(90 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: if err := xm.conn.PingC2S("", ""); err != nil { log.Printf("PING failed %#v\n", err) } case <-done: return } } }() return done } func (xm *XMPP) handleXMPP() error { done := xm.xmppKeepAlive() defer close(done) for { m, err := xm.conn.Recv() if err != nil { return err } fmt.Printf("XMPP: %#v\n", m) switch v := m.(type) { case gxmpp.Chat: log.Printf("== Receiving %#v\n", v) if v.Text == "" && v.Type == "groupchat" { // Empty message when we joined group chat if !strings.Contains(v.Remote, "/") { xm.handler.Joined(RoomID(v.Remote)) } if v.Subject != "" { author := UserID("") remote_sp := strings.Split(v.Remote, "/") if len(remote_sp) == 2 { author = UserID(remote_sp[1] + "@" + remote_sp[0]) } xm.handler.RoomInfoUpdated(RoomID(v.Remote), author, &RoomInfo{ Topic: v.Subject, }) } continue } if v.Text == "" || v.Remote == xm.jid { continue } event := &Event{ Type: EVENT_MESSAGE, Text: v.Text, } log.Printf("Remote: %s\n", v.Remote) if strings.HasPrefix(event.Text, "/me ") { event.Type = EVENT_ACTION event.Text = strings.Replace(event.Text, "/me ", "", 1) } if v.Type == "chat" { remote_jid := strings.Split(v.Remote, "/")[0] event.Author = UserID(remote_jid) xm.handler.Event(event) } if v.Type == "groupchat" { remote := strings.Split(v.Remote, "/") if len(remote) != 2 { log.Printf("Invalid remote: %s\n", v.Remote) continue } event.Room = RoomID(remote[0]) event.Author = UserID(remote[1] + "@" + remote[0]) if strings.Contains(v.Text, "has set the subject to:") { // TODO continue } xm.handler.Event(event) } case gxmpp.Presence: remote := strings.Split(v.From, "/") if muc, ok := xm.isMUC[remote[0]]; ok && muc && len(remote) == 2 { event := &Event{ Type: EVENT_JOIN, Room: RoomID(remote[0]), Author: UserID(remote[1] + "@" + remote[0]), } if v.Type == "unavailable" { event.Type = EVENT_LEAVE } xm.handler.Event(event) } // Do nothing. } } } func (xm *XMPP) User() UserID { return UserID(xm.jid) } func (xm *XMPP) SetUserInfo(info *UserInfo) error { return fmt.Errorf("Not implemented") } func (xm *XMPP) SetRoomInfo(roomId RoomID, info *RoomInfo) error { // TODO return fmt.Errorf("Not implemented") } func (xm *XMPP) Join(roomId RoomID) error { xm.isMUC[string(roomId)] = true fmt.Printf("Join %s with nick %s\n", roomId, xm.nickname) _, err := xm.conn.JoinMUCNoHistory(string(roomId), xm.nickname) return err } func (xm *XMPP) Invite(userId UserID, roomId RoomID) error { // TODO return fmt.Errorf("Not implemented") } func (xm *XMPP) Leave(roomId RoomID) { // TODO } func (xm *XMPP) Send(event *Event) error { if len(event.Recipient) > 0 { xm.conn.Send(gxmpp.Chat{ Type: "chat", Remote: string(event.Recipient), Text: event.Text, }) return nil } else if len(event.Room) > 0 { xm.conn.Send(gxmpp.Chat{ Type: "groupchat", Remote: string(event.Room), Text: event.Text, }) return nil } else { return fmt.Errorf("Invalid event") } } func (xm *XMPP) Close() { xm.conn.Close() xm.conn = nil xm.connectorLoopNum += 1 }