diff --git a/bridge/config/config.go b/bridge/config/config.go index 3d1206c7..f1122793 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -184,6 +184,7 @@ type BridgeValues struct { Telegram map[string]Protocol Rocketchat map[string]Protocol SSHChat map[string]Protocol + WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results Zulip map[string]Protocol General Protocol Gateway []Gateway diff --git a/bridge/whatsapp/whatsapp.go b/bridge/whatsapp/whatsapp.go new file mode 100644 index 00000000..ab7d5f11 --- /dev/null +++ b/bridge/whatsapp/whatsapp.go @@ -0,0 +1,436 @@ +package bwhatsapp + +import ( + "encoding/gob" + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/42wim/matterbridge/bridge" + "github.com/42wim/matterbridge/bridge/config" + + "github.com/Baozisoftware/qrcode-terminal-go" + "github.com/Rhymen/go-whatsapp" + + "maunium.net/go/mautrix-whatsapp/types" // TODO check what is used? + // "maunium.net/go/mautrix-whatsapp/whatsapp-ext" + // "@c.us" -> "@s.whatsapp.net" + // "github.com/tulir/mautrix-whatsapp" +) + +const ( + + // Account config parameters + cfgNumber = "Number" +) + +type Bwhatsapp struct { + *bridge.Config + + // https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21 + session *whatsapp.Session + // connExt *whatsappExt.ExtendedConn // https://github.com/tulir/mautrix-whatsapp/blob/master/whatsapp-ext/whatsapp.go + conn *whatsapp.Conn + startedAt uint64 +} + +func New(cfg *bridge.Config) bridge.Bridger { + number := cfg.GetString(cfgNumber) + if number == "" { + cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") + } + + // TODO do we need cache? + //newCache, err := lru.New(5000) + //if err != nil { + // cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err) + //} + b := &Bwhatsapp{ + Config: cfg, + + //uuid: xid.New().String(), + //users: map[string]*slack.User{}, + //channelsByID: map[string]*slack.Channel{}, + //channelsByName: map[string]*slack.Channel{}, + //earliestChannelRefresh: time.Now(), + //earliestUserRefresh: time.Now(), + } + return b +} + +// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76 +//func (b *Bwhatsapp) Command(cmd string) string { +// return "" +//} + +// TODO learning GO: What is "(b *Bwhatsapp)" in this function's signature? Not argument and not a return value, so what? Does it add method on struct? +func (b *Bwhatsapp) Connect() error { + b.RLock() // TODO do we need locking for Whatsapp? + defer b.RUnlock() + + number := b.GetString(cfgNumber) + if number == "" { + return errors.New("WhatsApp's telephone Number need to be configured") + } + + // https://github.com/Rhymen/go-whatsapp#creating-a-connection + b.Log.Debugln("Connecting to WhatsApp..") + conn, err := whatsapp.NewConn(20 * time.Second) + if err != nil { + return errors.New("Failed to connect to WhatsApp: " + err.Error()) + } + + b.conn = conn + //b.connExt = whatsappExt.ExtendConn(b.conn) + //b.connExt.SetClientName("Matterbridge WhatsApp bridge", "mb-wa") + b.conn.AddHandler(b) + b.Log.Debugln("WhatsApp connection successful") + + // load existing session in order to keep it between restarts + // TODO try to load session from env vars or otherwise for Azure and other clouds + // now implemented: load session from file + if b.session == nil { + session, err := readSession() + if err == nil { + sess, err := b.conn.RestoreSession(session) // https://github.com/Rhymen/go-whatsapp#restore + if err != nil { // restore session connection timed out + // TODO return or continue to normal login? + return errors.New("Failed to restore session: " + err.Error()) + } + + b.session = &sess + b.Log.Debugln("Session restored successfully!") + } + } + + // login to a new session + if b.session == nil { + if err := b.Login(); err != nil { + return err + } + } + b.startedAt = uint64(time.Now().Unix()) + + _, err = b.conn.Chats() + if err != nil { + b.Log.Errorln("Error on update of chats: %v", err) + return nil + } + + _, err = b.conn.Contacts() + if err != nil { + b.Log.Errorln("Error on update of contacts: %v", err) + return nil + } + + b.Log.Debugln("Importing all contacts done") + + return nil +} + +func (b *Bwhatsapp) Login() error { + b.Log.Debugln("Logging in..") + + // TODO qrCode, err := qrcode.Encode(code, qrcode.Low, 256) to encode as image/png + // and possibly send it to connected channels (to admin) to authorize the app + // TODO invert configured in settings + qrChan := qrFromTerminal(true) + + session, err := b.conn.Login(qrChan) + if err != nil { + b.Log.Warnln("Failed to log in:", err) + return err + } + b.session = &session + + b.Log.Infof("Logged into session: %#v", session) + b.Log.Infof("Connection: %#v", b.conn) + + err = writeSession(session) + if err != nil { + fmt.Fprintf(os.Stderr, "error saving session: %v\n", err) + } + + // TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp + // TODO get also a nice logo + + // session.Wid + // conn.Info: Wid, Pushname, Connected, Battery, Plugged (TODO notification about unplugged and dead battery) + // jid = strings.Replace(b.conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1) + + return nil +} + +func qrFromTerminal(invert bool) chan string { + qr := make(chan string) + go func() { + terminal := qrcodeTerminal.New() + if invert { + terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) + } + + terminal.Get(<-qr).Print() + }() + + return qr +} + +func readSession() (whatsapp.Session, error) { + session := whatsapp.Session{} + file, err := os.Open("whatsappSession.gob") + if err != nil { + return session, err + } + defer file.Close() + decoder := gob.NewDecoder(file) + err = decoder.Decode(&session) + if err != nil { + return session, err + } + return session, nil +} + +func writeSession(session whatsapp.Session) error { + file, err := os.Create("whatsappSession.gob") + if err != nil { + return err + } + defer file.Close() + encoder := gob.NewEncoder(file) + err = encoder.Encode(session) + if err != nil { + return err + } + return nil +} + +func (b *Bwhatsapp) Disconnect() error { + return nil +} + +func isGroupJid(identifier string) bool { + return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") +} + +func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error { + byJid := isGroupJid(channel.Name) + + // verify if we are member of the given group + if byJid { + // channel.Name specifies static group jID, not the name + if _, exists := b.conn.Store.Contacts[channel.Name]; !exists { + return fmt.Errorf("Account doesn't belong to group with jid %s", channel.Name) + } + } else { + // channel.Name specifies group name that might change, warn about it + var jids []string + for id, contact := range b.conn.Store.Contacts { + if isGroupJid(id) && contact.Name == channel.Name { + jids = append(jids, id) + } + } + + if len(jids) == 0 { + // didn't match any group - print out possibilites + // TODO sort + // copy b; + //sort.Slice(people, func(i, j int) bool { + // return people[i].Age > people[j].Age + //}) + for id, contact := range b.conn.Store.Contacts { + if isGroupJid(id) { + // TODO b.Log.Info + fmt.Printf("%s %s\n", contact.Jid, contact.Name) + } + } + return fmt.Errorf("Please specify group's JID from the below list instead of the name '%s'", channel.Name) + + } else if len(jids) > 1 { + return fmt.Errorf("There is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) + + } else { + return fmt.Errorf("Group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) + } + } + + return nil +} + +func (b *Bwhatsapp) Send(msg config.Message) (string, error) { + b.Log.Debugf("=> Receiving %#v", msg) + + // msg.Channel target group name + // msg.Username empty // TODO why I'm not getting Nickname + // msg.UserID a weird string , probably slack user id + // msg.Avatar has a nice image + // msg.Timestamp has a nice timestamp with loc(ation) / timezone + // msg.ID empty, // TODO why empty?! + + text := whatsapp.TextMessage{ + Info: whatsapp.MessageInfo{ + // Id: "", // TODO id + // TODO Timestamp + RemoteJid: msg.Channel, // which equals to group id + PushName: "pushname", + }, + Text: msg.Text, + } + + // TODO adapt gitter code + //roomID := b.getRoomID(msg.Channel) + //if roomID == "" { + // b.Log.Errorf("Could not find roomID for %v", msg.Channel) + // return "", nil + //} + // + //// Delete message + //if msg.Event == config.EventMsgDelete { + // if msg.ID == "" { + // return "", nil + // } + // // gitter has no delete message api so we edit message to "" + // _, err := b.c.UpdateMessage(roomID, msg.ID, "") + // if err != nil { + // return "", err + // } + // return "", nil + //} + // + //// Upload a file (in gitter case send the upload URL because gitter has no native upload support) + //if msg.Extra != nil { + // for _, rmsg := range helper.HandleExtra(&msg, b.General) { + // b.c.SendMessage(roomID, rmsg.Username+rmsg.Text) + // } + // if len(msg.Extra["file"]) > 0 { + // return b.handleUploadFile(&msg, roomID) + // } + //} + // + //// Edit message + //if msg.ID != "" { + // b.Log.Debugf("updating message with id %s", msg.ID) + // _, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text) + // if err != nil { + // return "", err + // } + // return "", nil + //} + // + //// Post normal message + //resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text) + //if err != nil { + // return "", err + //} + //return resp.ID, nil + + b.Log.Debugf("=> Sending %#v", msg) + + err := b.conn.Send(text) + + // TODO return message id + return "", err +} + +// ================================================================ +// handlers https://github.com/Rhymen/go-whatsapp#add-message-handlers & https://github.com/Rhymen/go-whatsapp/blob/master/handler.go + +func (b *Bwhatsapp) HandleError(err error) { + b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types +} + +func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { + if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { // || message.Info.Timestamp < wh.startTime { + return + } + // whatsapp sends last messages to show context , cut them + if message.Info.Timestamp < b.startedAt { + return + } + + //type MessageInfo struct { + // Id string + // RemoteJid string + // SenderJid string + // Timestamp uint64 + // PushName string + // Status MessageStatus + // QuotedMessageID string // TODO map to parentId + // + // Source *proto.WebMessageInfo + //} + // + //type MessageStatus int + // + //const ( + // Error MessageStatus = 0 + // Pending = 1 + // ServerAck = 2 + // DeliveryAck = 3 + // Read = 4 + // Played = 5 + //) + + messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones + fmt.Println(messageTime.Format(time.UnixDate)) // TODO delete + groupJid := message.Info.RemoteJid + + senderJid := message.Info.SenderJid + if len(senderJid) == 0 { + // TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved + senderJid = *message.Info.Source.Participant + } + + b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJid, b.Account) + rmsg := config.Message{ + UserID: senderJid, + Username: senderJid, // TODO mapping + Text: message.Text, + Timestamp: messageTime, + Channel: groupJid, + Account: b.Account, + Protocol: b.Protocol, + Extra: make(map[string][]interface{}), + // Avatar: b.getAvatar(ev.Message.From.Username), + // ParentID: TODO, // TODO handle thread replies + ID: message.Info.Id} + + //type Message struct { + // Text string `json:"text"` + // Channel string `json:"channel"` + // Username string `json:"username"` + // UserID string `json:"userid"` // userid on the bridge + // Avatar string `json:"avatar"` + // Account string `json:"account"` + // Event string `json:"event"` + // Protocol string `json:"protocol"` + // Gateway string `json:"gateway"` + // ParentID string `json:"parent_id"` + // Timestamp time.Time `json:"timestamp"` + // ID string `json:"id"` + // Extra map[string][]interface{} + //} + + b.Log.Debugf("<= Message is %#v", rmsg) + b.Remote <- rmsg +} + +// +//func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { +// fmt.Println(message) // TODO implement +//} +// +//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { +// fmt.Println(message) // TODO implement +//} +// +//func (b *Bwhatsapp) HandleJsonMessage(message string) { +// fmt.Println(message) // TODO implement +//} +// TODO HandleRawMessage +// TODO HandleAudioMessage + +// TODO questions to Tulir +// Why are you locking on message processing? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/portal.go#L212 +// How are you showing nicks in WhatsApp? Inside message? diff --git a/gateway/bridgemap/bridgemap.go b/gateway/bridgemap/bridgemap.go index 20577dc1..1ad013ed 100644 --- a/gateway/bridgemap/bridgemap.go +++ b/gateway/bridgemap/bridgemap.go @@ -13,6 +13,7 @@ import ( "github.com/42wim/matterbridge/bridge/sshchat" "github.com/42wim/matterbridge/bridge/steam" "github.com/42wim/matterbridge/bridge/telegram" + "github.com/42wim/matterbridge/bridge/whatsapp" "github.com/42wim/matterbridge/bridge/xmpp" "github.com/42wim/matterbridge/bridge/zulip" ) @@ -30,6 +31,7 @@ var FullMap = map[string]bridge.Factory{ "sshchat": bsshchat.New, "steam": bsteam.New, "telegram": btelegram.New, + "whatsapp": bwhatsapp.New, "xmpp": bxmpp.New, "zulip": bzulip.New, } diff --git a/go.mod b/go.mod index 1f8cb17f..c4f74eab 100644 --- a/go.mod +++ b/go.mod @@ -2,13 +2,14 @@ module github.com/42wim/matterbridge require ( github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 + github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 // indirect github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 + github.com/Rhymen/go-whatsapp v0.0.0-20181218094654-2ca6af00572c github.com/bwmarrin/discordgo v0.19.0 github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec github.com/fsnotify/fsnotify v1.4.7 github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible - github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc // indirect github.com/google/gops v0.3.5 github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect github.com/gorilla/schema v1.0.2 @@ -43,8 +44,8 @@ require ( github.com/russross/blackfriday v2.0.0+incompatible github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 - github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect github.com/sirupsen/logrus v1.3.0 + github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 // indirect github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect github.com/spf13/viper v1.3.1 @@ -61,10 +62,8 @@ require ( go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 // indirect - golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 // indirect - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3 ) diff --git a/go.sum b/go.sum index c5aca265..72593e88 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557 h1:IZtuWGfzQnKnCSu+vl8WGLhpVQ5Uvy3rlSwqXSg+sQg= github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557/go.mod h1:jL0YSXMs/txjtGJ4PWrmETOk6KUHMDPMshgQZlTeB3Y= +github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= +github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14 h1:v/zr4ns/4sSahF9KBm4Uc933bLsEEv7LuT63CJ019yo= github.com/BurntSushi/toml v0.0.0-20170318202913-d94612f9fc14/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329 h1:xZBoq249G9MSt+XuY7sVQzcfONJ6IQuwpCK+KAaOpnY= github.com/Philipp15b/go-steam v1.0.1-0.20180818081528-681bd9573329/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg= +github.com/Rhymen/go-whatsapp v0.0.0-20181218094654-2ca6af00572c h1:ldRXgMEfKmzBomrZusl3edG9AGEeztA7jovLEQy62us= +github.com/Rhymen/go-whatsapp v0.0.0-20181218094654-2ca6af00572c/go.mod h1:MSDmePOOkbFFbVW2WRRppBcbA+aabwpXRgyIIG7jDFQ= github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58 h1:MkpmYfld/S8kXqTYI68DfL8/hHXjHogL120Dy00TIxc= github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= @@ -19,16 +23,20 @@ github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec h1:JEUiu7P9smN7zgX github.com/dfordsoft/golib v0.0.0-20180902042739-76ee6ab99bec/go.mod h1:UGa5M2Sz/Uh13AMse4+RELKCDw7kqgqlTjeGae+7vUY= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible h1:i64CCJcSqkRIkm5OSdZQjZq84/gJsk2zNwHWIRYWlKE= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20181225215658-ec221ba9ea45+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc h1:wdhDSKrkYy24mcfzuA3oYm58h0QkyXjwERCkzJDP5kA= github.com/golang/protobuf v0.0.0-20170613224224-e325f446bebc/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/gops v0.3.5 h1:SIWvPLiYvy5vMwjxB3rVFTE4QBhUFj2KKWr3Xm7CKhw= github.com/google/gops v0.3.5/go.mod h1:pMQgrscwEK/aUSW1IFSaBPbJX82FPHWaSoJw1axQfD0= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ= github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/schema v1.0.2 h1:sAgNfOcNYvdDSrzGHVy9nzCQahG+qmsg+nE8dK85QRA= github.com/gorilla/schema v1.0.2/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= @@ -80,6 +88,7 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= @@ -112,6 +121,7 @@ github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk= github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0= @@ -120,8 +130,13 @@ github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296 h1:8RLq547MSVc6vhO github.com/shazow/ssh-chat v0.0.0-20190125184227-81d7e1686296/go.mod h1:1GLXsL4esywkpNId3v4QWuMf3THtWGitWvtQ/L3aSA4= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/skip2/go-qrcode v0.0.0-20171229120447-cf5f9fa2f0d8/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= +github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= +github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9 h1:lXQ+j+KwZcbwrbgU0Rp4Eglg3EJLHbuZU3BbOqAGBmg= github.com/smartystreets/assertions v0.0.0-20180803164922-886ec427f6b9/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo= @@ -177,14 +192,20 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664 h1:YbZJ76lQ1BqNhVe7dKTSB67wDrc2VPRR75IyGyyPDX8= golang.org/x/crypto v0.0.0-20190130090550-b01c7a725664/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho= golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181212120007-b05ddf57801d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc h1:WiYx1rIFmx8c0mXAFtv5D/mHyKe1+jmuP7PViuwqwuQ= golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -200,3 +221,9 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= +maunium.net/go/maulogger/v2 v2.0.0/go.mod h1:Hbbkq3NV6jvJodByZu1mgEF3fpT7Kz9z0MjEZ3/BusI= +maunium.net/go/mautrix v0.1.0-alpha.3/go.mod h1:GTVu6WDHR+98DKOrYetWsXorvUeKQV3jsSWO6ScbuFI= +maunium.net/go/mautrix-appservice v0.1.0-alpha.3/go.mod h1:wOnWOIuprYad7ly12rHIo3JLCPh4jwvx1prVrAB9RhM= +maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3 h1:A18t5Lp7I3aK0V7B7zdpb0hb/PBlu0X/Ai2AyU/XEk4= +maunium.net/go/mautrix-whatsapp v0.0.0-20190127121751-281b3e8f77f3/go.mod h1:r5E3J4urDEsjfui9OYZYMLBfCliaAqcCwM2xeczta6k=