diff options
author | Alex Auvolat <alex@adnab.me> | 2020-02-16 17:53:31 +0100 |
---|---|---|
committer | Alex Auvolat <alex@adnab.me> | 2020-02-16 17:53:31 +0100 |
commit | 225fc84f097aa615239df6deece647a19794234a (patch) | |
tree | 14ce2ea0a61993c56da21f8f0c3f157177717bbb /connector | |
parent | b7f0776784b6e5fe0f0a028c2913f58e995afa73 (diff) | |
download | easybridge-225fc84f097aa615239df6deece647a19794234a.tar.gz easybridge-225fc84f097aa615239df6deece647a19794234a.zip |
Basic XMPP
Diffstat (limited to 'connector')
-rw-r--r-- | connector/config.go | 34 | ||||
-rw-r--r-- | connector/connector.go | 2 | ||||
-rw-r--r-- | connector/irc/irc.go | 43 | ||||
-rw-r--r-- | connector/xmpp/xmpp.go | 274 |
4 files changed, 332 insertions, 21 deletions
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") + irc.nick, err = c.GetString("nick") + if err != nil { + return err + } - port, err := c.GetInt("port") + irc.server, err = c.GetString("server") if err != nil { return err } - if port == 0 { - port = 6667 + + 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 +} + |