Add WhatsApp->Matrix read receipts and phone connection notifications

This commit is contained in:
Tulir Asokan 2018-08-24 19:46:14 +03:00
parent 60529bf022
commit 1f87deb317
12 changed files with 178 additions and 69 deletions

View file

@ -28,7 +28,7 @@
* [x] Avatars * [x] Avatars
* [ ] Presence * [ ] Presence
* [ ] Typing notifications * [ ] Typing notifications
* [ ] Read receipts * [x] Read receipts
* [ ] Admin/superadmin status * [ ] Admin/superadmin status
* [ ] Membership actions * [ ] Membership actions
* [ ] Invite * [ ] Invite

View file

@ -59,7 +59,7 @@ func (mq *MessageQuery) GetAll(owner types.MatrixUserID) (messages []*Message) {
} }
func (mq *MessageQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppMessageID) *Message { func (mq *MessageQuery) GetByJID(owner types.MatrixUserID, jid types.WhatsAppMessageID) *Message {
return mq.get("SELECT * FROM message WHERE jid=?", jid) return mq.get("SELECT * FROM message WHERE owner=? AND jid=?", owner, jid)
} }
func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message { func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message {

View file

@ -93,7 +93,7 @@ func (puppet *Puppet) Insert() error {
_, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)", _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)",
puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar) puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar)
if err != nil { if err != nil {
puppet.log.Errorln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err) puppet.log.Errorfln("Failed to insert %s->%s: %v", puppet.JID, puppet.Receiver, err)
} }
return err return err
} }
@ -103,7 +103,7 @@ func (puppet *Puppet) Update() error {
puppet.Displayname, puppet.Avatar, puppet.Displayname, puppet.Avatar,
puppet.JID, puppet.Receiver) puppet.JID, puppet.Receiver)
if err != nil { if err != nil {
puppet.log.Errorln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err) puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, puppet.Receiver, err)
} }
return err return err
} }

View file

@ -17,21 +17,24 @@
package main package main
import ( import (
"maunium.net/go/mautrix-whatsapp/database"
log "maunium.net/go/maulogger"
"fmt"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/gomatrix"
"strings"
"maunium.net/go/mautrix-appservice"
"github.com/Rhymen/go-whatsapp"
"sync"
"net/http"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
"mime"
"image"
"bytes" "bytes"
"encoding/hex"
"fmt"
"image"
"math/rand"
"mime"
"net/http"
"strings"
"sync"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/gomatrix"
"maunium.net/go/gomatrix/format" "maunium.net/go/gomatrix/format"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
@ -221,7 +224,7 @@ func (portal *Portal) CreateMatrixRoom() error {
name := portal.Name name := portal.Name
topic := portal.Topic topic := portal.Topic
isPrivateChat := false isPrivateChat := false
if strings.HasSuffix(portal.JID, "s.whatsapp.net") { if strings.HasSuffix(portal.JID, whatsapp_ext.NewUserSuffix) {
puppet := portal.user.GetPuppetByJID(portal.JID) puppet := portal.user.GetPuppetByJID(portal.JID)
name = puppet.Displayname name = puppet.Displayname
topic = "WhatsApp private chat" topic = "WhatsApp private chat"
@ -244,7 +247,7 @@ func (portal *Portal) CreateMatrixRoom() error {
} }
func (portal *Portal) IsPrivateChat() bool { func (portal *Portal) IsPrivateChat() bool {
return strings.HasSuffix(portal.JID, puppetJIDStrippedSuffix) return strings.HasSuffix(portal.JID, whatsapp_ext.NewUserSuffix)
} }
func (portal *Portal) MainIntent() *appservice.IntentAPI { func (portal *Portal) MainIntent() *appservice.IntentAPI {
@ -282,13 +285,18 @@ func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.In
return puppet.Intent() return puppet.Intent()
} }
func (portal *Portal) GetRelations(info whatsapp.MessageInfo) (reply gomatrix.RelatesTo) { func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) {
if len(info.QuotedMessageID) == 0 { if len(info.QuotedMessageID) == 0 {
return return
} }
message := portal.bridge.DB.Message.GetByJID(portal.Owner, info.QuotedMessageID) message := portal.bridge.DB.Message.GetByJID(portal.Owner, info.QuotedMessageID)
if message != nil { if message != nil {
reply.InReplyTo.EventID = message.MXID event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
if err != nil {
portal.log.Warnln("Failed to get reply target:", err)
return
}
content.SetReply(event)
} }
return return
@ -299,18 +307,24 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
return return
} }
portal.CreateMatrixRoom() err := portal.CreateMatrixRoom()
if err != nil {
portal.log.Errorln("Failed to create portal room:", err)
return
}
intent := portal.GetMessageIntent(message.Info) intent := portal.GetMessageIntent(message.Info)
if intent == nil { if intent == nil {
return return
} }
resp, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, gomatrix.Content{ content := gomatrix.Content{
Body: message.Text, Body: message.Text,
MsgType: gomatrix.MsgText, MsgType: gomatrix.MsgText,
RelatesTo: portal.GetRelations(message.Info), }
}, int64(message.Info.Timestamp*1000)) portal.SetReply(&content, message.Info)
resp, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, content, int64(message.Info.Timestamp*1000))
if err != nil { if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return return
@ -324,7 +338,11 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
return return
} }
portal.CreateMatrixRoom() err := portal.CreateMatrixRoom()
if err != nil {
portal.log.Errorln("Failed to create portal room:", err)
return
}
intent := portal.GetMessageIntent(info) intent := portal.GetMessageIntent(info)
if intent == nil { if intent == nil {
@ -353,12 +371,12 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
content := gomatrix.Content{ content := gomatrix.Content{
Body: caption, Body: caption,
URL: uploaded.ContentURI, URL: uploaded.ContentURI,
Info: gomatrix.FileInfo{ Info: &gomatrix.FileInfo{
Size: len(data), Size: len(data),
MimeType: mimeType, MimeType: mimeType,
}, },
RelatesTo: portal.GetRelations(info),
} }
portal.SetReply(&content, info)
if thumbnail != nil { if thumbnail != nil {
thumbnailMime := http.DetectContentType(thumbnail) thumbnailMime := http.DetectContentType(thumbnail)
@ -422,6 +440,12 @@ var htmlParser = format.HTMLParser{
}, },
} }
func makeMessageID() string {
b := make([]byte, 10)
rand.Read(b)
return strings.ToUpper(hex.EncodeToString(b))
}
func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) { func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
var err error var err error
switch evt.Content.MsgType { switch evt.Content.MsgType {
@ -430,18 +454,21 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
if evt.Content.Format == gomatrix.FormatHTML { if evt.Content.Format == gomatrix.FormatHTML {
text = htmlParser.Parse(evt.Content.FormattedBody) text = htmlParser.Parse(evt.Content.FormattedBody)
} }
id := makeMessageID()
err = portal.user.Conn.Send(whatsapp.TextMessage{ err = portal.user.Conn.Send(whatsapp.TextMessage{
Text: text, Text: text,
Info: whatsapp.MessageInfo{ Info: whatsapp.MessageInfo{
Id: id,
RemoteJid: portal.JID, RemoteJid: portal.JID,
}, },
}) })
portal.MarkHandled(id, evt.ID)
default: default:
portal.log.Debugln("Unhandled Matrix event:", evt) portal.log.Debugln("Unhandled Matrix event:", evt)
return return
} }
if err != nil { if err != nil {
portal.log.Errorln("Error handling Matrix event %s: %v", evt.ID, err) portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
} else { } else {
portal.log.Debugln("Handled Matrix event:", evt) portal.log.Debugln("Handled Matrix event:", evt)
} }

View file

@ -17,18 +17,18 @@
package main package main
import ( import (
"maunium.net/go/mautrix-whatsapp/database"
log "maunium.net/go/maulogger"
"fmt" "fmt"
"regexp"
"maunium.net/go/mautrix-whatsapp/types"
"strings"
"maunium.net/go/mautrix-appservice"
"github.com/Rhymen/go-whatsapp"
"net/http" "net/http"
) "regexp"
"strings"
const puppetJIDStrippedSuffix = "@s.whatsapp.net" "github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
)
func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) { func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
@ -47,7 +47,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUser
receiver = strings.Replace(receiver, "=40", "@", 1) receiver = strings.Replace(receiver, "=40", "@", 1)
colonIndex := strings.LastIndex(receiver, "=3") colonIndex := strings.LastIndex(receiver, "=3")
receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):] receiver = receiver[:colonIndex] + ":" + receiver[colonIndex+len("=3"):]
jid := types.WhatsAppID(match[2] + puppetJIDStrippedSuffix) jid := types.WhatsAppID(match[2] + whatsapp_ext.NewUserSuffix)
return receiver, jid, true return receiver, jid, true
} }
@ -120,7 +120,7 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
dbPuppet.Receiver, dbPuppet.Receiver,
strings.Replace( strings.Replace(
dbPuppet.JID, dbPuppet.JID,
puppetJIDStrippedSuffix, "", 1)), whatsapp_ext.NewUserSuffix, "", 1)),
user.bridge.Config.Homeserver.Domain), user.bridge.Config.Homeserver.Domain),
} }
} }

66
user.go
View file

@ -17,14 +17,16 @@
package main package main
import ( import (
"maunium.net/go/mautrix-whatsapp/database" "fmt"
"github.com/Rhymen/go-whatsapp"
"time"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types"
"strings" "strings"
"sync" "sync"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
@ -186,7 +188,7 @@ func (user *User) Sync() {
user.log.Debugln("Syncing...") user.log.Debugln("Syncing...")
user.Conn.Contacts() user.Conn.Contacts()
for jid, contact := range user.Conn.Store.Contacts { for jid, contact := range user.Conn.Store.Contacts {
if strings.HasSuffix(jid, puppetJIDStrippedSuffix) { if strings.HasSuffix(jid, whatsapp_ext.NewUserSuffix) {
puppet := user.GetPuppetByJID(contact.Jid) puppet := user.GetPuppetByJID(contact.Jid)
puppet.Sync(contact) puppet.Sync(contact)
} }
@ -205,6 +207,10 @@ func (user *User) HandleError(err error) {
user.log.Errorln("WhatsApp error:", err) user.log.Errorln("WhatsApp error:", err)
} }
func (user *User) HandleJSONParseError(err error) {
user.log.Errorln("WhatsApp JSON parse error:", err)
}
func (user *User) HandleTextMessage(message whatsapp.TextMessage) { func (user *User) HandleTextMessage(message whatsapp.TextMessage) {
user.log.Debugln("Received text message:", message) user.log.Debugln("Received text message:", message)
portal := user.GetPortalByJID(message.Info.RemoteJid) portal := user.GetPortalByJID(message.Info.RemoteJid)
@ -231,6 +237,52 @@ func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Title) portal.HandleMediaMessage(message.Download, message.Thumbnail, message.Info, message.Type, message.Title)
} }
func (user *User) HandleStreamEvent(stream whatsapp_ext.StreamEvent) {
if len(user.ManagementRoom) == 0 {
return
}
switch stream.Type {
case whatsapp_ext.StreamSleep:
user.bridge.AppService.BotIntent().SendNotice(user.ManagementRoom, "WhatsApp client disconnected.")
case whatsapp_ext.StreamUpdate:
if user.Conn.Info != nil && user.Conn.Info.Phone != nil {
user.bridge.AppService.BotIntent().SendNotice(user.ManagementRoom,
fmt.Sprintf("WhatsApp v%s client connected from %s %s (OS v%s).",
user.Conn.Info.Phone.WaVersion, user.Conn.Info.Phone.DeviceManufacturer, user.Conn.Info.Phone.DeviceModel, user.Conn.Info.Phone.OsVersion))
}
}
}
func (user *User) HandleConnInfo(info whatsapp_ext.ConnInfo) {
if len(user.ManagementRoom) > 0 && len(info.ProtocolVersion) > 0 {
user.bridge.AppService.BotIntent().SendNotice(user.ManagementRoom,
fmt.Sprintf("WhatsApp v%s client connected from %s %s (OS v%s).",
info.Phone.WhatsAppVersion, info.Phone.DeviceManufacturer, info.Phone.DeviceModel, info.Phone.OSVersion))
}
}
func (user *User) HandleMsgInfo(info whatsapp_ext.MsgInfo) {
if (info.Command == whatsapp_ext.MsgInfoCommandAck || info.Command == whatsapp_ext.MsgInfoCommandAcks) && info.Acknowledgement == whatsapp_ext.AckMessageRead {
portal := user.GetPortalByJID(info.ToJID)
if len(portal.MXID) == 0 {
return
}
intent := user.GetPuppetByJID(info.SenderJID).Intent()
user.log.Debugln(info.IDs)
for _, id := range info.IDs {
msg := user.bridge.DB.Message.GetByJID(user.ID, id)
if msg == nil {
continue
}
err := intent.MarkRead(portal.MXID, msg.MXID)
if err != nil {
user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err)
}
}
}
}
func (user *User) HandleJsonMessage(message string) { func (user *User) HandleJsonMessage(message string) {
user.log.Debugln("JSON message:", message) user.log.Debugln("JSON message:", message)
} }

View file

@ -17,8 +17,9 @@
package whatsapp_ext package whatsapp_ext
import ( import (
"github.com/Rhymen/go-whatsapp"
"encoding/json" "encoding/json"
"github.com/Rhymen/go-whatsapp"
) )
type ConnInfo struct { type ConnInfo struct {
@ -26,8 +27,8 @@ type ConnInfo struct {
BinaryVersion int `json:"binVersion"` BinaryVersion int `json:"binVersion"`
Phone struct { Phone struct {
WhatsAppVersion string `json:"wa_version"` WhatsAppVersion string `json:"wa_version"`
MCC int `json:"mcc"` MCC string `json:"mcc"`
MNC int `json:"mnc"` MNC string `json:"mnc"`
OSVersion string `json:"os_version"` OSVersion string `json:"os_version"`
DeviceManufacturer string `json:"device_manufacturer"` DeviceManufacturer string `json:"device_manufacturer"`
DeviceModel string `json:"device_model"` DeviceModel string `json:"device_model"`

View file

@ -18,6 +18,7 @@ package whatsapp_ext
import ( import (
"encoding/json" "encoding/json"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
) )
@ -27,6 +28,7 @@ type JSONMessageType string
const ( const (
MessageMsgInfo JSONMessageType = "MsgInfo" MessageMsgInfo JSONMessageType = "MsgInfo"
MessageMsg JSONMessageType = "Msg"
MessagePresence JSONMessageType = "Presence" MessagePresence JSONMessageType = "Presence"
MessageStream JSONMessageType = "Stream" MessageStream JSONMessageType = "Stream"
MessageConn JSONMessageType = "Conn" MessageConn JSONMessageType = "Conn"
@ -76,11 +78,11 @@ func (ext *ExtendedConn) HandleJsonMessage(message string) {
case MessageStream: case MessageStream:
ext.handleMessageStream(msg[1:]) ext.handleMessageStream(msg[1:])
case MessageConn: case MessageConn:
ext.handleMessageProps(msg[1]) ext.handleMessageConn(msg[1])
case MessageProps: case MessageProps:
ext.handleMessageProps(msg[1]) ext.handleMessageProps(msg[1])
case MessageMsgInfo: case MessageMsgInfo, MessageMsg:
ext.handleMessageMsgInfo(msg[1]) ext.handleMessageMsgInfo(msgType, msg[1])
default: default:
for _, handler := range ext.handlers { for _, handler := range ext.handlers {
ujmHandler, ok := handler.(UnhandledJSONMessageHandler) ujmHandler, ok := handler.(UnhandledJSONMessageHandler)

View file

@ -17,25 +17,49 @@
package whatsapp_ext package whatsapp_ext
import ( import (
"github.com/Rhymen/go-whatsapp"
"encoding/json" "encoding/json"
"strings" "strings"
"github.com/Rhymen/go-whatsapp"
) )
type MsgInfoCommand string type MsgInfoCommand string
const ( const (
MsgInfoCommandAcknowledge MsgInfoCommand = "ack" MsgInfoCommandAck MsgInfoCommand = "ack"
MsgInfoCommandAcks MsgInfoCommand = "acks"
) )
type Acknowledgement int
const (
AckMessageSent Acknowledgement = 1
AckMessageDelivered Acknowledgement = 2
AckMessageRead Acknowledgement = 3
)
type JSONStringOrArray []string
func (jsoa *JSONStringOrArray) UnmarshalJSON(data []byte) error {
var str string
if json.Unmarshal(data, &str) == nil {
*jsoa = []string{str}
return nil
}
var strs []string
json.Unmarshal(data, &strs)
*jsoa = strs
return nil
}
type MsgInfo struct { type MsgInfo struct {
Command MsgInfoCommand `json:"cmd"` Command MsgInfoCommand `json:"cmd"`
ID string `json:"id"` IDs JSONStringOrArray `json:"id"`
Acknowledgement int `json:"ack"` Acknowledgement Acknowledgement `json:"ack"`
MessageFromJID string `json:"from"` MessageFromJID string `json:"from"`
SenderJID string `json:"participant"` SenderJID string `json:"participant"`
ToJID string `json:"to"` ToJID string `json:"to"`
Timestamp int64 `json:"t"` Timestamp int64 `json:"t"`
} }
type MsgInfoHandler interface { type MsgInfoHandler interface {
@ -43,7 +67,7 @@ type MsgInfoHandler interface {
HandleMsgInfo(MsgInfo) HandleMsgInfo(MsgInfo)
} }
func (ext *ExtendedConn) handleMessageMsgInfo(message []byte) { func (ext *ExtendedConn) handleMessageMsgInfo(msgType JSONMessageType, message []byte) {
var event MsgInfo var event MsgInfo
err := json.Unmarshal(message, &event) err := json.Unmarshal(message, &event)
if err != nil { if err != nil {
@ -53,11 +77,14 @@ func (ext *ExtendedConn) handleMessageMsgInfo(message []byte) {
event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1) event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1)
event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1) event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1)
event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1) event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1)
if msgType == MessageMsg {
event.SenderJID = event.MessageFromJID
}
for _, handler := range ext.handlers { for _, handler := range ext.handlers {
msgInfoHandler, ok := handler.(MsgInfoHandler) msgInfoHandler, ok := handler.(MsgInfoHandler)
if !ok { if !ok {
continue continue
} }
msgInfoHandler.HandleMsgInfo(event) go msgInfoHandler.HandleMsgInfo(event)
} }
} }

View file

@ -53,6 +53,6 @@ func (ext *ExtendedConn) handleMessagePresence(message []byte) {
if !ok { if !ok {
continue continue
} }
presenceHandler.HandlePresence(event) go presenceHandler.HandlePresence(event)
} }
} }

View file

@ -62,6 +62,6 @@ func (ext *ExtendedConn) handleMessageProps(message []byte) {
if !ok { if !ok {
continue continue
} }
protocolPropsHandler.HandleProtocolProps(event) go protocolPropsHandler.HandleProtocolProps(event)
} }
} }

View file

@ -57,6 +57,6 @@ func (ext *ExtendedConn) handleMessageStream(message []json.RawMessage) {
if !ok { if !ok {
continue continue
} }
streamHandler.HandleStreamEvent(event) go streamHandler.HandleStreamEvent(event)
} }
} }