From 225fc84f097aa615239df6deece647a19794234a Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Sun, 16 Feb 2020 17:53:31 +0100 Subject: [PATCH] Basic XMPP --- connector/config.go | 34 ++++- connector/connector.go | 2 +- connector/irc/irc.go | 45 ++++--- connector/xmpp/xmpp.go | 274 +++++++++++++++++++++++++++++++++++++++++ go.mod | 6 +- go.sum | 4 + main.go | 66 ++++++++-- 7 files changed, 401 insertions(+), 30 deletions(-) create mode 100644 connector/xmpp/xmpp.go diff --git a/connector/config.go b/connector/config.go index d719b49..e0fcf17 100644 --- a/connector/config.go +++ b/connector/config.go @@ -1,21 +1,45 @@ package connector import ( + "fmt" "strconv" + "strings" ) type Configuration map[string]string -func (c Configuration) GetString(k string) string { +func (c Configuration) GetString(k string, deflt ...string) (string, error) { if ss, ok := c[k]; ok { - return ss + return ss, nil } - return "" + if len(deflt) > 0 { + return deflt[0], nil + } + return "", fmt.Errorf("Missing configuration key: %s", k) } -func (c Configuration) GetInt(k string) (int, error) { +func (c Configuration) GetInt(k string, deflt ...int) (int, error) { if ss, ok := c[k]; ok { return strconv.Atoi(ss) } - return 0, nil + if len(deflt) > 0 { + return deflt[0], nil + } + return 0, fmt.Errorf("Missing configuration key: %s", k) +} + +func (c Configuration) GetBool(k string, deflt ...bool) (bool, error) { + if ss, ok := c[k]; ok { + if strings.EqualFold(ss, "true") { + return true, nil + } else if strings.EqualFold(ss, "false") { + return false, nil + } else { + return false, fmt.Errorf("Invalid value: %s", ss) + } + } + if len(deflt) > 0 { + return deflt[0], nil + } + return false, fmt.Errorf("Missing configuration key: %s", k) } diff --git a/connector/connector.go b/connector/connector.go index 9894bfc..92b1adc 100644 --- a/connector/connector.go +++ b/connector/connector.go @@ -106,7 +106,7 @@ type Event struct { Room RoomID // Message text or action text - Message string + Text string // Attached files such as images Attachements map[string]MediaObject diff --git a/connector/irc/irc.go b/connector/irc/irc.go index b9c58ad..671d19a 100644 --- a/connector/irc/irc.go +++ b/connector/irc/irc.go @@ -12,11 +12,11 @@ import ( ) // User id format: nickname@server + // Room id format: #room_name@server type IRC struct { handler Handler - config Configuration connected bool timeout int @@ -40,17 +40,26 @@ func (irc *IRC) Configure(c Configuration) error { irc.Close() } - irc.config = c + var err error - irc.nick = c.GetString("nick") - irc.server = c.GetString("server") - - port, err := c.GetInt("port") + irc.nick, err = c.GetString("nick") if err != nil { return err } - if port == 0 { - port = 6667 + + irc.server, err = c.GetString("server") + if err != nil { + return err + } + + port, err := c.GetInt("port", 6666) + if err != nil { + return err + } + + ssl, err := c.GetBool("ssl", true) + if err != nil { + return err } client := girc.New(girc.Config{ @@ -59,7 +68,7 @@ func (irc *IRC) Configure(c Configuration) error { Nick: irc.nick, User: irc.nick, Out: os.Stderr, - SSL: true, + SSL: ssl, }) client.Handlers.Add(girc.CONNECTED, irc.ircConnected) @@ -83,7 +92,7 @@ func (irc *IRC) Configure(c Configuration) error { return nil } } - return fmt.Errorf("Failed to conncect after 42s attempting") + return fmt.Errorf("Failed to connect after 42s attempting") } func (irc *IRC) User() UserID { @@ -184,9 +193,9 @@ func (irc *IRC) Send(event *Event) error { } if event.Type == EVENT_MESSAGE { - irc.conn.Cmd.Message(dest, event.Message) + irc.conn.Cmd.Message(dest, event.Text) } else if event.Type == EVENT_ACTION { - irc.conn.Cmd.Action(dest, event.Message) + irc.conn.Cmd.Action(dest, event.Text) } else { return fmt.Errorf("Invalid event type") } @@ -194,8 +203,9 @@ func (irc *IRC) Send(event *Event) error { } func (irc *IRC) Close() { - irc.conn.Close() + conn := irc.conn irc.conn = nil + conn.Close() } func (irc *IRC) connectLoop(c *girc.Client) { @@ -206,10 +216,13 @@ func (irc *IRC) connectLoop(c *girc.Client) { } if err := c.Connect(); err != nil { irc.connected = false - fmt.Printf("IRC failed to connect / disconnected: %s", err) - fmt.Printf("Retrying in %ds", irc.timeout) + fmt.Printf("IRC failed to connect / disconnected: %s\n", err) + fmt.Printf("Retrying in %ds\n", irc.timeout) time.Sleep(time.Duration(irc.timeout) * time.Second) irc.timeout *= 2 + if irc.timeout > 600 { + irc.timeout = 600 + } } else { return } @@ -226,7 +239,7 @@ func (irc *IRC) ircPrivmsg(c *girc.Client, e girc.Event) { ev := &Event{ Type: EVENT_MESSAGE, Author: UserID(e.Source.Name + "@" + irc.server), - Message: e.Last(), + Text: e.Last(), } if e.IsFromChannel() { ev.Room = RoomID(e.Params[0] + "@" + irc.server) diff --git a/connector/xmpp/xmpp.go b/connector/xmpp/xmpp.go new file mode 100644 index 0000000..2b2977e --- /dev/null +++ b/connector/xmpp/xmpp.go @@ -0,0 +1,274 @@ +package xmpp + +import ( + "log" + "time" + //"os" + "strings" + "fmt" + "crypto/tls" + + . "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" + + gxmpp "github.com/mattn/go-xmpp" +) + +// 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 +} + +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 + 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 + } + + switch v := m.(type) { + case gxmpp.Chat: + log.Printf("== Receiving %#v\n", v) + + 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: + // Do nothing. + } + } +} + +func (xm *XMPP) User() UserID { + return UserID(xm.jid) +} + +func (xm *XMPP) SetUserInfo(info *UserInfo) error { + //TODO + 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 { + 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 +} + diff --git a/go.mod b/go.mod index c032fe7..00f04ae 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module git.deuxfleurs.fr/Deuxfleurs/easybridge go 1.13 -require github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 +require ( + github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 + github.com/matterbridge/go-xmpp v0.0.0-20180131083630-7ec2b8b7def6 + github.com/mattn/go-xmpp v0.0.0-20200128155807-a86b6abcb3ad +) diff --git a/go.sum b/go.sum index 59abef6..f4be87f 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 h1:BS9tqL0OCiOGuy/CYYk2gc33fxqaqh5/rhqMKu4tcYA= github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7/go.mod h1:liX5MxHPrwgHaKowoLkYGwbXfYABh1jbZ6FpElbGF1I= +github.com/matterbridge/go-xmpp v0.0.0-20180131083630-7ec2b8b7def6 h1:GDh7egrbDEzP41mScMt7Q/uPM2nJENh9LNFXjUOGts8= +github.com/matterbridge/go-xmpp v0.0.0-20180131083630-7ec2b8b7def6/go.mod h1:ECDRehsR9TYTKCAsRS8/wLeOk6UUqDydw47ln7wG41Q= +github.com/mattn/go-xmpp v0.0.0-20200128155807-a86b6abcb3ad h1:ntj2CDcRNjFht20llTwIwwguKa00u0UCLtF2J5+Gmxo= +github.com/mattn/go-xmpp v0.0.0-20200128155807-a86b6abcb3ad/go.mod h1:Cs5mF0OsrRRmhkyOod//ldNPOwJsrBvJ+1WRspv0xoc= diff --git a/main.go b/main.go index f2b5b31..9c5c015 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector" "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/irc" + "git.deuxfleurs.fr/Deuxfleurs/easybridge/connector/xmpp" ) type TmpHandler struct{ @@ -35,17 +36,17 @@ func (h *TmpHandler) Event(e *connector.Event) { } else if e.Type == connector.EVENT_LEAVE { fmt.Printf("C E Leave %s %s\n", e.Author, e.Room) } else if e.Type == connector.EVENT_MESSAGE { - fmt.Printf("C E Message %s %s %s\n", e.Author, e.Room, e.Message) - if strings.Contains(e.Message, "ezbrexit") { - fmt.Printf("we have to exit") + fmt.Printf("C E Message %s %s %s\n", e.Author, e.Room, e.Text) + if strings.Contains(e.Text, "ezbrexit") { + fmt.Printf("we have to exit\n") h.exit <- true } } else if e.Type == connector.EVENT_ACTION { - fmt.Printf("C E Action %s %s %s\n", e.Author, e.Room, e.Message) + fmt.Printf("C E Action %s %s %s\n", e.Author, e.Room, e.Text) } } -func main() { +func testIrc() { irc := &irc.IRC{} h := TmpHandler{ exit: make(chan bool), @@ -70,7 +71,7 @@ func main() { err = irc.Send(&connector.Event{ Room: connector.RoomID("#ezbrtest@irc.ulminfo.fr"), Type: connector.EVENT_MESSAGE, - Message: "EZBR TEST", + Text: "EZBR TEST", }) if err != nil { log.Fatalf("Send: %s", err) @@ -80,7 +81,7 @@ func main() { err = irc.Send(&connector.Event{ Recipient: connector.UserID("lx@irc.ulminfo.fr"), Type: connector.EVENT_MESSAGE, - Message: "EZBR TEST direct message lol", + Text: "EZBR TEST direct message lol", }) if err != nil { log.Fatalf("Send: %s", err) @@ -91,3 +92,54 @@ func main() { fmt.Printf("got exit signal\n") irc.Close() } + +func testXmpp() { + xmpp := &xmpp.XMPP{} + h := TmpHandler{ + exit: make(chan bool), + } + xmpp.SetHandler(&h) + + err := xmpp.Configure(connector.Configuration{ + "server": "jabber.fr", + "jid": "ezbr@jabber.fr", + "password": "azerty1234", + }) + if err != nil { + log.Fatalf("Connect: %s", err) + } + + err = xmpp.Join(connector.RoomID("ezbrtest@muc.linkmauve.fr")) + if err != nil { + log.Fatalf("Join: %s", err) + } + + time.Sleep(time.Duration(1)*time.Second) + err = xmpp.Send(&connector.Event{ + Room: connector.RoomID("ezbrtest@muc.linkmauve.fr"), + Type: connector.EVENT_MESSAGE, + Text: "EZBR TEST", + }) + if err != nil { + log.Fatalf("Send: %s", err) + } + + time.Sleep(time.Duration(1)*time.Second) + err = xmpp.Send(&connector.Event{ + Recipient: connector.UserID("alexis211@jabber.fr"), + Type: connector.EVENT_MESSAGE, + Text: "EZBR TEST direct message lol", + }) + if err != nil { + log.Fatalf("Send: %s", err) + } + + fmt.Printf("waiting exit signal\n") + <-h.exit + fmt.Printf("got exit signal\n") + xmpp.Close() +} + +func main() { + testXmpp() +}