diff --git a/bridge/whatsapp/handlers.go b/bridge/whatsapp/handlers.go index f0512a74..ff905b04 100644 --- a/bridge/whatsapp/handlers.go +++ b/bridge/whatsapp/handlers.go @@ -1,330 +1,332 @@ package bwhatsapp import ( "fmt" "mime" "strings" "github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/helper" "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" ) // nolint:gocritic func (b *Bwhatsapp) eventHandler(evt interface{}) { switch e := evt.(type) { case *events.Message: b.handleMessage(e) } } func (b *Bwhatsapp) handleMessage(message *events.Message) { msg := message.Message switch { case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt): return } + b.Log.Infof("Receiving message %#v", msg) + switch { case msg.Conversation != nil || msg.ExtendedTextMessage != nil: b.handleTextMessage(message.Info, msg) case msg.VideoMessage != nil: b.handleVideoMessage(message) case msg.AudioMessage != nil: b.handleAudioMessage(message) case msg.DocumentMessage != nil: b.handleDocumentMessage(message) case msg.ImageMessage != nil: b.handleImageMessage(message) } } func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) { senderJID := messageInfo.Sender senderName := b.getSenderName(messageInfo.Sender) if senderName == "" { senderName = "Someone" // don't expose telephone number } if msg.GetExtendedTextMessage() == nil { return } text := msg.GetExtendedTextMessage().GetText() ci := msg.GetExtendedTextMessage().GetContextInfo() if senderJID == (types.JID{}) && ci.Participant != nil { senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) } if ci.MentionedJid != nil { // handle user mentions for _, mentionedJID := range ci.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(types.NewJID(numberAndSuffix[0], types.DefaultUserServer)) if mention == "" { mention = "someone" } text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1) } } rmsg := config.Message{ UserID: senderJID.String(), Username: senderName, Text: msg.GetExtendedTextMessage().GetText(), Channel: ci.GetRemoteJid(), Account: b.Account, Protocol: b.Protocol, Extra: make(map[string][]interface{}), // ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string ID: messageInfo.ID, } if avatarURL, exists := b.userAvatars[senderJID.String()]; 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 func (b *Bwhatsapp) handleImageMessage(msg *events.Message) { imsg := msg.Message.GetImageMessage() senderJID := msg.Info.Sender senderName := b.getSenderName(senderJID) ci := imsg.GetContextInfo() if senderJID == (types.JID{}) && ci.Participant != nil { senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) } rmsg := config.Message{ UserID: senderJID.String(), Username: senderName, Channel: ci.GetRemoteJid(), Account: b.Account, Protocol: b.Protocol, Extra: make(map[string][]interface{}), ID: msg.Info.ID, } if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { rmsg.Avatar = avatarURL } fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) 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", msg.Info.ID, fileExt[0]) b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype()) data, err := b.wc.Download(imsg) if err != nil { b.Log.Errorf("Download image failed: %s", err) return } // Move file to bridge storage helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &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(msg *events.Message) { imsg := msg.Message.GetVideoMessage() senderJID := msg.Info.Sender senderName := b.getSenderName(senderJID) ci := imsg.GetContextInfo() if senderJID == (types.JID{}) && ci.Participant != nil { senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) } rmsg := config.Message{ UserID: senderJID.String(), Username: senderName, Channel: ci.GetRemoteJid(), Account: b.Account, Protocol: b.Protocol, Extra: make(map[string][]interface{}), ID: msg.Info.ID, } if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { rmsg.Avatar = avatarURL } fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) 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", msg.Info.ID, fileExt[0]) b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype()) data, err := b.wc.Download(imsg) if err != nil { b.Log.Errorf("Download video failed: %s", err) return } // Move file to bridge storage helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &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(msg *events.Message) { imsg := msg.Message.GetAudioMessage() senderJID := msg.Info.Sender senderName := b.getSenderName(senderJID) ci := imsg.GetContextInfo() if senderJID == (types.JID{}) && ci.Participant != nil { senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) } rmsg := config.Message{ UserID: senderJID.String(), Username: senderName, Channel: ci.GetRemoteJid(), Account: b.Account, Protocol: b.Protocol, Extra: make(map[string][]interface{}), ID: msg.Info.ID, } if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { rmsg.Avatar = avatarURL } fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) 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", msg.Info.ID, fileExt[0]) b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype()) data, err := b.wc.Download(imsg) if err != nil { b.Log.Errorf("Download video 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(msg *events.Message) { imsg := msg.Message.GetDocumentMessage() senderJID := msg.Info.Sender senderName := b.getSenderName(senderJID) ci := imsg.GetContextInfo() if senderJID == (types.JID{}) && ci.Participant != nil { senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer) } rmsg := config.Message{ UserID: senderJID.String(), Username: senderName, Channel: ci.GetRemoteJid(), Account: b.Account, Protocol: b.Protocol, Extra: make(map[string][]interface{}), ID: msg.Info.ID, } if avatarURL, exists := b.userAvatars[senderJID.String()]; exists { rmsg.Avatar = avatarURL } fileExt, err := mime.ExtensionsByType(imsg.GetMimetype()) if err != nil { b.Log.Errorf("Mimetype detection error: %s", err) return } filename := fmt.Sprintf("%v", imsg.GetFileName()) b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype()) data, err := b.wc.Download(imsg) 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/bridge/whatsapp/whatsapp.go b/bridge/whatsapp/whatsapp.go index f4d73534..c28b3c89 100644 --- a/bridge/whatsapp/whatsapp.go +++ b/bridge/whatsapp/whatsapp.go @@ -1,319 +1,319 @@ package bwhatsapp import ( "context" "errors" "fmt" "mime" "os" "path/filepath" "time" "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" "github.com/mdp/qrterminal" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/binary/proto" "go.mau.fi/whatsmeow/types" waLog "go.mau.fi/whatsmeow/util/log" goproto "google.golang.org/protobuf/proto" _ "modernc.org/sqlite" // needed for sqlite ) const ( // Account config parameters cfgNumber = "Number" ) // Bwhatsapp Bridge structure keeping all the information needed for relying type Bwhatsapp struct { *bridge.Config startedAt time.Time wc *whatsmeow.Client contacts map[types.JID]types.ContactInfo users map[string]types.ContactInfo userAvatars map[string]string } // New Create a new WhatsApp bridge. This will be called for each [whatsapp.] entry you have in the config file func New(cfg *bridge.Config) bridge.Bridger { number := cfg.GetString(cfgNumber) if number == "" { cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number") } b := &Bwhatsapp{ Config: cfg, users: make(map[string]types.ContactInfo), userAvatars: make(map[string]string), } return b } // Connect to WhatsApp. Required implementation of the Bridger interface func (b *Bwhatsapp) Connect() error { device, err := b.getDevice() if err != nil { return err } number := b.GetString(cfgNumber) if number == "" { return errors.New("whatsapp's telephone number need to be configured") } b.Log.Debugln("Connecting to WhatsApp..") b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true)) b.wc.AddEventHandler(b.eventHandler) // No ID stored, new login qrChan, err := b.wc.GetQRChannel(context.Background()) // This error means that we're already logged in, so ignore it. if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) { return errors.New("failed to to get QR channel:" + err.Error()) } err = b.wc.Connect() if err != nil { return errors.New("failed to connect to WhatsApp: " + err.Error()) } for evt := range qrChan { if evt.Event == "code" { // Render the QR code here qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) } else { b.Log.Infof("QR channel result: %s", evt.Event) } } b.Log.Infoln("WhatsApp connection successful") b.contacts, err = b.wc.Store.Contacts.GetAllContacts() if err != nil { return errors.New("failed to get contacts: " + err.Error()) } b.startedAt = time.Now() // map all the users for id, contact := range b.contacts { if !isGroupJid(id.String()) && id.String() != "status@broadcast" { // it is user b.users[id.String()] = 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() - } + // go func() { + b.Log.Info("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..") - }() + b.Log.Info("Finished getting avatars..") + // }() return nil } // Disconnect is called while reconnecting to the bridge // Required implementation of the Bridger interface func (b *Bwhatsapp) Disconnect() error { b.wc.Disconnect() 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) groups, err := b.wc.GetJoinedGroups() if err != nil { return err } // verify if we are member of the given group if byJid { gJID, err := types.ParseJID(channel.Name) if err != nil { return err } for _, group := range groups { if group.JID == gJID { return nil } } } foundGroups := []string{} for _, group := range groups { if group.Name == channel.Name { foundGroups = append(foundGroups, group.Name) } } switch len(foundGroups) { case 0: // didn't match any group - print out possibilites for _, group := range groups { b.Log.Infof("%s %s", group.JID, group.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\"", foundGroups[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, foundGroups) } } // Post a document message from the bridge to WhatsApp func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) { groupJID, _ := types.ParseJID(msg.Channel) fi := msg.Extra["file"][0].(config.FileInfo) resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument) if err != nil { return "", err } // Post document message var message proto.Message message.DocumentMessage = &proto.DocumentMessage{ Title: &fi.Name, FileName: &fi.Name, Mimetype: &filetype, MediaKey: resp.MediaKey, FileEncSha256: resp.FileEncSHA256, FileSha256: resp.FileSHA256, FileLength: goproto.Uint64(resp.FileLength), Url: &resp.URL, } b.Log.Debugf("=> Sending %#v", msg) ID := whatsmeow.GenerateMessageID() _, err = b.wc.SendMessage(groupJID, ID, &message) return 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) { groupJID, _ := types.ParseJID(msg.Channel) fi := msg.Extra["file"][0].(config.FileInfo) caption := msg.Username + fi.Comment resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage) if err != nil { return "", err } var message proto.Message message.ImageMessage = &proto.ImageMessage{ Mimetype: &filetype, Caption: &caption, MediaKey: resp.MediaKey, FileEncSha256: resp.FileEncSHA256, FileSha256: resp.FileSHA256, FileLength: goproto.Uint64(resp.FileLength), Url: &resp.URL, } b.Log.Debugf("=> Sending %#v", msg) ID := whatsmeow.GenerateMessageID() _, err = b.wc.SendMessage(groupJID, ID, &message) return ID, err } // Send a message from the bridge to WhatsApp func (b *Bwhatsapp) Send(msg config.Message) (string, error) { groupJID, _ := types.ParseJID(msg.Channel) 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.wc.RevokeMessage(groupJID, msg.ID) 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) } } var message proto.Message text := msg.Username + msg.Text message.ExtendedTextMessage = &proto.ExtendedTextMessage{ Text: &text, } b.Log.Debugf("=> Sending %#v", msg) ID := whatsmeow.GenerateMessageID() _, err := b.wc.SendMessage(groupJID, ID, &message) return ID, err }