summaryrefslogtreecommitdiff
path: root/teleirc/matterbridge/bridge/whatsapp
diff options
context:
space:
mode:
Diffstat (limited to 'teleirc/matterbridge/bridge/whatsapp')
-rw-r--r--teleirc/matterbridge/bridge/whatsapp/handlers.go382
-rw-r--r--teleirc/matterbridge/bridge/whatsapp/helpers.go162
-rw-r--r--teleirc/matterbridge/bridge/whatsapp/whatsapp.go341
3 files changed, 885 insertions, 0 deletions
diff --git a/teleirc/matterbridge/bridge/whatsapp/handlers.go b/teleirc/matterbridge/bridge/whatsapp/handlers.go
new file mode 100644
index 0000000..8f9fef1
--- /dev/null
+++ b/teleirc/matterbridge/bridge/whatsapp/handlers.go
@@ -0,0 +1,382 @@
+// nolint:goconst
+package bwhatsapp
+
+import (
+ "fmt"
+ "mime"
+ "strings"
+ "time"
+
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/helper"
+ "github.com/Rhymen/go-whatsapp"
+ "github.com/jpillora/backoff"
+)
+
+/*
+Implement handling messages coming from WhatsApp
+Check:
+- https://github.com/Rhymen/go-whatsapp#add-message-handlers
+- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
+- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
+*/
+
+// HandleError received from WhatsApp
+func (b *Bwhatsapp) HandleError(err error) {
+ // ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
+ // ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
+ if strings.Contains(err.Error(), "error processing data: received invalid data") ||
+ strings.Contains(err.Error(), "invalid string with tag 174") {
+ return
+ }
+
+ switch err.(type) {
+ case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
+ b.reconnect(err)
+ default:
+ switch err {
+ case whatsapp.ErrConnectionTimeout:
+ b.reconnect(err)
+ default:
+ b.Log.Errorf("%v", err)
+ }
+ }
+}
+
+func (b *Bwhatsapp) reconnect(err error) {
+ bf := &backoff.Backoff{
+ Min: time.Second,
+ Max: 5 * time.Minute,
+ Jitter: true,
+ }
+
+ for {
+ d := bf.Duration()
+
+ b.Log.Errorf("Connection failed, underlying error: %v", err)
+ b.Log.Infof("Waiting %s...", d)
+
+ time.Sleep(d)
+
+ b.Log.Info("Reconnecting...")
+
+ err := b.conn.Restore()
+ if err == nil {
+ bf.Reset()
+ b.startedAt = uint64(time.Now().Unix())
+
+ return
+ }
+ }
+}
+
+// HandleTextMessage sent from WhatsApp, relay it to the brige
+func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
+ if message.Info.FromMe {
+ return
+ }
+ // whatsapp sends last messages to show context , cut them
+ if message.Info.Timestamp < b.startedAt {
+ return
+ }
+
+ groupJID := message.Info.RemoteJid
+ senderJID := message.Info.SenderJid
+
+ if len(senderJID) == 0 {
+ if message.Info.Source != nil && message.Info.Source.Participant != nil {
+ senderJID = *message.Info.Source.Participant
+ }
+ }
+
+ // translate sender's JID to the nicest username we can get
+ senderName := b.getSenderName(senderJID)
+ if senderName == "" {
+ senderName = "Someone" // don't expose telephone number
+ }
+
+ extText := message.Info.Source.Message.ExtendedTextMessage
+ if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
+ // handle user mentions
+ for _, mentionedJID := range extText.ContextInfo.MentionedJid {
+ numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
+
+ // mentions comes as telephone numbers and we don't want to expose it to other bridges
+ // replace it with something more meaninful to others
+ mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
+ if mention == "" {
+ mention = "someone"
+ }
+
+ message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
+ }
+ }
+
+ rmsg := config.Message{
+ UserID: senderJID,
+ Username: senderName,
+ Text: message.Text,
+ Channel: groupJID,
+ Account: b.Account,
+ Protocol: b.Protocol,
+ Extra: make(map[string][]interface{}),
+ // ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
+ ID: message.Info.Id,
+ }
+
+ if avatarURL, exists := b.userAvatars[senderJID]; exists {
+ rmsg.Avatar = avatarURL
+ }
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+
+ b.Remote <- rmsg
+}
+
+// HandleImageMessage sent from WhatsApp, relay it to the brige
+// nolint:funlen
+func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
+ if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
+ return
+ }
+
+ senderJID := message.Info.SenderJid
+ if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
+ senderJID = *message.Info.Source.Participant
+ }
+
+ senderName := b.getSenderName(message.Info.SenderJid)
+ if senderName == "" {
+ senderName = "Someone" // don't expose telephone number
+ }
+
+ rmsg := config.Message{
+ UserID: senderJID,
+ Username: senderName,
+ Channel: message.Info.RemoteJid,
+ Account: b.Account,
+ Protocol: b.Protocol,
+ Extra: make(map[string][]interface{}),
+ ID: message.Info.Id,
+ }
+
+ if avatarURL, exists := b.userAvatars[senderJID]; exists {
+ rmsg.Avatar = avatarURL
+ }
+
+ fileExt, err := mime.ExtensionsByType(message.Type)
+ if err != nil {
+ b.Log.Errorf("Mimetype detection error: %s", err)
+
+ return
+ }
+
+ // rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
+ if fileExt[0] == ".jfif" {
+ fileExt[0] = ".jpg"
+ }
+
+ // rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
+ if fileExt[0] == ".jpe" {
+ fileExt[0] = ".jpg"
+ }
+
+ filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
+
+ b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
+
+ data, err := message.Download()
+ if err != nil {
+ b.Log.Errorf("Download image failed: %s", err)
+
+ return
+ }
+
+ // Move file to bridge storage
+ helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+
+ b.Remote <- rmsg
+}
+
+// HandleVideoMessage downloads video messages
+func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
+ if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
+ return
+ }
+
+ senderJID := message.Info.SenderJid
+ if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
+ senderJID = *message.Info.Source.Participant
+ }
+
+ senderName := b.getSenderName(message.Info.SenderJid)
+ if senderName == "" {
+ senderName = "Someone" // don't expose telephone number
+ }
+
+ rmsg := config.Message{
+ UserID: senderJID,
+ Username: senderName,
+ Channel: message.Info.RemoteJid,
+ Account: b.Account,
+ Protocol: b.Protocol,
+ Extra: make(map[string][]interface{}),
+ ID: message.Info.Id,
+ }
+
+ if avatarURL, exists := b.userAvatars[senderJID]; exists {
+ rmsg.Avatar = avatarURL
+ }
+
+ fileExt, err := mime.ExtensionsByType(message.Type)
+ if err != nil {
+ b.Log.Errorf("Mimetype detection error: %s", err)
+
+ return
+ }
+
+ if len(fileExt) == 0 {
+ fileExt = append(fileExt, ".mp4")
+ }
+
+ filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
+
+ b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
+
+ data, err := message.Download()
+ if err != nil {
+ b.Log.Errorf("Download video failed: %s", err)
+
+ return
+ }
+
+ // Move file to bridge storage
+ helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+
+ b.Remote <- rmsg
+}
+
+// HandleAudioMessage downloads audio messages
+func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
+ if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
+ return
+ }
+
+ senderJID := message.Info.SenderJid
+ if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
+ senderJID = *message.Info.Source.Participant
+ }
+
+ senderName := b.getSenderName(message.Info.SenderJid)
+ if senderName == "" {
+ senderName = "Someone" // don't expose telephone number
+ }
+
+ rmsg := config.Message{
+ UserID: senderJID,
+ Username: senderName,
+ Channel: message.Info.RemoteJid,
+ Account: b.Account,
+ Protocol: b.Protocol,
+ Extra: make(map[string][]interface{}),
+ ID: message.Info.Id,
+ }
+
+ if avatarURL, exists := b.userAvatars[senderJID]; exists {
+ rmsg.Avatar = avatarURL
+ }
+
+ fileExt, err := mime.ExtensionsByType(message.Type)
+ if err != nil {
+ b.Log.Errorf("Mimetype detection error: %s", err)
+
+ return
+ }
+
+ if len(fileExt) == 0 {
+ fileExt = append(fileExt, ".ogg")
+ }
+
+ filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
+
+ b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
+
+ data, err := message.Download()
+ if err != nil {
+ b.Log.Errorf("Download audio failed: %s", err)
+
+ return
+ }
+
+ // Move file to bridge storage
+ helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+
+ b.Remote <- rmsg
+}
+
+// HandleDocumentMessage downloads documents
+func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
+ if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
+ return
+ }
+
+ senderJID := message.Info.SenderJid
+ if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
+ senderJID = *message.Info.Source.Participant
+ }
+
+ senderName := b.getSenderName(message.Info.SenderJid)
+ if senderName == "" {
+ senderName = "Someone" // don't expose telephone number
+ }
+
+ rmsg := config.Message{
+ UserID: senderJID,
+ Username: senderName,
+ Channel: message.Info.RemoteJid,
+ Account: b.Account,
+ Protocol: b.Protocol,
+ Extra: make(map[string][]interface{}),
+ ID: message.Info.Id,
+ }
+
+ if avatarURL, exists := b.userAvatars[senderJID]; exists {
+ rmsg.Avatar = avatarURL
+ }
+
+ fileExt, err := mime.ExtensionsByType(message.Type)
+ if err != nil {
+ b.Log.Errorf("Mimetype detection error: %s", err)
+
+ return
+ }
+
+ filename := fmt.Sprintf("%v", message.FileName)
+
+ b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
+
+ data, err := message.Download()
+ if err != nil {
+ b.Log.Errorf("Download document message failed: %s", err)
+
+ return
+ }
+
+ // Move file to bridge storage
+ helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
+
+ b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
+ b.Log.Debugf("<= Message is %#v", rmsg)
+
+ b.Remote <- rmsg
+}
diff --git a/teleirc/matterbridge/bridge/whatsapp/helpers.go b/teleirc/matterbridge/bridge/whatsapp/helpers.go
new file mode 100644
index 0000000..9d39d36
--- /dev/null
+++ b/teleirc/matterbridge/bridge/whatsapp/helpers.go
@@ -0,0 +1,162 @@
+package bwhatsapp
+
+import (
+ "encoding/gob"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
+ "github.com/Rhymen/go-whatsapp"
+)
+
+type ProfilePicInfo struct {
+ URL string `json:"eurl"`
+ Tag string `json:"tag"`
+ Status int16 `json:"status"`
+}
+
+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 (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
+ session := whatsapp.Session{}
+ sessionFile := b.Config.GetString(sessionFile)
+
+ if sessionFile == "" {
+ return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
+ }
+
+ file, err := os.Open(sessionFile)
+ if err != nil {
+ return session, err
+ }
+
+ defer file.Close()
+
+ decoder := gob.NewDecoder(file)
+
+ return session, decoder.Decode(&session)
+}
+
+func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
+ sessionFile := b.Config.GetString(sessionFile)
+
+ if sessionFile == "" {
+ // we already sent a warning while starting the bridge, so let's be quiet here
+ return nil
+ }
+
+ file, err := os.Create(sessionFile)
+ if err != nil {
+ return err
+ }
+
+ defer file.Close()
+
+ encoder := gob.NewEncoder(file)
+
+ return encoder.Encode(session)
+}
+
+func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
+ session, err := b.readSession()
+ if err != nil {
+ b.Log.Warn(err.Error())
+ }
+
+ b.Log.Debugln("Restoring WhatsApp session..")
+
+ session, err = b.conn.RestoreWithSession(session)
+ if err != nil {
+ // restore session connection timed out (I couldn't get over it without logging in again)
+ return nil, errors.New("failed to restore session: " + err.Error())
+ }
+
+ b.Log.Debugln("Session restored successfully!")
+
+ return &session, nil
+}
+
+func (b *Bwhatsapp) getSenderName(senderJid string) string {
+ if sender, exists := b.users[senderJid]; exists {
+ if sender.Name != "" {
+ return sender.Name
+ }
+ // if user is not in phone contacts
+ // it is the most obvious scenario unless you sync your phone contacts with some remote updated source
+ // users can change it in their WhatsApp settings -> profile -> click on Avatar
+ if sender.Notify != "" {
+ return sender.Notify
+ }
+
+ if sender.Short != "" {
+ return sender.Short
+ }
+ }
+
+ // try to reload this contact
+ if _, err := b.conn.Contacts(); err != nil {
+ b.Log.Errorf("error on update of contacts: %v", err)
+ }
+
+ if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
+ // Add it to the user map
+ b.users[senderJid] = contact
+
+ if contact.Name != "" {
+ return contact.Name
+ }
+ // if user is not in phone contacts
+ // same as above
+ return contact.Notify
+ }
+
+ return ""
+}
+
+func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
+ if sender, exists := b.users[senderJid]; exists {
+ return sender.Notify
+ }
+
+ return ""
+}
+
+func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
+ data, err := b.conn.GetProfilePicThumb(jid)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get avatar: %v", err)
+ }
+
+ content := <-data
+ info := &ProfilePicInfo{}
+
+ err = json.Unmarshal([]byte(content), info)
+ if err != nil {
+ return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
+ }
+
+ return info, nil
+}
+
+func isGroupJid(identifier string) bool {
+ return strings.HasSuffix(identifier, "@g.us") ||
+ strings.HasSuffix(identifier, "@temp") ||
+ strings.HasSuffix(identifier, "@broadcast")
+}
diff --git a/teleirc/matterbridge/bridge/whatsapp/whatsapp.go b/teleirc/matterbridge/bridge/whatsapp/whatsapp.go
new file mode 100644
index 0000000..bb0dfe5
--- /dev/null
+++ b/teleirc/matterbridge/bridge/whatsapp/whatsapp.go
@@ -0,0 +1,341 @@
+package bwhatsapp
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "mime"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/42wim/matterbridge/bridge"
+ "github.com/42wim/matterbridge/bridge/config"
+ "github.com/Rhymen/go-whatsapp"
+)
+
+const (
+ // Account config parameters
+ cfgNumber = "Number"
+ qrOnWhiteTerminal = "QrOnWhiteTerminal"
+ sessionFile = "SessionFile"
+)
+
+// Bwhatsapp Bridge structure keeping all the information needed for relying
+type Bwhatsapp struct {
+ *bridge.Config
+
+ session *whatsapp.Session
+ conn *whatsapp.Conn
+ startedAt uint64
+
+ users map[string]whatsapp.Contact
+ userAvatars map[string]string
+}
+
+// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
+func New(cfg *bridge.Config) bridge.Bridger {
+ number := cfg.GetString(cfgNumber)
+
+ cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge")
+ cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info")
+ cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+
+ if number == "" {
+ cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
+ }
+
+ b := &Bwhatsapp{
+ Config: cfg,
+
+ users: make(map[string]whatsapp.Contact),
+ userAvatars: make(map[string]string),
+ }
+
+ return b
+}
+
+// Connect to WhatsApp. Required implementation of the Bridger interface
+func (b *Bwhatsapp) Connect() error {
+ number := b.GetString(cfgNumber)
+ if number == "" {
+ return errors.New("whatsapp's telephone number need to be configured")
+ }
+
+ 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.conn.AddHandler(b)
+ b.Log.Debugln("WhatsApp connection successful")
+
+ // load existing session in order to keep it between restarts
+ b.session, err = b.restoreSession()
+ if err != nil {
+ b.Log.Warn(err.Error())
+ }
+
+ // 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.Contacts()
+ if err != nil {
+ return fmt.Errorf("error on update of contacts: %v", err)
+ }
+
+ // see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
+ for len(b.conn.Store.Contacts) == 0 {
+ b.conn.Contacts() // nolint:errcheck
+
+ <-time.After(1 * time.Second)
+ }
+
+ // map all the users
+ for id, contact := range b.conn.Store.Contacts {
+ if !isGroupJid(id) && id != "status@broadcast" {
+ // it is user
+ b.users[id] = contact
+ }
+ }
+
+ // get user avatar asynchronously
+ go func() {
+ b.Log.Debug("Getting user avatars..")
+
+ for jid := range b.users {
+ info, err := b.GetProfilePicThumb(jid)
+ if err != nil {
+ b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
+ } else {
+ b.Lock()
+ b.userAvatars[jid] = info.URL
+ b.Unlock()
+ }
+ }
+
+ b.Log.Debug("Finished getting avatars..")
+ }()
+
+ return nil
+}
+
+// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
+func (b *Bwhatsapp) Login() error {
+ b.Log.Debugln("Logging in..")
+
+ invert := b.GetBool(qrOnWhiteTerminal) // false is the default
+ qrChan := qrFromTerminal(invert)
+
+ 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 = b.writeSession(session)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
+ }
+
+ return nil
+}
+
+// Disconnect is called while reconnecting to the bridge
+// Required implementation of the Bridger interface
+func (b *Bwhatsapp) Disconnect() error {
+ // We could Logout, but that would close the session completely and would require a new QR code scan
+ // https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
+ return nil
+}
+
+// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
+// Required implementation of the Bridger interface
+// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
+func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
+ byJid := isGroupJid(channel.Name)
+
+ // see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
+ for len(b.conn.Store.Contacts) == 0 {
+ b.conn.Contacts() // nolint:errcheck
+ <-time.After(1 * time.Second)
+ }
+
+ // 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)
+ }
+
+ return nil
+ }
+
+ // 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)
+ }
+ }
+
+ switch len(jids) {
+ case 0:
+ // didn't match any group - print out possibilites
+ for id, contact := range b.conn.Store.Contacts {
+ if isGroupJid(id) {
+ b.Log.Infof("%s %s", contact.Jid, contact.Name)
+ }
+ }
+
+ return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
+ case 1:
+ return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
+ default:
+ return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
+ }
+}
+
+// Post a document message from the bridge to WhatsApp
+func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
+ fi := msg.Extra["file"][0].(config.FileInfo)
+
+ // Post document message
+ message := whatsapp.DocumentMessage{
+ Info: whatsapp.MessageInfo{
+ RemoteJid: msg.Channel,
+ },
+ Title: fi.Name,
+ FileName: fi.Name,
+ Type: filetype,
+ Content: bytes.NewReader(*fi.Data),
+ }
+
+ b.Log.Debugf("=> Sending %#v", msg)
+
+ // create message ID
+ // TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
+ idBytes := make([]byte, 10)
+ if _, err := rand.Read(idBytes); err != nil {
+ b.Log.Warn(err.Error())
+ }
+
+ message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
+ _, err := b.conn.Send(message)
+
+ return message.Info.Id, err
+}
+
+// Post an image message from the bridge to WhatsApp
+// Handle, for sure image/jpeg, image/png and image/gif MIME types
+func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
+ fi := msg.Extra["file"][0].(config.FileInfo)
+
+ // Post image message
+ message := whatsapp.ImageMessage{
+ Info: whatsapp.MessageInfo{
+ RemoteJid: msg.Channel,
+ },
+ Type: filetype,
+ Caption: msg.Username + fi.Comment,
+ Content: bytes.NewReader(*fi.Data),
+ }
+
+ b.Log.Debugf("=> Sending %#v", msg)
+
+ // create message ID
+ // TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
+ idBytes := make([]byte, 10)
+ if _, err := rand.Read(idBytes); err != nil {
+ b.Log.Warn(err.Error())
+ }
+
+ message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
+ _, err := b.conn.Send(message)
+
+ return message.Info.Id, err
+}
+
+// Send a message from the bridge to WhatsApp
+// Required implementation of the Bridger interface
+// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
+func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
+ b.Log.Debugf("=> Receiving %#v", msg)
+
+ // Delete message
+ if msg.Event == config.EventMsgDelete {
+ if msg.ID == "" {
+ // No message ID in case action is executed on a message sent before the bridge was started
+ // and then the bridge cache doesn't have this message ID mapped
+ return "", nil
+ }
+
+ _, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
+
+ return "", err
+ }
+
+ // Edit message
+ if msg.ID != "" {
+ b.Log.Debugf("updating message with id %s", msg.ID)
+
+ if b.GetString("editsuffix") != "" {
+ msg.Text += b.GetString("EditSuffix")
+ } else {
+ msg.Text += " (edited)"
+ }
+ }
+
+ // Handle Upload a file
+ if msg.Extra["file"] != nil {
+ fi := msg.Extra["file"][0].(config.FileInfo)
+ filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
+
+ b.Log.Debugf("Extra file is %#v", filetype)
+
+ // TODO: add different types
+ // TODO: add webp conversion
+ switch filetype {
+ case "image/jpeg", "image/png", "image/gif":
+ return b.PostImageMessage(msg, filetype)
+ default:
+ return b.PostDocumentMessage(msg, filetype)
+ }
+ }
+
+ // Post text message
+ message := whatsapp.TextMessage{
+ Info: whatsapp.MessageInfo{
+ RemoteJid: msg.Channel, // which equals to group id
+ },
+ Text: msg.Username + msg.Text,
+ }
+
+ b.Log.Debugf("=> Sending %#v", msg)
+
+ return b.conn.Send(message)
+}
+
+// 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 ""
+//}