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
* [ ] Presence
* [ ] Typing notifications
* [ ] Read receipts
* [x] Read receipts
* [ ] Admin/superadmin status
* [ ] Membership actions
* [ ] 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 {
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 {

View file

@ -93,7 +93,7 @@ func (puppet *Puppet) Insert() error {
_, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)",
puppet.JID, puppet.Receiver, puppet.Displayname, puppet.Avatar)
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
}
@ -103,7 +103,7 @@ func (puppet *Puppet) Update() error {
puppet.Displayname, puppet.Avatar,
puppet.JID, puppet.Receiver)
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
}

View file

@ -17,21 +17,24 @@
package main
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"
"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"
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 {
@ -221,7 +224,7 @@ func (portal *Portal) CreateMatrixRoom() error {
name := portal.Name
topic := portal.Topic
isPrivateChat := false
if strings.HasSuffix(portal.JID, "s.whatsapp.net") {
if strings.HasSuffix(portal.JID, whatsapp_ext.NewUserSuffix) {
puppet := portal.user.GetPuppetByJID(portal.JID)
name = puppet.Displayname
topic = "WhatsApp private chat"
@ -244,7 +247,7 @@ func (portal *Portal) CreateMatrixRoom() error {
}
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 {
@ -282,13 +285,18 @@ func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.In
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 {
return
}
message := portal.bridge.DB.Message.GetByJID(portal.Owner, info.QuotedMessageID)
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
@ -299,18 +307,24 @@ func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
return
}
portal.CreateMatrixRoom()
err := portal.CreateMatrixRoom()
if err != nil {
portal.log.Errorln("Failed to create portal room:", err)
return
}
intent := portal.GetMessageIntent(message.Info)
if intent == nil {
return
}
resp, err := intent.SendMassagedMessageEvent(portal.MXID, gomatrix.EventMessage, gomatrix.Content{
content := gomatrix.Content{
Body: message.Text,
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 {
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return
@ -324,7 +338,11 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
return
}
portal.CreateMatrixRoom()
err := portal.CreateMatrixRoom()
if err != nil {
portal.log.Errorln("Failed to create portal room:", err)
return
}
intent := portal.GetMessageIntent(info)
if intent == nil {
@ -353,12 +371,12 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
content := gomatrix.Content{
Body: caption,
URL: uploaded.ContentURI,
Info: gomatrix.FileInfo{
Info: &gomatrix.FileInfo{
Size: len(data),
MimeType: mimeType,
},
RelatesTo: portal.GetRelations(info),
}
portal.SetReply(&content, info)
if thumbnail != nil {
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) {
var err error
switch evt.Content.MsgType {
@ -430,18 +454,21 @@ func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
if evt.Content.Format == gomatrix.FormatHTML {
text = htmlParser.Parse(evt.Content.FormattedBody)
}
id := makeMessageID()
err = portal.user.Conn.Send(whatsapp.TextMessage{
Text: text,
Info: whatsapp.MessageInfo{
Id: id,
RemoteJid: portal.JID,
},
})
portal.MarkHandled(id, evt.ID)
default:
portal.log.Debugln("Unhandled Matrix event:", evt)
return
}
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 {
portal.log.Debugln("Handled Matrix event:", evt)
}

View file

@ -17,18 +17,18 @@
package main
import (
"maunium.net/go/mautrix-whatsapp/database"
log "maunium.net/go/maulogger"
"fmt"
"regexp"
"maunium.net/go/mautrix-whatsapp/types"
"strings"
"maunium.net/go/mautrix-appservice"
"github.com/Rhymen/go-whatsapp"
"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) {
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)
colonIndex := strings.LastIndex(receiver, "=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
}
@ -120,7 +120,7 @@ func (user *User) NewPuppet(dbPuppet *database.Puppet) *Puppet {
dbPuppet.Receiver,
strings.Replace(
dbPuppet.JID,
puppetJIDStrippedSuffix, "", 1)),
whatsapp_ext.NewUserSuffix, "", 1)),
user.bridge.Config.Homeserver.Domain),
}
}

66
user.go
View file

@ -17,14 +17,16 @@
package main
import (
"maunium.net/go/mautrix-whatsapp/database"
"github.com/Rhymen/go-whatsapp"
"time"
"github.com/skip2/go-qrcode"
log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types"
"fmt"
"strings"
"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"
)
@ -186,7 +188,7 @@ func (user *User) Sync() {
user.log.Debugln("Syncing...")
user.Conn.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.Sync(contact)
}
@ -205,6 +207,10 @@ func (user *User) HandleError(err error) {
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) {
user.log.Debugln("Received text message:", message)
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)
}
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) {
user.log.Debugln("JSON message:", message)
}

View file

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

View file

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

View file

@ -17,21 +17,45 @@
package whatsapp_ext
import (
"github.com/Rhymen/go-whatsapp"
"encoding/json"
"strings"
"github.com/Rhymen/go-whatsapp"
)
type MsgInfoCommand string
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 {
Command MsgInfoCommand `json:"cmd"`
ID string `json:"id"`
Acknowledgement int `json:"ack"`
IDs JSONStringOrArray `json:"id"`
Acknowledgement Acknowledgement `json:"ack"`
MessageFromJID string `json:"from"`
SenderJID string `json:"participant"`
ToJID string `json:"to"`
@ -43,7 +67,7 @@ type MsgInfoHandler interface {
HandleMsgInfo(MsgInfo)
}
func (ext *ExtendedConn) handleMessageMsgInfo(message []byte) {
func (ext *ExtendedConn) handleMessageMsgInfo(msgType JSONMessageType, message []byte) {
var event MsgInfo
err := json.Unmarshal(message, &event)
if err != nil {
@ -53,11 +77,14 @@ func (ext *ExtendedConn) handleMessageMsgInfo(message []byte) {
event.MessageFromJID = strings.Replace(event.MessageFromJID, OldUserSuffix, NewUserSuffix, 1)
event.SenderJID = strings.Replace(event.SenderJID, OldUserSuffix, NewUserSuffix, 1)
event.ToJID = strings.Replace(event.ToJID, OldUserSuffix, NewUserSuffix, 1)
if msgType == MessageMsg {
event.SenderJID = event.MessageFromJID
}
for _, handler := range ext.handlers {
msgInfoHandler, ok := handler.(MsgInfoHandler)
if !ok {
continue
}
msgInfoHandler.HandleMsgInfo(event)
go msgInfoHandler.HandleMsgInfo(event)
}
}

View file

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

View file

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

View file

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