mirror of
https://github.com/mautrix/whatsapp.git
synced 2025-03-14 14:15:38 +00:00
Add support for bridging reactions
This commit is contained in:
parent
601864131e
commit
1eb210c249
15 changed files with 363 additions and 54 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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
106
database/reaction.go
Normal 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)
|
||||
}
|
39
database/upgrades/2022-03-05-reactions.go
Normal file
39
database/upgrades/2022-03-05-reactions.go
Normal 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
|
||||
}}
|
||||
}
|
|
@ -40,7 +40,7 @@ type upgrade struct {
|
|||
fn upgradeFunc
|
||||
}
|
||||
|
||||
const NumberOfUpgrades = 38
|
||||
const NumberOfUpgrades = 39
|
||||
|
||||
var upgrades [NumberOfUpgrades]upgrade
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
16
matrix.go
16
matrix.go
|
@ -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
158
portal.go
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
2
user.go
2
user.go
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue