handlewhatsapp: implement various special event types

Includes:

* Identity change notices
* Call start notices
* Delete chat for me events
* Delete message for me events
* Mark chat as read events
This commit is contained in:
Tulir Asokan 2024-09-26 12:37:21 +03:00
parent 3ccafa4dc9
commit acc6017308
7 changed files with 125 additions and 13 deletions

2
go.mod
View file

@ -22,7 +22,7 @@ require (
golang.org/x/sync v0.8.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.21.1-0.20240925131409-0c7f701828f0
maunium.net/go/mautrix v0.21.1-0.20240926091654-7a5f15b03c2e
)
require (

4
go.sum
View file

@ -114,5 +114,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.21.1-0.20240925131409-0c7f701828f0 h1:nX8gIPDHf2UXkcGMX982mtdTFXgbXtJGIDUQta+mRbE=
maunium.net/go/mautrix v0.21.1-0.20240925131409-0c7f701828f0/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0=
maunium.net/go/mautrix v0.21.1-0.20240926091654-7a5f15b03c2e h1:elQExbBoPGaI0jKMt2IzhpGT7NoTUqZCFxNSvRw0s1A=
maunium.net/go/mautrix v0.21.1-0.20240926091654-7a5f15b03c2e/go.mod h1:qm9oDhcHxF/Xby5RUuONIGpXw1SXXqLZj/GgvMxJxu0=

View file

@ -103,7 +103,7 @@ func applyHistoryInfo(info *bridgev2.ChatInfo, conv *wadb.Conversation) {
} else if ptr.Val(conv.Archived) {
info.UserLocal.Tag = ptr.Ptr(event.RoomTagLowPriority)
}
if ptr.Val(conv.EphemeralExpiration) > 0 {
if info.Disappear == nil && ptr.Val(conv.EphemeralExpiration) > 0 {
info.Disappear = &database.DisappearingSetting{
Type: database.DisappearingTypeAfterRead,
Timer: time.Duration(*conv.EphemeralExpiration) * time.Second,

View file

@ -34,6 +34,7 @@ type Config struct {
DisplaynameTemplate string `yaml:"displayname_template"`
CallStartNotices bool `yaml:"call_start_notices"`
IdentityChangeNotices bool `yaml:"identity_change_notices"`
SendPresenceOnTyping bool `yaml:"send_presence_on_typing"`
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
DisableStatusBroadcastSend bool `yaml:"disable_status_broadcast_send"`
@ -91,6 +92,7 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "displayname_template")
helper.Copy(up.Bool, "call_start_notices")
helper.Copy(up.Bool, "identity_change_notices")
helper.Copy(up.Bool, "send_presence_on_typing")
helper.Copy(up.Bool, "enable_status_broadcast")
helper.Copy(up.Bool, "disable_status_broadcast_send")

View file

@ -21,6 +21,8 @@ displayname_template: "{{or .FullName .BusinessName .PushName .Phone}} (WA)"
# Should incoming calls send a message to the Matrix room?
call_start_notices: true
# Should another user's cryptographic identity changing send a message to Matrix?
identity_change_notices: false
# 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.

View file

@ -1,7 +1,9 @@
package connector
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@ -12,6 +14,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix-whatsapp/pkg/waid"
)
@ -72,14 +75,11 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) {
case *events.IdentityChange:
wa.handleWAIdentityChange(evt)
case *events.MarkChatAsRead:
// TODO
//wa.handleWAMarkChatAsRead(evt)
wa.handleWAMarkChatAsRead(evt)
case *events.DeleteForMe:
// TODO
//wa.handleWADeleteForMe(evt)
wa.handleWADeleteForMe(evt)
case *events.DeleteChat:
// TODO
//wa.handleWADeleteChat(evt)
wa.handleWADeleteChat(evt)
case *events.Mute:
// TODO
case *events.Archive:
@ -330,9 +330,110 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
}
func (wa *WhatsAppClient) handleWACallStart(sender types.JID, id, callType string, ts time.Time) {
// TODO
if !wa.Main.Config.CallStartNotices {
return
}
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[string]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(sender),
Sender: wa.makeEventSender(sender),
CreatePortal: true,
Timestamp: ts,
},
Data: callType,
ID: waid.MakeFakeMessageID(sender, sender, "call-"+id),
ConvertMessageFunc: convertCallStart,
})
}
func convertCallStart(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, callType string) (*bridgev2.ConvertedMessage, error) {
text := "Incoming call. Use the WhatsApp app to answer."
if callType != "" {
text = fmt.Sprintf("Incoming %s call. Use the WhatsApp app to answer.", callType)
}
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgText,
Body: text,
},
}},
}, nil
}
func (wa *WhatsAppClient) handleWAIdentityChange(evt *events.IdentityChange) {
// TODO
if !wa.Main.Config.IdentityChangeNotices {
return
}
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage,
LogContext: nil,
PortalKey: wa.makeWAPortalKey(evt.JID),
Sender: wa.makeEventSender(evt.JID),
CreatePortal: false,
Timestamp: evt.Timestamp,
},
Data: evt,
ID: waid.MakeFakeMessageID(evt.JID, evt.JID, "idchange-"+strconv.FormatInt(evt.Timestamp.UnixMilli(), 10)),
ConvertMessageFunc: convertIdentityChange,
})
}
func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data *events.IdentityChange) (*bridgev2.ConvertedMessage, error) {
ghost, err := portal.Bridge.GetGhostByID(ctx, waid.MakeUserID(data.JID))
if err != nil {
return nil, err
}
text := fmt.Sprintf("Your security code with %s changed.", ghost.Name)
if data.Implicit {
text = fmt.Sprintf("Your security code with %s (device #%d) changed.", ghost.Name, data.JID.Device)
}
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: text,
},
}},
}, nil
}
func (wa *WhatsAppClient) handleWADeleteChat(evt *events.DeleteChat) {
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatDelete,
PortalKey: wa.makeWAPortalKey(evt.JID),
Timestamp: evt.Timestamp,
},
OnlyForMe: true,
})
}
func (wa *WhatsAppClient) handleWADeleteForMe(evt *events.DeleteForMe) {
wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessageRemove,
PortalKey: wa.makeWAPortalKey(evt.ChatJID),
Timestamp: evt.Timestamp,
},
TargetMessage: waid.MakeMessageID(evt.ChatJID, evt.SenderJID, evt.MessageID),
OnlyForMe: true,
})
}
func (wa *WhatsAppClient) handleWAMarkChatAsRead(evt *events.MarkChatAsRead) {
wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt,
PortalKey: wa.makeWAPortalKey(evt.JID),
Sender: wa.makeEventSender(wa.JID),
Timestamp: evt.Timestamp,
},
ReadUpTo: evt.Timestamp,
})
}

View file

@ -60,6 +60,10 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message
return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id))
}
func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID {
return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data))
}
type ParsedMessageID struct {
Chat types.JID
Sender types.JID
@ -69,6 +73,9 @@ type ParsedMessageID struct {
func ParseMessageID(messageID networkid.MessageID) (*ParsedMessageID, error) {
parts := strings.SplitN(string(messageID), ":", 3)
if len(parts) == 3 {
if parts[0] == "fake" || strings.HasPrefix(parts[2], "FAKE::") {
return nil, fmt.Errorf("fake message ID")
}
chat, err := types.ParseJID(parts[0])
if err != nil {
return nil, err
@ -79,6 +86,6 @@ func ParseMessageID(messageID networkid.MessageID) (*ParsedMessageID, error) {
}
return &ParsedMessageID{Chat: chat, Sender: sender, ID: parts[2]}, nil
} else {
return nil, fmt.Errorf("invalid message id")
return nil, fmt.Errorf("invalid message ID")
}
}