package xmpp import ( "crypto/tls" "fmt" "log" "net" "strings" "time" "git.hacknology.de/projekte/spaceapi" "git.hacknology.de/projekte/spaceapi/txt" "github.com/mattn/go-xmpp" ) // Listener returns a storage listener that broadcasts changes in the state to an XMPP MUC. func Listener(storage *spaceapi.Storage, jid, password, target, handle string) (spaceapi.Listener, error) { xmppHost, err := lookupHost(jid) if err != nil { return nil, fmt.Errorf("can not find SRV record: %s", err) } clientOpts := xmpp.Options{ Host: xmppHost, User: jid, Password: password, NoTLS: true, Debug: false, Session: false, TLSConfig: &tls.Config{ InsecureSkipVerify: true, }, } sendChan := make(chan string, 1) go clientLoop(storage, sendChan, clientOpts, target, handle) return func(old, new spaceapi.SpaceStatus) { if new.State.Open == nil { return } if old.State.Open == new.State.Open { return } msg := txt.TransitionToOpen if !*new.State.Open { msg = txt.TransitionToClosed } sendChan <- msg }, nil } const ( defaultClientPort = 5222 backOffFactor = 1.2 maxReconnectWait = 5 * time.Minute ) func clientLoop(storage *spaceapi.Storage, sendChan <-chan string, clientOpts xmpp.Options, target, handle string) { wait := 1 * time.Second for { client, err := clientConnect(clientOpts, target, handle) if err != nil { log.Printf("Error connecting to XMPP server: %s", err) wait = time.Duration(float64(wait) * backOffFactor) if wait > maxReconnectWait { wait = maxReconnectWait } log.Printf("Retrying in %s...", wait) time.Sleep(wait) continue } receiveChan, errorChan := receiveLoop(storage, client, target) receiveLoop: for { select { case err := <-errorChan: log.Printf("Error receiving messages: %s", err) break receiveLoop case msg := <-sendChan: if err := sendGroupMessage(client, target, msg); err != nil { log.Printf("Error sending message: %s", err) } case chat := <-receiveChan: if err := handleMessage(storage, client, target, chat); err != nil { log.Printf("Error handling message: %s", err) } } } log.Printf("Closing XMPP connection.") client.Close() } } func handleMessage(storage *spaceapi.Storage, client *xmpp.Client, target string, chat xmpp.Chat) error { switch chat.Text { case txt.CommandHelp: if err := sendMessage(client, chat.Remote, txt.HelpMessage); err != nil { return fmt.Errorf("error sending reply: %s", err) } case txt.CommandState: state := storage.Status().State msg := txt.StateUnknown if state.Open != nil { switch *state.Open { case true: msg = txt.StateOpen case false: msg = txt.StateClosed } } sendFunc := sendGroupMessage replyTarget := target if chat.Type != "groupchat" { sendFunc = sendMessage replyTarget = chat.Remote } if err := sendFunc(client, replyTarget, msg); err != nil { return fmt.Errorf("error sending reply: %s", err) } case txt.CommandClose, txt.CommandClosed: if chat.Type != "groupchat" { if err := sendMessage(client, chat.Remote, txt.ErrorNonPublic); err != nil { return fmt.Errorf("error sending reply: %s", err) } return nil } storage.Modify(func(status *spaceapi.SpaceStatus) { open := false status.State.Open = &open }) } return nil } func clientConnect(clientOpts xmpp.Options, target, handle string) (*xmpp.Client, error) { log.Printf("Connecting to %s as %s", clientOpts.Host, clientOpts.User) client, err := clientOpts.NewClient() if err != nil { return nil, fmt.Errorf("can not connect to server: %s", err) } if _, err := client.JoinMUCNoHistory(target, handle); err != nil { return nil, fmt.Errorf("can not join room: %s", err) } return client, nil } func receiveLoop(storage *spaceapi.Storage, client *xmpp.Client, target string) (<-chan xmpp.Chat, <-chan error) { receiveChan := make(chan xmpp.Chat) errorChan := make(chan error) go func() { defer close(errorChan) defer close(receiveChan) for { chat, err := client.Recv() if err != nil { errorChan <- err return } switch v := chat.(type) { case xmpp.Chat: receiveChan <- v } } }() return receiveChan, errorChan } func lookupHost(jid string) (string, error) { if !strings.Contains(jid, "@") { return "", fmt.Errorf("not a valid JID: %s", jid) } parts := strings.SplitN(jid, "@", 2) _, addrs, err := net.LookupSRV("xmpp-client", "tcp", parts[1]) if err != nil { return "", fmt.Errorf("%s", err) } if len(addrs) == 0 { return fmt.Sprintf("%s:%d", parts[1], defaultClientPort), nil } return fmt.Sprintf("%s:%d", addrs[0].Target, addrs[0].Port), nil } func sendMessage(client *xmpp.Client, target, msg string) error { chat := xmpp.Chat{ Remote: target, Type: "chat", Text: msg, } if _, err := client.Send(chat); err != nil { return err } return nil } func sendGroupMessage(client *xmpp.Client, target, msg string) error { chat := xmpp.Chat{ Remote: target, Type: "groupchat", Text: msg, } if _, err := client.Send(chat); err != nil { return err } return nil }