Browse Source

Add Matrix->WhatsApp EDU bridging

Tulir Asokan 6 years ago
parent
commit
bfe5af7edc
5 changed files with 114 additions and 30 deletions
  1. 3 4
      ROADMAP.md
  2. 98 9
      custompuppet.go
  3. 2 0
      puppet.go
  4. 6 4
      user.go
  5. 5 13
      whatsapp-ext/presence.go

+ 3 - 4
ROADMAP.md

@@ -6,9 +6,9 @@
     * [x] Media/files
     * [x] Replies
   * [x] Message redactions
-  * [ ] Presence<sup>[4]</sup>
-  * [ ] Typing notifications<sup>[4]</sup>
-  * [ ] Read receipts<sup>[4]</sup>
+  * [x] Presence
+  * [x] Typing notifications
+  * [x] Read receipts
   * [ ] Power level
   * [ ] Membership actions
     * [ ] Invite
@@ -65,4 +65,3 @@
 <sup>[1]</sup> May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp  
 <sup>[2]</sup> May already work  
 <sup>[3]</sup> May not be possible  
-<sup>[4]</sup> Requires [matrix-org/synapse#2954](https://github.com/matrix-org/synapse/issues/2954) or Matrix puppeting

+ 98 - 9
custompuppet.go

@@ -24,6 +24,7 @@ import (
 
 	"github.com/pkg/errors"
 
+	"github.com/Rhymen/go-whatsapp"
 	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix-appservice"
 )
@@ -77,30 +78,38 @@ func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
 	return ia, nil
 }
 
+func (puppet *Puppet) clearCustomMXID() {
+	puppet.CustomMXID = ""
+	puppet.AccessToken = ""
+	puppet.customIntent = nil
+	puppet.customTypingIn = nil
+	puppet.customUser = nil
+}
+
 func (puppet *Puppet) StartCustomMXID() error {
 	if len(puppet.CustomMXID) == 0 {
+		puppet.clearCustomMXID()
 		return nil
 	}
 	intent, err := puppet.newCustomIntent()
 	if err != nil {
-		puppet.CustomMXID = ""
-		puppet.AccessToken = ""
+		puppet.clearCustomMXID()
 		return err
 	}
 	urlPath := intent.BuildURL("account", "whoami")
 	var resp struct{ UserID string `json:"user_id"` }
 	_, err = intent.MakeRequest("GET", urlPath, nil, &resp)
 	if err != nil {
-		puppet.CustomMXID = ""
-		puppet.AccessToken = ""
+		puppet.clearCustomMXID()
 		return err
 	}
 	if resp.UserID != puppet.CustomMXID {
-		puppet.CustomMXID = ""
-		puppet.AccessToken = ""
+		puppet.clearCustomMXID()
 		return ErrMismatchingMXID
 	}
 	puppet.customIntent = intent
+	puppet.customTypingIn = make(map[string]bool)
+	puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
 	puppet.startSyncing()
 	return nil
 }
@@ -111,6 +120,7 @@ func (puppet *Puppet) startSyncing() {
 	}
 	go func() {
 		puppet.log.Debugln("Starting syncing...")
+		puppet.customIntent.SyncPresence = "offline"
 		err := puppet.customIntent.Sync()
 		if err != nil {
 			puppet.log.Errorln("Fatal error syncing:", err)
@@ -126,12 +136,91 @@ func (puppet *Puppet) stopSyncing() {
 }
 
 func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error {
-	d, _ := json.Marshal(resp)
-	puppet.log.Debugln("Sync data:", string(d), since)
-	// TODO handle sync data
+	if !puppet.customUser.Connected {
+		return fmt.Errorf("custom user not connected to whatsapp")
+	}
+	for roomID, events := range resp.Rooms.Join {
+		portal := puppet.bridge.GetPortalByMXID(roomID)
+		if portal == nil {
+			continue
+		}
+		for _, event := range events.Ephemeral.Events {
+			switch event.Type {
+			case mautrix.EphemeralEventReceipt:
+				go puppet.handleReceiptEvent(portal, event)
+			case mautrix.EphemeralEventTyping:
+				go puppet.handleTypingEvent(portal, event)
+			}
+		}
+	}
+	for _, event := range resp.Presence.Events {
+		if event.Sender != puppet.CustomMXID {
+			continue
+		}
+		go puppet.handlePresenceEvent(event)
+	}
 	return nil
 }
 
+func (puppet *Puppet) handlePresenceEvent(event *mautrix.Event) {
+	presence := whatsapp.PresenceAvailable
+	if event.Content.Raw["presence"].(string) != "online" {
+		presence = whatsapp.PresenceUnavailable
+		puppet.customUser.log.Infoln("Marking offline")
+	} else {
+		puppet.customUser.log.Infoln("Marking online")
+	}
+	_, err := puppet.customUser.Conn.Presence("", presence)
+	if err != nil {
+		puppet.customUser.log.Warnln("Failed to set presence:", err)
+	}
+}
+
+func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *mautrix.Event) {
+	for eventID, rawReceipts := range event.Content.Raw {
+		if receipts, ok := rawReceipts.(map[string]interface{}); !ok {
+			continue
+		} else if readReceipt, ok := receipts["m.read"].(map[string]interface{}); !ok {
+			continue
+		} else if _, ok = readReceipt[puppet.CustomMXID].(map[string]interface{}); !ok {
+			continue
+		}
+		message := puppet.bridge.DB.Message.GetByMXID(eventID)
+		if message == nil {
+			continue
+		}
+		puppet.customUser.log.Infofln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
+		_, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
+		if err != nil {
+			puppet.customUser.log.Warnln("Error marking read:", err)
+		}
+	}
+}
+
+func (puppet *Puppet) handleTypingEvent(portal *Portal, event *mautrix.Event) {
+	isTyping := false
+	for _, userID := range event.Content.TypingUserIDs {
+		if userID == puppet.CustomMXID {
+			isTyping = true
+			break
+		}
+	}
+	if puppet.customTypingIn[event.RoomID] != isTyping {
+		puppet.customTypingIn[event.RoomID] = isTyping
+		presence := whatsapp.PresenceComposing
+		if !isTyping {
+			puppet.customUser.log.Infofln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
+			presence = whatsapp.PresencePaused
+		} else {
+			puppet.customUser.log.Infofln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
+		}
+		_, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
+		if err != nil {
+			puppet.customUser.log.Warnln("Error setting typing:", err)
+		}
+	}
+}
+
 func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) {
 	puppet.log.Warnln("Sync error:", err)
 	return 10 * time.Second, nil

+ 2 - 0
puppet.go

@@ -147,6 +147,8 @@ type Puppet struct {
 	MXID types.MatrixUserID
 
 	customIntent *appservice.IntentAPI
+	customTypingIn map[string]bool
+	customUser *User
 }
 
 func (puppet *Puppet) PhoneNumber() string {

+ 6 - 4
user.go

@@ -344,7 +344,9 @@ func (user *User) updateLastConnectionIfNecessary() {
 }
 
 func (user *User) HandleError(err error) {
-	user.log.Errorln("WhatsApp error:", err)
+	if err != whatsapp.ErrInvalidWsData {
+		user.log.Errorln("WhatsApp error:", err)
+	}
 	var msg string
 	if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok {
 		user.Connected = false
@@ -464,9 +466,9 @@ func (user *User) HandleMessageRevoke(message whatsappExt.MessageRevocation) {
 func (user *User) HandlePresence(info whatsappExt.Presence) {
 	puppet := user.bridge.GetPuppetByJID(info.SenderJID)
 	switch info.Status {
-	case whatsappExt.PresenceUnavailable:
+	case whatsapp.PresenceUnavailable:
 		_ = puppet.DefaultIntent().SetPresence("offline")
-	case whatsappExt.PresenceAvailable:
+	case whatsapp.PresenceAvailable:
 		if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
 			portal := user.bridge.GetPortalByMXID(puppet.typingIn)
 			_, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0)
@@ -475,7 +477,7 @@ func (user *User) HandlePresence(info whatsappExt.Presence) {
 		} else {
 			_ = puppet.DefaultIntent().SetPresence("online")
 		}
-	case whatsappExt.PresenceComposing:
+	case whatsapp.PresenceComposing:
 		portal := user.GetPortalByJID(info.JID)
 		if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
 			if puppet.typingIn == portal.MXID {

+ 5 - 13
whatsapp-ext/presence.go

@@ -23,20 +23,12 @@ import (
 	"github.com/Rhymen/go-whatsapp"
 )
 
-type PresenceType string
-
-const (
-	PresenceUnavailable PresenceType = "unavailable"
-	PresenceAvailable   PresenceType = "available"
-	PresenceComposing   PresenceType = "composing"
-)
-
 type Presence struct {
-	JID       string       `json:"id"`
-	SenderJID string       `json:"participant"`
-	Status    PresenceType `json:"type"`
-	Timestamp int64        `json:"t"`
-	Deny      bool         `json:"deny"`
+	JID       string            `json:"id"`
+	SenderJID string            `json:"participant"`
+	Status    whatsapp.Presence `json:"type"`
+	Timestamp int64             `json:"t"`
+	Deny      bool              `json:"deny"`
 }
 
 type PresenceHandler interface {