Add support for bridging reactions

This commit is contained in:
Tulir Asokan 2022-03-05 21:22:31 +02:00
parent 601864131e
commit 1eb210c249
15 changed files with 363 additions and 54 deletions

View File

@ -1,12 +1,31 @@
# unreleased
# v0.3.0 (unreleased)
* Added reaction bridging in both directions.
* Added automatic sending of hidden messages to primary device to prevent
false-positive disconnection warnings if there have been no messages sent or
received in >12 days.
* Added proper error message when WhatsApp rejects the connection due to the
bridge being out of date.
* Added experimental provisioning API to list contacts and start DMs.
* Added experimental provisioning API to list contacts/groups, start DMs and
open group portals. Note that these APIs are subject to change at any time.
* Added option to always send "active" delivery receipts (two gray ticks), even
if presence bridging is disabled. By default, WhatsApp web only sends those
receipts when it's in the foreground (i.e. showing online status).
* Added option to send online presence on typing notifications (thanks to
[@abmantis] in [#452]). This can be used to enable incoming typing
notifications without enabling Matrix presence (WhatsApp only sends typing
notifications if you're online).
* Exposed maximum database connection idle time and lifetime options.
* Fixed syncing group topics. To get topics into existing portals on Matrix,
you can use `!wa sync groups`.
* Fixed sticker events on Matrix including a redundant `msgtype` field.
* Disabled file logging in Docker image by default.
* To enable it, mount a directory for the logs that's writable for the user
inside the container (1337 by default), then point the bridge at it using
the `logging` -> `directory` field, and finally set `file_name_format` to
something non-empty (the default is `{{.Date}}-{{.Index}}.log`).
[#452]: https://github.com/mautrix/whatsapp/pull/452
# v0.2.4 (2022-02-16)

View File

@ -7,6 +7,7 @@
* [x] Media/files
* [x] Replies
* [x] Message redactions
* [x] Reactions
* [x] Presence
* [x] Typing notifications
* [x] Read receipts
@ -34,6 +35,7 @@
* [x] Status broadcast
* [ ] Broadcast list (not currently supported on WhatsApp web)
* [x] Message deletions
* [x] Reactions
* [x] Avatars
* [ ] Presence
* [x] Typing notifications

View File

@ -38,7 +38,6 @@ type BridgeConfig struct {
PortalMessageBuffer int `yaml:"portal_message_buffer"`
CallStartNotices bool `yaml:"call_start_notices"`
IdentityChangeNotices bool `yaml:"identity_change_notices"`
ReactionNotices bool `yaml:"reaction_notices"`
HistorySync struct {
CreatePortals bool `yaml:"create_portals"`

View File

@ -75,7 +75,6 @@ func (helper *UpgradeHelper) doUpgrade() {
helper.Copy(Int, "bridge", "portal_message_buffer")
helper.Copy(Bool, "bridge", "call_start_notices")
helper.Copy(Bool, "bridge", "identity_change_notices")
helper.Copy(Bool, "bridge", "reaction_notices")
helper.Copy(Bool, "bridge", "history_sync", "create_portals")
helper.Copy(Int, "bridge", "history_sync", "max_age")
helper.Copy(Bool, "bridge", "history_sync", "backfill")

View File

@ -40,10 +40,11 @@ type Database struct {
log log.Logger
dialect string
User *UserQuery
Portal *PortalQuery
Puppet *PuppetQuery
Message *MessageQuery
User *UserQuery
Portal *PortalQuery
Puppet *PuppetQuery
Message *MessageQuery
Reaction *ReactionQuery
DisappearingMessage *DisappearingMessageQuery
}
@ -75,6 +76,10 @@ func New(cfg config.DatabaseConfig, baseLog log.Logger) (*Database, error) {
db: db,
log: db.log.Sub("Message"),
}
db.Reaction = &ReactionQuery{
db: db,
log: db.log.Sub("Reaction"),
}
db.DisappearingMessage = &DisappearingMessageQuery{
db: db,
log: db.log.Sub("DisappearingMessage"),

View File

@ -43,27 +43,27 @@ func (mq *MessageQuery) New() *Message {
const (
getAllMessagesQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE chat_jid=$1 AND chat_receiver=$2
`
getMessageByJIDQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3
`
getMessageByMXIDQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE mxid=$1
`
getLastMessageInChatQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp<=$3 AND sent=true ORDER BY timestamp DESC LIMIT 1
`
getFirstMessageInChatQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND sent=true ORDER BY timestamp ASC LIMIT 1
`
getMessagesBetweenQuery = `
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid FROM message
SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid FROM message
WHERE chat_jid=$1 AND chat_receiver=$2 AND timestamp>$3 AND timestamp<=$4 AND sent=true ORDER BY timestamp ASC
`
)
@ -130,6 +130,15 @@ const (
MsgErrMediaNotFound MessageErrorType = "media_not_found"
)
type MessageType string
const (
MsgUnknown MessageType = ""
MsgFake MessageType = "fake"
MsgNormal MessageType = "message"
MsgReaction MessageType = "reaction"
)
type Message struct {
db *Database
log log.Logger
@ -140,8 +149,9 @@ type Message struct {
Sender types.JID
Timestamp time.Time
Sent bool
Type MessageType
Error MessageErrorType
Error MessageErrorType
BroadcastListJID types.JID
}
@ -155,7 +165,7 @@ func (msg *Message) IsFakeJID() bool {
func (msg *Message) Scan(row Scannable) *Message {
var ts int64
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &ts, &msg.Sent, &msg.Error, &msg.BroadcastListJID)
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &ts, &msg.Sent, &msg.Type, &msg.Error, &msg.BroadcastListJID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
msg.log.Errorln("Database scan failed:", err)
@ -175,9 +185,9 @@ func (msg *Message) Insert() {
sender = ""
}
_, err := msg.db.Exec(`INSERT INTO message
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, error, broadcast_list_jid)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.Timestamp.Unix(), msg.Sent, msg.Error, msg.BroadcastListJID)
(chat_jid, chat_receiver, jid, mxid, sender, timestamp, sent, type, error, broadcast_list_jid)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.Timestamp.Unix(), msg.Sent, msg.Type, msg.Error, msg.BroadcastListJID)
if err != nil {
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
}
@ -192,10 +202,11 @@ func (msg *Message) MarkSent(ts time.Time) {
}
}
func (msg *Message) UpdateMXID(mxid id.EventID, newError MessageErrorType) {
func (msg *Message) UpdateMXID(mxid id.EventID, newType MessageType, newError MessageErrorType) {
msg.MXID = mxid
msg.Type = newType
msg.Error = newError
_, err := msg.db.Exec("UPDATE message SET mxid=$1, error=$2 WHERE chat_jid=$3 AND chat_receiver=$4 AND jid=$5", mxid, newError, msg.Chat.JID, msg.Chat.Receiver, msg.JID)
_, err := msg.db.Exec("UPDATE message SET mxid=$1, type=$2, error=$3 WHERE chat_jid=$4 AND chat_receiver=$5 AND jid=$6", mxid, newType, newError, msg.Chat.JID, msg.Chat.Receiver, msg.JID)
if err != nil {
msg.log.Warnfln("Failed to update %s@%s: %v", msg.Chat, msg.JID, err)
}

106
database/reaction.go Normal file
View File

@ -0,0 +1,106 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package database
import (
"database/sql"
"errors"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"go.mau.fi/whatsmeow/types"
)
type ReactionQuery struct {
db *Database
log log.Logger
}
func (rq *ReactionQuery) New() *Reaction {
return &Reaction{
db: rq.db,
log: rq.log,
}
}
const (
getReactionByTargetJIDQuery = `
SELECT chat_jid, chat_receiver, target_jid, sender, mxid, jid FROM reaction
WHERE chat_jid=$1 AND chat_receiver=$2 AND target_jid=$3 AND sender=$4
`
getReactionByMXIDQuery = `
SELECT chat_jid, chat_receiver, target_jid, sender, mxid, jid FROM reaction
WHERE mxid=$1
`
upsertReactionQuery = `
INSERT INTO reaction (chat_jid, chat_receiver, target_jid, sender, mxid, jid)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (chat_jid, chat_receiver, target_jid, sender)
DO UPDATE SET mxid=excluded.mxid, jid=excluded.jid
`
)
func (rq *ReactionQuery) GetByTargetJID(chat PortalKey, jid types.MessageID, sender types.JID) *Reaction {
return rq.maybeScan(rq.db.QueryRow(getReactionByTargetJIDQuery, chat.JID, chat.Receiver, jid, sender.ToNonAD()))
}
func (rq *ReactionQuery) GetByMXID(mxid id.EventID) *Reaction {
return rq.maybeScan(rq.db.QueryRow(getReactionByMXIDQuery, mxid))
}
func (rq *ReactionQuery) maybeScan(row *sql.Row) *Reaction {
if row == nil {
return nil
}
return rq.New().Scan(row)
}
type Reaction struct {
db *Database
log log.Logger
Chat PortalKey
TargetJID types.MessageID
Sender types.JID
MXID id.EventID
JID types.MessageID
}
func (reaction *Reaction) Scan(row Scannable) *Reaction {
err := row.Scan(&reaction.Chat.JID, &reaction.Chat.Receiver, &reaction.TargetJID, &reaction.Sender, &reaction.MXID, &reaction.JID)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
reaction.log.Errorln("Database scan failed:", err)
}
return nil
}
return reaction
}
func (reaction *Reaction) Upsert() {
reaction.Sender = reaction.Sender.ToNonAD()
_, err := reaction.db.Exec(upsertReactionQuery, reaction.Chat.JID, reaction.Chat.Receiver, reaction.TargetJID, reaction.Sender, reaction.MXID, reaction.JID)
if err != nil {
reaction.log.Warnfln("Failed to upsert reaction to %s@%s by %s: %v", reaction.Chat, reaction.TargetJID, reaction.Sender, err)
}
}
func (reaction *Reaction) GetTarget() *Message {
return reaction.db.Message.GetByJID(reaction.Chat, reaction.TargetJID)
}

View File

@ -0,0 +1,39 @@
package upgrades
import "database/sql"
func init() {
upgrades[38] = upgrade{"Add support for reactions", func(tx *sql.Tx, ctx context) error {
_, err := tx.Exec(`ALTER TABLE message ADD COLUMN type TEXT NOT NULL DEFAULT 'message'`)
if err != nil {
return err
}
if ctx.dialect == Postgres {
_, err = tx.Exec("ALTER TABLE message ALTER COLUMN type DROP DEFAULT")
if err != nil {
return err
}
}
_, err = tx.Exec("UPDATE message SET type='' WHERE error='decryption_failed'")
if err != nil {
return err
}
_, err = tx.Exec("UPDATE message SET type='fake' WHERE jid LIKE 'FAKE::%' OR mxid LIKE 'net.maunium.whatsapp.fake::%' OR jid=mxid")
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE reaction (
chat_jid TEXT,
chat_receiver TEXT,
target_jid TEXT,
sender TEXT,
mxid TEXT NOT NULL,
jid TEXT NOT NULL,
PRIMARY KEY (chat_jid, chat_receiver, target_jid, sender),
CONSTRAINT target_message_fkey FOREIGN KEY (chat_jid, chat_receiver, target_jid)
REFERENCES message(chat_jid, chat_receiver, jid)
ON DELETE CASCADE ON UPDATE CASCADE
)`)
return err
}}
}

View File

@ -40,7 +40,7 @@ type upgrade struct {
fn upgradeFunc
}
const NumberOfUpgrades = 38
const NumberOfUpgrades = 39
var upgrades [NumberOfUpgrades]upgrade

View File

@ -107,8 +107,6 @@ bridge:
call_start_notices: true
# Should another user's cryptographic identity changing send a message to Matrix?
identity_change_notices: false
# Should a "reactions not yet supported" warning be sent to the Matrix room when a user reacts to a message?
reaction_notices: true
portal_message_buffer: 128
# Settings for handling history sync payloads. These settings only apply right after login,
# because the phone only sends the history sync data once, and there's no way to re-request it
@ -147,8 +145,8 @@ bridge:
# Existing users won't be affected when these are changed.
default_bridge_receipts: true
default_bridge_presence: true
# Send the presence as "available" to whatsapp when users start typing on a portal.
# This works as a workaround for homeservers that do not support presence, and allows
# Send the presence as "available" to whatsapp when users start typing on a portal.
# This works as a workaround for homeservers that do not support presence, and allows
# users to see when the whatsapp user on the other side is typing during a conversation.
send_presence_on_typing: false
# Should the bridge always send "active" delivery receipts (two gray ticks on WhatsApp)

View File

@ -48,6 +48,7 @@ type portalToBackfill struct {
type wrappedInfo struct {
*types.MessageInfo
Type database.MessageType
Error database.MessageErrorType
}
@ -503,10 +504,10 @@ func (portal *Portal) appendBatchEvents(converted *ConvertedMessage, info *types
return err
}
*eventsArray = append(*eventsArray, mainEvt, captionEvt)
*infoArray = append(*infoArray, &wrappedInfo{info, converted.Error}, nil)
*infoArray = append(*infoArray, &wrappedInfo{info, database.MsgNormal, converted.Error}, nil)
} else {
*eventsArray = append(*eventsArray, mainEvt)
*infoArray = append(*infoArray, &wrappedInfo{info, converted.Error})
*infoArray = append(*infoArray, &wrappedInfo{info, database.MsgNormal, converted.Error})
}
if converted.MultiEvent != nil {
for _, subEvtContent := range converted.MultiEvent {
@ -562,13 +563,13 @@ func (portal *Portal) finishBatch(eventIDs []id.EventID, infos []*wrappedInfo) {
} else if info, ok := infoMap[types.MessageID(msgID)]; !ok {
portal.log.Warnfln("Didn't find info of message %s (event %s) to register it in the database", msgID, eventID)
} else {
portal.markHandled(nil, info.MessageInfo, eventID, true, false, info.Error)
portal.markHandled(nil, info.MessageInfo, eventID, true, false, info.Type, info.Error)
}
}
} else {
for i := 0; i < len(infos); i++ {
if infos[i] != nil {
portal.markHandled(nil, infos[i].MessageInfo, eventIDs[i], true, false, infos[i].Error)
portal.markHandled(nil, infos[i].MessageInfo, eventIDs[i], true, false, infos[i].Type, infos[i].Error)
}
}
portal.log.Infofln("Successfully sent %d events", len(eventIDs))

View File

@ -471,22 +471,24 @@ func (mx *MatrixHandler) HandleReaction(evt *event.Event) {
}
user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.RelayWhitelisted {
if user == nil || !user.Whitelisted || !user.IsLoggedIn() {
return
}
portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal == nil || (!user.Whitelisted && !portal.HasRelaybot()) {
if portal == nil {
return
}
content := evt.Content.AsReaction()
if content.RelatesTo.Key == "click to retry" || strings.HasPrefix(content.RelatesTo.Key, "\u267b") { // ♻️
portal.requestMediaRetry(user, content.RelatesTo.EventID)
} else if mx.bridge.Config.Bridge.ReactionNotices {
_, _ = portal.sendMainIntentMessage(&event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("\u26a0 Reactions are not yet supported by WhatsApp."),
})
} else {
if portal.IsPrivateChat() && user.JID.User != portal.Key.Receiver.User {
// One user can only react once, so we don't use the relay user for reactions
return
}
portal.HandleMatrixReaction(user, evt)
}
}

158
portal.go
View File

@ -554,7 +554,7 @@ func (portal *Portal) handleUndecryptableMessage(source *User, evt *events.Undec
if err != nil {
portal.log.Errorln("Failed to send decryption error of %s to Matrix: %v", evt.Info.ID, err)
}
portal.finishHandling(nil, &evt.Info, resp.EventID, database.MsgErrDecryptionFailed)
portal.finishHandling(nil, &evt.Info, resp.EventID, database.MsgUnknown, database.MsgErrDecryptionFailed)
}
func (portal *Portal) handleFakeMessage(msg fakeMessage) {
@ -587,7 +587,7 @@ func (portal *Portal) handleFakeMessage(msg fakeMessage) {
MessageSource: types.MessageSource{
Sender: msg.Sender,
},
}, resp.EventID, database.MsgNoError)
}, resp.EventID, database.MsgFake, database.MsgNoError)
}
}
@ -662,15 +662,17 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
}
}
if len(eventID) != 0 {
portal.finishHandling(existingMsg, &evt.Info, eventID, converted.Error)
portal.finishHandling(existingMsg, &evt.Info, eventID, database.MsgNormal, converted.Error)
}
} else if msgType == "reaction" {
portal.HandleMessageReaction(intent, source, &evt.Info, evt.Message.GetReactionMessage(), existingMsg)
} else if msgType == "revoke" {
portal.HandleMessageRevoke(source, &evt.Info, evt.Message.GetProtocolMessage().GetKey())
if existingMsg != nil {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
Reason: "The undecryptable message was actually the deletion of another message",
})
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, database.MsgNoError)
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, database.MsgFake, database.MsgNoError)
}
} else {
portal.log.Warnfln("Unhandled message: %+v (%s)", evt.Info, msgType)
@ -678,7 +680,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
Reason: "The undecryptable message contained an unsupported message type",
})
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, database.MsgNoError)
existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, database.MsgFake, database.MsgNoError)
}
return
}
@ -696,7 +698,7 @@ func (portal *Portal) isRecentlyHandled(id types.MessageID, error database.Messa
return false
}
func (portal *Portal) markHandled(msg *database.Message, info *types.MessageInfo, mxid id.EventID, isSent, recent bool, error database.MessageErrorType) *database.Message {
func (portal *Portal) markHandled(msg *database.Message, info *types.MessageInfo, mxid id.EventID, isSent, recent bool, msgType database.MessageType, error database.MessageErrorType) *database.Message {
if msg == nil {
msg = portal.bridge.DB.Message.New()
msg.Chat = portal.Key
@ -705,13 +707,14 @@ func (portal *Portal) markHandled(msg *database.Message, info *types.MessageInfo
msg.Timestamp = info.Timestamp
msg.Sender = info.Sender
msg.Sent = isSent
msg.Type = msgType
msg.Error = error
if info.IsIncomingBroadcast() {
msg.BroadcastListJID = info.Chat
}
msg.Insert()
} else {
msg.UpdateMXID(mxid, error)
msg.UpdateMXID(mxid, msgType, error)
}
if recent {
@ -740,8 +743,8 @@ func (portal *Portal) getMessageIntent(user *User, info *types.MessageInfo) *app
return portal.getMessagePuppet(user, info).IntentFor(portal)
}
func (portal *Portal) finishHandling(existing *database.Message, message *types.MessageInfo, mxid id.EventID, error database.MessageErrorType) {
portal.markHandled(existing, message, mxid, true, true, error)
func (portal *Portal) finishHandling(existing *database.Message, message *types.MessageInfo, mxid id.EventID, msgType database.MessageType, error database.MessageErrorType) {
portal.markHandled(existing, message, mxid, true, true, msgType, error)
portal.sendDeliveryReceipt(mxid)
var suffix string
if error == database.MsgErrDecryptionFailed {
@ -749,7 +752,7 @@ func (portal *Portal) finishHandling(existing *database.Message, message *types.
} else if error == database.MsgErrMediaNotFound {
suffix = "(media not found notice)"
}
portal.log.Debugln("Handled message", message.ID, "->", mxid, suffix)
portal.log.Debugfln("Handled message %s (%s) -> %s %s", message.ID, msgType, mxid, suffix)
}
func (portal *Portal) kickExtraUsers(participantMap map[types.JID]bool) {
@ -1417,6 +1420,43 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, replyToID typ
return true
}
type sendReactionContent struct {
event.ReactionEventContent
DoublePuppet string `json:"fi.mau.double_puppet_source,omitempty"`
}
func (portal *Portal) HandleMessageReaction(intent *appservice.IntentAPI, user *User, info *types.MessageInfo, reaction *waProto.ReactionMessage, existingMsg *database.Message) {
if existingMsg != nil {
_, _ = intent.RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{
Reason: "The undecryptable message was actually a reaction",
})
}
target := portal.bridge.DB.Message.GetByJID(portal.Key, reaction.GetKey().GetId())
if target == nil {
portal.log.Debugfln("Dropping reaction %s from %s to unknown message %s", info.ID, info.Sender, reaction.GetKey().GetId())
return
}
var content sendReactionContent
content.RelatesTo = event.RelatesTo{
Type: event.RelAnnotation,
EventID: target.MXID,
Key: reaction.GetText(),
}
if intent.IsCustomPuppet {
content.DoublePuppet = doublePuppetValue
}
resp, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventReaction, &content, info.Timestamp.UnixMilli())
if err != nil {
portal.log.Errorfln("Failed to bridge reaction %s from %s to %s: %v", info.ID, info.Sender, target.JID, err)
return
}
portal.finishHandling(existingMsg, info, resp.EventID, database.MsgReaction, database.MsgNoError)
portal.upsertReaction(intent, target.JID, info.Sender, resp.EventID, info.ID)
}
func (portal *Portal) HandleMessageRevoke(user *User, info *types.MessageInfo, key *waProto.MessageKey) bool {
msg := portal.bridge.DB.Message.GetByJID(portal.Key, key.GetId())
if msg == nil || msg.IsFakeMXID() {
@ -2194,7 +2234,7 @@ func (portal *Portal) handleMediaRetry(retry *events.MediaRetry, source *User) {
return
}
portal.log.Debugfln("Successfully edited %s -> %s after retry notification for %s", msg.MXID, resp.EventID, retry.MessageID)
msg.UpdateMXID(resp.EventID, database.MsgNoError)
msg.UpdateMXID(resp.EventID, database.MsgNormal, database.MsgNoError)
}
func (portal *Portal) requestMediaRetry(user *User, eventID id.EventID) {
@ -2466,7 +2506,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
replyToID := content.GetReplyTo()
if len(replyToID) > 0 {
replyToMsg := portal.bridge.DB.Message.GetByMXID(replyToID)
if replyToMsg != nil && !replyToMsg.IsFakeJID() {
if replyToMsg != nil && !replyToMsg.IsFakeJID() && replyToMsg.Type == database.MsgNormal {
ctxInfo.StanzaId = &replyToMsg.JID
ctxInfo.Participant = proto.String(replyToMsg.Sender.ToNonAD().String())
// Using blank content here seems to work fine on all official WhatsApp apps.
@ -2664,7 +2704,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
}
portal.MarkDisappearing(evt.ID, portal.ExpirationTime, true)
info := portal.generateMessageInfo(sender)
dbMsg := portal.markHandled(nil, info, evt.ID, false, true, database.MsgNoError)
dbMsg := portal.markHandled(nil, info, evt.ID, false, true, database.MsgNormal, database.MsgNoError)
portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID)
ts, err := sender.Client.SendMessage(portal.Key.JID, info.ID, msg)
if err != nil {
@ -2685,6 +2725,78 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
}
}
func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) {
// TODO checkpoints
portal.log.Debugfln("Received reaction event %s from %s", evt.ID, evt.Sender)
content, ok := evt.Content.Parsed.(*event.ReactionEventContent)
if !ok {
portal.log.Debugfln("Failed to handle reaction event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed)
return
}
target := portal.bridge.DB.Message.GetByMXID(content.RelatesTo.EventID)
if target == nil || target.Type == database.MsgReaction {
portal.log.Debugfln("Dropping reaction to unknown event %s", content.RelatesTo.EventID)
return
}
info := portal.generateMessageInfo(sender)
dbMsg := portal.markHandled(nil, info, evt.ID, false, true, database.MsgReaction, database.MsgNoError)
portal.upsertReaction(nil, target.JID, sender.JID, evt.ID, info.ID)
portal.log.Debugln("Sending reaction", evt.ID, "to WhatsApp", info.ID)
ts, err := portal.sendReactionToWhatsApp(sender, info.ID, target, content.RelatesTo.Key, evt.Timestamp)
if err != nil {
portal.log.Errorfln("Error sending reaction: %v", err)
} else {
portal.log.Debugfln("Handled Matrix reaction %s", evt.ID)
portal.sendDeliveryReceipt(evt.ID)
dbMsg.MarkSent(ts)
}
}
func (portal *Portal) sendReactionToWhatsApp(sender *User, id types.MessageID, target *database.Message, key string, timestamp int64) (time.Time, error) {
var messageKeyParticipant *string
if !portal.IsPrivateChat() {
messageKeyParticipant = proto.String(target.Sender.ToNonAD().String())
}
return sender.Client.SendMessage(portal.Key.JID, id, &waProto.Message{
ReactionMessage: &waProto.ReactionMessage{
Key: &waProto.MessageKey{
RemoteJid: proto.String(portal.Key.JID.String()),
FromMe: proto.Bool(target.Sender.User == sender.JID.User),
Id: proto.String(target.JID),
Participant: messageKeyParticipant,
},
Text: proto.String(key),
GroupingKey: proto.String(key), // TODO is this correct?
SenderTimestampMs: proto.Int64(timestamp),
},
})
}
func (portal *Portal) upsertReaction(intent *appservice.IntentAPI, targetJID types.MessageID, senderJID types.JID, mxid id.EventID, jid types.MessageID) {
dbReaction := portal.bridge.DB.Reaction.GetByTargetJID(portal.Key, targetJID, senderJID)
if dbReaction == nil {
dbReaction = portal.bridge.DB.Reaction.New()
dbReaction.Chat = portal.Key
dbReaction.TargetJID = targetJID
dbReaction.Sender = senderJID
} else {
portal.log.Debugfln("Redacting old Matrix reaction %s after new one (%s) was sent", dbReaction.MXID, mxid)
var err error
if intent != nil {
_, err = intent.RedactEvent(portal.MXID, dbReaction.MXID)
}
if intent == nil || errors.Is(err, mautrix.MForbidden) {
_, err = portal.MainIntent().RedactEvent(portal.MXID, dbReaction.MXID)
}
if err != nil {
portal.log.Warnfln("Failed to remove old reaction %s: %v", dbReaction.MXID, err)
}
}
dbReaction.MXID = mxid
dbReaction.JID = jid
dbReaction.Upsert()
}
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
if !portal.canBridgeFrom(sender, "redaction") {
return
@ -2712,8 +2824,24 @@ func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
return
}
portal.log.Debugfln("Sending redaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID)
_, err := sender.Client.RevokeMessage(portal.Key.JID, msg.JID)
var err error
if msg.Type == database.MsgReaction {
if reaction := portal.bridge.DB.Reaction.GetByMXID(evt.Redacts); reaction == nil {
portal.log.Debugfln("Ignoring redaction of reaction %s: reaction database entry not found", evt.ID)
portal.bridge.AS.SendErrorMessageSendCheckpoint(evt, appservice.StepRemote, errors.New("reaction database entry not found"), true, 0)
return
} else if reactionTarget := reaction.GetTarget(); reactionTarget == nil {
portal.log.Debugfln("Ignoring redaction of reaction %s: reaction target message not found", evt.ID)
portal.bridge.AS.SendErrorMessageSendCheckpoint(evt, appservice.StepRemote, errors.New("reaction target message not found"), true, 0)
return
} else {
portal.log.Debugfln("Sending redaction reaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID)
_, err = portal.sendReactionToWhatsApp(sender, "", reactionTarget, "", evt.Timestamp)
}
} else {
portal.log.Debugfln("Sending redaction %s of %s/%s to WhatsApp", evt.ID, msg.MXID, msg.JID)
_, err = sender.Client.RevokeMessage(portal.Key.JID, msg.JID)
}
if err != nil {
portal.log.Errorfln("Error handling Matrix redaction %s: %v", evt.ID, err)
portal.bridge.AS.SendErrorMessageSendCheckpoint(evt, appservice.StepRemote, err, true, 0)

View File

@ -475,7 +475,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
qrChan, err := user.Login(ctx)
if err != nil {
user.log.Errorf("Failed to log in from provisioning API:", err)
user.log.Errorln("Failed to log in from provisioning API:", err)
if errors.Is(err, ErrAlreadyLoggedIn) {
go user.Connect()
_ = c.WriteJSON(Error{

View File

@ -626,7 +626,7 @@ func (user *User) HandleEvent(event interface{}) {
go user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Message: v.String()})
user.bridge.Metrics.TrackConnectionState(user.JID, false)
case *events.Disconnected:
go user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect})
go user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Message: "Disconnected from WhatsApp. Trying to reconnect."})
user.bridge.Metrics.TrackConnectionState(user.JID, false)
case *events.Contact:
go user.syncPuppet(v.JID, "contact event")