diff --git a/ROADMAP.md b/ROADMAP.md
index 3d53bb5..65cc9d0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -6,9 +6,9 @@
* [x] Media/files
* [x] Replies
* [x] Message redactions
- * [ ] Presence[4]
- * [ ] Typing notifications[4]
- * [ ] Read receipts[4]
+ * [x] Presence
+ * [x] Typing notifications
+ * [x] Read receipts
* [ ] Power level
* [ ] Membership actions
* [ ] Invite
@@ -65,4 +65,3 @@
[1] May involve reverse-engineering the WhatsApp Web API and/or editing go-whatsapp
[2] May already work
[3] May not be possible
-[4] Requires [matrix-org/synapse#2954](https://github.com/matrix-org/synapse/issues/2954) or Matrix puppeting
diff --git a/custompuppet.go b/custompuppet.go
index e7e68c8..5b31438 100644
--- a/custompuppet.go
+++ b/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
diff --git a/puppet.go b/puppet.go
index 2c3c2d1..28361b4 100644
--- a/puppet.go
+++ b/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 {
diff --git a/user.go b/user.go
index 7f9e69a..586c22c 100644
--- a/user.go
+++ b/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 {
diff --git a/whatsapp-ext/presence.go b/whatsapp-ext/presence.go
index 12808e2..16ec982 100644
--- a/whatsapp-ext/presence.go
+++ b/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 {