Add support for disappearing messages

This commit is contained in:
Tulir Asokan 2022-01-07 14:32:00 +02:00
parent a6adf61417
commit 18ea5af45e
12 changed files with 375 additions and 33 deletions

View file

@ -5,6 +5,7 @@
very obscure extensions in their mime type database.
* Added support for personal filtering spaces (started by [@HelderFSFerreira] and [@clmnin] in [#413]).
* Added support for multi-contact messages.
* Added support for disappearing messages.
* Fixed avatar remove events from WhatsApp being ignored.
* Fixed the bridge using the wrong Olm session if a client established a new
one due to corruption.

View file

@ -73,6 +73,8 @@ type BridgeConfig struct {
AllowUserInvite bool `yaml:"allow_user_invite"`
FederateRooms bool `yaml:"federate_rooms"`
DisappearingMessagesInGroups bool `yaml:"disappearing_messages_in_groups"`
CommandPrefix string `yaml:"command_prefix"`
ManagementRoomText struct {

View file

@ -102,6 +102,7 @@ func (helper *UpgradeHelper) doUpgrade() {
helper.Copy(Bool, "bridge", "allow_user_invite")
helper.Copy(Str, "bridge", "command_prefix")
helper.Copy(Bool, "bridge", "federate_rooms")
helper.Copy(Bool, "bridge", "disappearing_messages_in_groups")
helper.Copy(Str, "bridge", "management_room_text", "welcome")
helper.Copy(Str, "bridge", "management_room_text", "welcome_connected")
helper.Copy(Str, "bridge", "management_room_text", "welcome_unconnected")

View file

@ -41,6 +41,8 @@ type Database struct {
Portal *PortalQuery
Puppet *PuppetQuery
Message *MessageQuery
DisappearingMessage *DisappearingMessageQuery
}
func New(dbType string, uri string, baseLog log.Logger) (*Database, error) {
@ -70,6 +72,10 @@ func New(dbType string, uri string, baseLog log.Logger) (*Database, error) {
db: db,
log: db.log.Sub("Message"),
}
db.DisappearingMessage = &DisappearingMessageQuery{
db: db,
log: db.log.Sub("DisappearingMessage"),
}
return db, nil
}

View file

@ -0,0 +1,140 @@
// 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"
"time"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
)
type DisappearingMessageQuery struct {
db *Database
log log.Logger
}
func (dmq *DisappearingMessageQuery) New() *DisappearingMessage {
return &DisappearingMessage{
db: dmq.db,
log: dmq.log,
}
}
func (dmq *DisappearingMessageQuery) NewWithValues(roomID id.RoomID, eventID id.EventID, expireIn time.Duration, startNow bool) *DisappearingMessage {
dm := &DisappearingMessage{
db: dmq.db,
log: dmq.log,
RoomID: roomID,
EventID: eventID,
ExpireIn: expireIn,
}
if startNow {
dm.ExpireAt = time.Now().Add(dm.ExpireIn)
}
return dm
}
const (
getAllScheduledDisappearingMessagesQuery = `
SELECT room_id, event_id, expire_in, expire_at FROM disappearing_message WHERE expire_at IS NOT NULL
`
startUnscheduledDisappearingMessagesInRoomQuery = `
UPDATE disappearing_message SET expire_at=$1+expire_in WHERE room_id=$2 AND expire_at IS NULL
RETURNING room_id, event_id, expire_in, expire_at
`
)
func (dmq *DisappearingMessageQuery) GetAllScheduled() (messages []*DisappearingMessage) {
rows, err := dmq.db.Query(getAllScheduledDisappearingMessagesQuery)
if err != nil || rows == nil {
return nil
}
for rows.Next() {
messages = append(messages, dmq.New().Scan(rows))
}
return
}
func (dmq *DisappearingMessageQuery) StartAllUnscheduledInRoom(roomID id.RoomID) (messages []*DisappearingMessage) {
rows, err := dmq.db.Query(startUnscheduledDisappearingMessagesInRoomQuery, time.Now().UnixMilli(), roomID)
if err != nil || rows == nil {
return nil
}
for rows.Next() {
messages = append(messages, dmq.New().Scan(rows))
}
return
}
type DisappearingMessage struct {
db *Database
log log.Logger
RoomID id.RoomID
EventID id.EventID
ExpireIn time.Duration
ExpireAt time.Time
}
func (msg *DisappearingMessage) Scan(row Scannable) *DisappearingMessage {
var expireIn int64
var expireAt sql.NullInt64
err := row.Scan(&msg.RoomID, &msg.EventID, &expireIn, &expireAt)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
msg.log.Errorln("Database scan failed:", err)
}
return nil
}
msg.ExpireIn = time.Duration(expireIn) * time.Millisecond
if expireAt.Valid {
msg.ExpireAt = time.UnixMilli(expireAt.Int64)
}
return msg
}
func (msg *DisappearingMessage) Insert() {
var expireAt sql.NullInt64
if !msg.ExpireAt.IsZero() {
expireAt.Valid = true
expireAt.Int64 = msg.ExpireAt.UnixMilli()
}
_, err := msg.db.Exec(`INSERT INTO disappearing_message (room_id, event_id, expire_in, expire_at) VALUES ($1, $2, $3, $4)`,
msg.RoomID, msg.EventID, msg.ExpireIn.Milliseconds(), expireAt)
if err != nil {
msg.log.Warnfln("Failed to insert %s/%s: %v", msg.RoomID, msg.EventID, err)
}
}
func (msg *DisappearingMessage) StartTimer() {
msg.ExpireAt = time.Now().Add(msg.ExpireIn * time.Second)
_, err := msg.db.Exec("UPDATE disappearing_message SET expire_at=$1 WHERE room_id=$2 AND event_id=$3", msg.ExpireAt.Unix(), msg.RoomID, msg.EventID)
if err != nil {
msg.log.Warnfln("Failed to update %s/%s: %v", msg.RoomID, msg.EventID, err)
}
}
func (msg *DisappearingMessage) Delete() {
_, err := msg.db.Exec("DELETE FROM disappearing_message WHERE room_id=$1 AND event_id=$2", msg.RoomID, msg.EventID)
if err != nil {
msg.log.Warnfln("Failed to delete %s/%s: %v", msg.RoomID, msg.EventID, err)
}
}

View file

@ -140,11 +140,13 @@ type Portal struct {
NextBatchID id.BatchID
RelayUserID id.UserID
ExpirationTime uint32
}
func (portal *Portal) Scan(row Scannable) *Portal {
var mxid, avatarURL, firstEventID, nextBatchID, relayUserID sql.NullString
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID)
err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted, &firstEventID, &nextBatchID, &relayUserID, &portal.ExpirationTime)
if err != nil {
if err != sql.ErrNoRows {
portal.log.Errorln("Database scan failed:", err)
@ -174,16 +176,16 @@ func (portal *Portal) relayUserPtr() *id.UserID {
}
func (portal *Portal) Insert() {
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr())
_, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted, first_event_id, next_batch_id, relay_user_id, expiration_time) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime)
if err != nil {
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
}
}
func (portal *Portal) Update() {
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6, first_event_id=$7, next_batch_id=$8, relay_user_id=$9 WHERE jid=$10 AND receiver=$11",
portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.Key.JID, portal.Key.Receiver)
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6, first_event_id=$7, next_batch_id=$8, relay_user_id=$9, expiration_time=$10 WHERE jid=$11 AND receiver=$12",
portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.FirstEventID.String(), portal.NextBatchID.String(), portal.relayUserPtr(), portal.ExpirationTime, portal.Key.JID, portal.Key.Receiver)
if err != nil {
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
}

View file

@ -0,0 +1,20 @@
package upgrades
import "database/sql"
func init() {
upgrades[34] = upgrade{"Add support for disappearing messages", func(tx *sql.Tx, ctx context) error {
_, err := tx.Exec(`ALTER TABLE portal ADD COLUMN expiration_time BIGINT NOT NULL DEFAULT 0 CHECK (expiration_time >= 0 AND expiration_time < 4294967296)`)
if err != nil {
return err
}
_, err = tx.Exec(`CREATE TABLE disappearing_message (
room_id TEXT,
event_id TEXT,
expire_in BIGINT NOT NULL,
expire_at BIGINT,
PRIMARY KEY (room_id, event_id)
)`)
return err
}}
}

View file

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

71
disappear.go Normal file
View file

@ -0,0 +1,71 @@
// 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 main
import (
"fmt"
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
)
func (portal *Portal) MarkDisappearing(eventID id.EventID, expiresIn uint32, startNow bool) {
if expiresIn == 0 || (!portal.bridge.Config.Bridge.DisappearingMessagesInGroups && portal.IsGroupChat()) {
return
}
msg := portal.bridge.DB.DisappearingMessage.NewWithValues(portal.MXID, eventID, time.Duration(expiresIn)*time.Second, startNow)
msg.Insert()
if startNow {
go portal.sleepAndDelete(msg)
}
}
func (portal *Portal) ScheduleDisappearing() {
if !portal.bridge.Config.Bridge.DisappearingMessagesInGroups && portal.IsGroupChat() {
return
}
for _, msg := range portal.bridge.DB.DisappearingMessage.StartAllUnscheduledInRoom(portal.MXID) {
go portal.sleepAndDelete(msg)
}
}
func (bridge *Bridge) RestartAllDisappearing() {
for _, msg := range bridge.DB.DisappearingMessage.GetAllScheduled() {
portal := bridge.GetPortalByMXID(msg.RoomID)
go portal.sleepAndDelete(msg)
}
}
func (portal *Portal) sleepAndDelete(msg *database.DisappearingMessage) {
sleepTime := msg.ExpireAt.Sub(time.Now())
portal.log.Debugfln("Sleeping for %s to make %s disappear", sleepTime, msg.EventID)
time.Sleep(sleepTime)
_, err := portal.MainIntent().RedactEvent(msg.RoomID, msg.EventID, mautrix.ReqRedact{
Reason: "Message expired",
TxnID: fmt.Sprintf("mxwa_disappear_%s", msg.EventID),
})
if err != nil {
portal.log.Warnfln("Failed to make %s disappear: %v", msg.EventID, err)
} else {
portal.log.Debugfln("Disappeared %s", msg.EventID)
}
msg.Delete()
}

View file

@ -187,6 +187,10 @@ bridge:
# Whether or not created rooms should have federation enabled.
# If false, created portal rooms will never be federated.
federate_rooms: true
# Whether to enable disappearing messages in groups. If enabled, then the expiration time of
# the messages will be determined by the first user to read the message, rather than individually.
# If the bridge only has a single user, this can be turned on safely.
disappearing_messages_in_groups: false
# The prefix for commands. Only required in non-management rooms.
command_prefix: "!wa"

View file

@ -333,6 +333,7 @@ func (bridge *Bridge) Start() {
if bridge.Config.Bridge.ResendBridgeInfo {
go bridge.ResendBridgeInfo()
}
go bridge.RestartAllDisappearing()
bridge.AS.Ready = true
}

148
portal.go
View file

@ -300,6 +300,8 @@ func getMessageType(waMsg *waProto.Message) string {
switch waMsg.GetProtocolMessage().GetType() {
case waProto.ProtocolMessage_REVOKE:
return "revoke"
case waProto.ProtocolMessage_EPHEMERAL_SETTING:
return "disappearing timer change"
case waProto.ProtocolMessage_APP_STATE_SYNC_KEY_SHARE, waProto.ProtocolMessage_HISTORY_SYNC_NOTIFICATION, waProto.ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC:
return "ignore"
default:
@ -342,6 +344,52 @@ func getMessageType(waMsg *waProto.Message) string {
}
}
func pluralUnit(val int, name string) string {
if val == 1 {
return fmt.Sprintf("%d %s", val, name)
} else if val == 0 {
return ""
}
return fmt.Sprintf("%d %ss", val, name)
}
func naturalJoin(parts []string) string {
if len(parts) == 0 {
return ""
} else if len(parts) == 1 {
return parts[0]
} else if len(parts) == 2 {
return fmt.Sprintf("%s and %s", parts[0], parts[1])
} else {
return fmt.Sprintf("%s and %s", strings.Join(parts[:len(parts)-1], ", "), parts[len(parts)-1])
}
}
func formatDuration(d time.Duration) string {
const Day = time.Hour * 24
var days, hours, minutes, seconds int
days, d = int(d/Day), d%Day
hours, d = int(d/time.Hour), d%time.Hour
minutes, d = int(d/time.Minute), d%time.Minute
seconds = int(d / time.Second)
parts := make([]string, 0, 4)
if days > 0 {
parts = append(parts, pluralUnit(days, "day"))
}
if hours > 0 {
parts = append(parts, pluralUnit(hours, "hour"))
}
if minutes > 0 {
parts = append(parts, pluralUnit(seconds, "minute"))
}
if seconds > 0 {
parts = append(parts, pluralUnit(seconds, "second"))
}
return naturalJoin(parts)
}
func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User, info *types.MessageInfo, waMsg *waProto.Message) *ConvertedMessage {
switch {
case waMsg.Conversation != nil || waMsg.ExtendedTextMessage != nil:
@ -366,6 +414,23 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User,
return portal.convertLiveLocationMessage(intent, waMsg.GetLiveLocationMessage())
case waMsg.GroupInviteMessage != nil:
return portal.convertGroupInviteMessage(intent, info, waMsg.GetGroupInviteMessage())
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waProto.ProtocolMessage_EPHEMERAL_SETTING:
portal.ExpirationTime = waMsg.ProtocolMessage.GetEphemeralExpiration()
portal.Update()
var msg string
if portal.ExpirationTime == 0 {
msg = "Turned off disappearing messages"
} else {
msg = fmt.Sprintf("Set the disappearing message timer to %s", formatDuration(time.Duration(portal.ExpirationTime)*time.Second))
}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: &event.MessageEventContent{
Body: msg,
MsgType: event.MsgNotice,
},
}
default:
return nil
}
@ -477,6 +542,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
}
var eventID id.EventID
if existingMsg != nil {
portal.MarkDisappearing(existingMsg.MXID, converted.ExpiresIn, false)
converted.Content.SetEdit(existingMsg.MXID)
} else if len(converted.ReplyTo) > 0 {
portal.SetReply(converted.Content, converted.ReplyTo)
@ -485,6 +551,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
if err != nil {
portal.log.Errorfln("Failed to send %s to Matrix: %v", msgID, err)
} else {
portal.MarkDisappearing(resp.EventID, converted.ExpiresIn, false)
eventID = resp.EventID
}
// TODO figure out how to handle captions with undecryptable messages turning decryptable
@ -493,6 +560,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
if err != nil {
portal.log.Errorfln("Failed to send caption of %s to Matrix: %v", msgID, err)
} else {
portal.MarkDisappearing(resp.EventID, converted.ExpiresIn, false)
eventID = resp.EventID
}
}
@ -501,11 +569,13 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) {
resp, err = portal.sendMessage(converted.Intent, converted.Type, subEvt, nil, evt.Info.Timestamp.UnixMilli())
if err != nil {
portal.log.Errorfln("Failed to send sub-event %d of %s to Matrix: %v", index+1, msgID, err)
} else {
portal.MarkDisappearing(resp.EventID, converted.ExpiresIn, false)
}
}
}
if len(eventID) != 0 {
portal.finishHandling(existingMsg, &evt.Info, resp.EventID, false)
portal.finishHandling(existingMsg, &evt.Info, eventID, false)
}
} else if msgType == "revoke" {
portal.HandleMessageRevoke(source, &evt.Info, evt.Message.GetProtocolMessage().GetKey())
@ -1194,6 +1264,10 @@ func (portal *Portal) IsPrivateChat() bool {
return portal.Key.JID.Server == types.DefaultUserServer
}
func (portal *Portal) IsGroupChat() bool {
return portal.Key.JID.Server == types.GroupServer
}
func (portal *Portal) IsBroadcastList() bool {
return portal.Key.JID.Server == types.BroadcastServer
}
@ -1337,7 +1411,8 @@ type ConvertedMessage struct {
MultiEvent []*event.MessageEventContent
ReplyTo types.MessageID
ReplyTo types.MessageID
ExpiresIn uint32
}
func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, msg *waProto.Message) *ConvertedMessage {
@ -1346,6 +1421,7 @@ func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, msg *waPr
MsgType: event.MsgText,
}
var replyTo types.MessageID
var expiresIn uint32
if msg.GetExtendedTextMessage() != nil {
content.Body = msg.GetExtendedTextMessage().GetText()
@ -1354,9 +1430,16 @@ func (portal *Portal) convertTextMessage(intent *appservice.IntentAPI, msg *waPr
portal.bridge.Formatter.ParseWhatsApp(content, contextInfo.GetMentionedJid())
replyTo = contextInfo.GetStanzaId()
}
expiresIn = contextInfo.GetExpiration()
}
return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content, ReplyTo: replyTo}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: replyTo,
ExpiresIn: expiresIn,
}
}
func (portal *Portal) convertLiveLocationMessage(intent *appservice.IntentAPI, msg *waProto.LiveLocationMessage) *ConvertedMessage {
@ -1368,10 +1451,11 @@ func (portal *Portal) convertLiveLocationMessage(intent *appservice.IntentAPI, m
content.Body += ": " + msg.GetCaption()
}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
}
}
@ -1419,10 +1503,11 @@ func (portal *Portal) convertLocationMessage(intent *appservice.IntentAPI, msg *
}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
}
}
@ -1447,11 +1532,12 @@ func (portal *Portal) convertGroupInviteMessage(intent *appservice.IntentAPI, in
},
}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: content,
Extra: extraAttrs,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
Intent: intent,
Type: event.EventMessage,
Content: content,
Extra: extraAttrs,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
}
}
@ -1483,10 +1569,11 @@ func (portal *Portal) convertContactMessage(intent *appservice.IntentAPI, msg *w
}
return &ConvertedMessage{
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
Intent: intent,
Type: event.EventMessage,
Content: content,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
}
}
@ -1510,6 +1597,7 @@ func (portal *Portal) convertContactsArrayMessage(intent *appservice.IntentAPI,
Body: fmt.Sprintf("Sent %s", name),
},
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
MultiEvent: contacts,
}
}
@ -1815,12 +1903,13 @@ func (portal *Portal) convertMediaMessage(intent *appservice.IntentAPI, source *
}
return &ConvertedMessage{
Intent: intent,
Type: eventType,
Content: content,
Caption: captionContent,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
Extra: extraContent,
Intent: intent,
Type: eventType,
Content: content,
Caption: captionContent,
ReplyTo: msg.GetContextInfo().GetStanzaId(),
ExpiresIn: msg.GetContextInfo().GetExpiration(),
Extra: extraContent,
}
}
@ -2047,6 +2136,9 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
ctxInfo.QuotedMessage = &waProto.Message{Conversation: proto.String("")}
}
}
if portal.ExpirationTime != 0 {
ctxInfo.Expiration = proto.Uint32(portal.ExpirationTime)
}
relaybotFormatted := false
if !sender.IsLoggedIn() || (portal.IsPrivateChat() && sender.JID.User != portal.Key.Receiver.User) {
if !portal.HasRelaybot() {
@ -2075,7 +2167,7 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waP
if content.MsgType == event.MsgEmote && !relaybotFormatted {
text = "/me " + text
}
if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil {
if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil || ctxInfo.Expiration != nil {
msg.ExtendedTextMessage = &waProto.ExtendedTextMessage{
Text: &text,
ContextInfo: &ctxInfo,
@ -2227,6 +2319,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
if msg == nil {
return
}
portal.MarkDisappearing(evt.ID, portal.ExpirationTime, true)
info := portal.generateMessageInfo(sender)
dbMsg := portal.markHandled(nil, info, evt.ID, false, true, false)
portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID)
@ -2333,6 +2426,7 @@ func (portal *Portal) HandleMatrixReadReceipt(sender *User, eventID id.EventID,
portal.log.Warnfln("Failed to mark %v as read by %s: %v", ids, sender.JID, err)
}
}
portal.ScheduleDisappearing()
}
func typingDiff(prev, new []id.UserID) (started, stopped []id.UserID) {