mirror of
https://github.com/mautrix/whatsapp.git
synced 2025-03-14 14:15:38 +00:00
v2: update things
This commit is contained in:
parent
8fecdebf68
commit
2aef3b3f54
22 changed files with 1255 additions and 984 deletions
|
@ -1,7 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "go.mau.fi/util/dbutil/litestream"
|
||||
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/pkg/connector"
|
||||
|
@ -15,14 +14,16 @@ var (
|
|||
BuildTime = "unknown"
|
||||
)
|
||||
|
||||
var c = &connector.WhatsAppConnector{}
|
||||
var m = mxmain.BridgeMain{
|
||||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "0.11.0",
|
||||
Connector: c,
|
||||
}
|
||||
|
||||
func main() {
|
||||
m := mxmain.BridgeMain{
|
||||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "0.10.9",
|
||||
Connector: connector.NewConnector(),
|
||||
}
|
||||
m.InitVersion(Tag, Commit, BuildTime)
|
||||
m.Run()
|
||||
}
|
||||
|
|
21
go.mod
21
go.mod
|
@ -1,25 +1,26 @@
|
|||
module maunium.net/go/mautrix-whatsapp
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/mattn/go-sqlite3 v1.14.23
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
go.mau.fi/util v0.6.1-0.20240802175451-b430ebbffc98
|
||||
go.mau.fi/util v0.7.1-0.20240904173517-ca3b3fe376c2
|
||||
go.mau.fi/webp v0.1.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20240726213518-bb5852f056ca
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
go.mau.fi/whatsmeow v0.0.0-20240828153534-8acde1ba8592
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/sync v0.8.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
maunium.net/go/mautrix v0.19.1-0.20240809124413-8c0f705ee90b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.20.1-0.20240906105454-33d724bf4c78
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -31,10 +32,11 @@ require (
|
|||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
|
@ -42,9 +44,8 @@ require (
|
|||
go.mau.fi/libsignal v0.1.1 // indirect
|
||||
go.mau.fi/zeroconfig v0.1.3 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
)
|
||||
|
|
33
go.sum
33
go.sum
|
@ -32,8 +32,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
|||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
|
||||
github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -47,8 +49,9 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
|
|||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
|
@ -69,18 +72,18 @@ github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
|||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.mau.fi/libsignal v0.1.1 h1:m/0PGBh4QKP/I1MQ44ti4C0fMbLMuHb95cmDw01FIpI=
|
||||
go.mau.fi/libsignal v0.1.1/go.mod h1:QLs89F/OA3ThdSL2Wz2p+o+fi8uuQUz0e1BRa6ExdBw=
|
||||
go.mau.fi/util v0.6.1-0.20240802175451-b430ebbffc98 h1:gJ0peWecBm6TtlxKFVIc1KbooXSCHtPfsfb2Eha5A0A=
|
||||
go.mau.fi/util v0.6.1-0.20240802175451-b430ebbffc98/go.mod h1:S1juuPWGau2GctPY3FR/4ec/MDLhAG2QPhdnUwpzWIo=
|
||||
go.mau.fi/util v0.7.1-0.20240904173517-ca3b3fe376c2 h1:VZQlKBbeJ7KOlYSh6BnN5uWQTY/ypn/bJv0YyEd+pXc=
|
||||
go.mau.fi/util v0.7.1-0.20240904173517-ca3b3fe376c2/go.mod h1:WgYvbt9rVmoFeajP97NunQU7AjgvTPiNExN3oTHeePs=
|
||||
go.mau.fi/webp v0.1.0 h1:BHObH/DcFntT9KYun5pDr0Ot4eUZO8k2C7eP7vF4ueA=
|
||||
go.mau.fi/webp v0.1.0/go.mod h1:e42Z+VMFrUMS9cpEwGRIor+lQWO8oUAyPyMtcL+NMt8=
|
||||
go.mau.fi/whatsmeow v0.0.0-20240726213518-bb5852f056ca h1:L0Pc6fi5RevuEASIP6Nd65/HZwCK8wTwm62FEly6UeY=
|
||||
go.mau.fi/whatsmeow v0.0.0-20240726213518-bb5852f056ca/go.mod h1:BhHKalSq0qNtSCuGIUIvoJyU5KbT4a7k8DQ5yw1Ssk4=
|
||||
go.mau.fi/whatsmeow v0.0.0-20240828153534-8acde1ba8592 h1:wVBSwcGNGaI8agrExAXSb6rx9lfp9GvUtsAdl0wFWEY=
|
||||
go.mau.fi/whatsmeow v0.0.0-20240828153534-8acde1ba8592/go.mod h1:BhHKalSq0qNtSCuGIUIvoJyU5KbT4a7k8DQ5yw1Ssk4=
|
||||
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
|
||||
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
|
@ -90,10 +93,10 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -105,5 +108,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.19.1-0.20240809124413-8c0f705ee90b h1:Gn2UryUziwkRk7veueA2CUTNKy1CrAVkt3QJn1xff90=
|
||||
maunium.net/go/mautrix v0.19.1-0.20240809124413-8c0f705ee90b/go.mod h1:ZWyxoQxRTBxzWIMs0kQCVogZIY0clTu33h102veCT/Q=
|
||||
maunium.net/go/mautrix v0.20.1-0.20240906105454-33d724bf4c78 h1:F8ls6UNW8QlTpQ2QAnPuCN5gVB30Y3ojBYfXb9T6U64=
|
||||
maunium.net/go/mautrix v0.20.1-0.20240906105454-33d724bf4c78/go.mod h1:l6nYvD5/FMSrAZ/IP1AqJV0b47SRl/0uQNRiy4CcSVk=
|
||||
|
|
60
pkg/connector/capabilities.go
Normal file
60
pkg/connector/capabilities.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
||||
DisappearingMessages: true,
|
||||
AggressiveUpdateInfo: false,
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
|
||||
return WhatsAppGeneralCaps
|
||||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
|
||||
var whatsappCaps = &bridgev2.NetworkRoomCapabilities{
|
||||
FormattedText: true,
|
||||
UserMentions: true,
|
||||
LocationMessages: true,
|
||||
Captions: true,
|
||||
Replies: true,
|
||||
Edits: true,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: 15 * time.Minute,
|
||||
Deletes: true,
|
||||
DeleteMaxAge: 48 * time.Hour,
|
||||
DefaultFileRestriction: &bridgev2.FileRestriction{
|
||||
MaxSize: WAMaxFileSize,
|
||||
},
|
||||
Files: map[event.MessageType]bridgev2.FileRestriction{
|
||||
event.MsgImage: {
|
||||
MaxSize: WAMaxFileSize,
|
||||
MimeTypes: []string{"image/png", "image/jpeg"},
|
||||
},
|
||||
event.MsgAudio: {
|
||||
MaxSize: WAMaxFileSize,
|
||||
MimeTypes: []string{"audio/mpeg", "audio/mp4", "audio/ogg", "audio/aac", "audio/amr"},
|
||||
},
|
||||
event.MsgVideo: {
|
||||
MaxSize: WAMaxFileSize,
|
||||
MimeTypes: []string{"video/mp4", "video/3gpp"},
|
||||
},
|
||||
event.MsgFile: {
|
||||
MaxSize: WAMaxFileSize,
|
||||
},
|
||||
},
|
||||
ReadReceipts: true,
|
||||
Reactions: true,
|
||||
ReactionCount: 1,
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities {
|
||||
return whatsappCaps
|
||||
}
|
|
@ -3,9 +3,11 @@ package connector
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waHistorySync"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
@ -15,103 +17,275 @@ import (
|
|||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
var moderatorPL = 50
|
||||
func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (wrapped *bridgev2.ChatInfo, err error) {
|
||||
portalJID, err := waid.ParsePortalID(portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch portalJID.Server {
|
||||
case types.DefaultUserServer:
|
||||
wrapped = wa.wrapDMInfo(portalJID)
|
||||
case types.BroadcastServer:
|
||||
if portalJID == types.StatusBroadcastJID {
|
||||
wrapped = wa.wrapStatusBroadcastInfo()
|
||||
} else {
|
||||
return nil, fmt.Errorf("broadcast list bridging is currently not supported")
|
||||
}
|
||||
case types.GroupServer:
|
||||
info, err := wa.Client.GetGroupInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped = wa.wrapGroupInfo(info)
|
||||
case types.NewsletterServer:
|
||||
info, err := wa.Client.GetNewsletterInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped = wa.wrapNewsletterInfo(info)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server %s", portalJID.Server)
|
||||
}
|
||||
var conv *waHistorySync.Conversation
|
||||
applyHistoryInfo(wrapped, conv)
|
||||
wa.applyChatSettings(ctx, portalJID, wrapped)
|
||||
return wrapped, nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.ContactListingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
func updateDisappearingTimerSetAt(ts int64) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
||||
return func(_ context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*PortalMetadata)
|
||||
if meta.DisappearingTimerSetAt != ts {
|
||||
meta.DisappearingTimerSetAt = ts
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetChatInfo(_ context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
|
||||
myJID := wa.Client.Store.ID.ToNonAD()
|
||||
members := &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
Members: []bridgev2.ChatMember{
|
||||
{
|
||||
EventSender: wa.makeEventSender(&myJID),
|
||||
Membership: event.MembershipJoin,
|
||||
PowerLevel: &moderatorPL,
|
||||
func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JID, info *bridgev2.ChatInfo) {
|
||||
chat, err := wa.Client.Store.ChatSettings.GetChatSettings(chatID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
|
||||
return
|
||||
}
|
||||
info.UserLocal = &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: ptr.Ptr(chat.MutedUntil),
|
||||
}
|
||||
if chat.Pinned {
|
||||
info.UserLocal.Tag = ptr.Ptr(event.RoomTagFavourite)
|
||||
} else if chat.Archived {
|
||||
info.UserLocal.Tag = ptr.Ptr(event.RoomTagLowPriority)
|
||||
}
|
||||
}
|
||||
|
||||
func applyHistoryInfo(info *bridgev2.ChatInfo, conv *waHistorySync.Conversation) {
|
||||
if conv == nil {
|
||||
return
|
||||
}
|
||||
info.CanBackfill = true
|
||||
info.UserLocal = &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: ptr.Ptr(time.Unix(int64(conv.GetMuteEndTime()), 0)),
|
||||
}
|
||||
if conv.GetPinned() > 0 {
|
||||
info.UserLocal.Tag = ptr.Ptr(event.RoomTagFavourite)
|
||||
} else if conv.GetArchived() {
|
||||
info.UserLocal.Tag = ptr.Ptr(event.RoomTagLowPriority)
|
||||
}
|
||||
if conv.GetEphemeralExpiration() > 0 {
|
||||
info.Disappear = &database.DisappearingSetting{
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(conv.GetEphemeralExpiration()) * time.Second,
|
||||
}
|
||||
info.ExtraUpdates = bridgev2.MergeExtraUpdaters(info.ExtraUpdates, updateDisappearingTimerSetAt(conv.GetEphemeralSettingTimestamp()))
|
||||
}
|
||||
}
|
||||
|
||||
const StatusBroadcastTopic = "WhatsApp status updates from your contacts"
|
||||
const StatusBroadcastName = "WhatsApp Status Broadcast"
|
||||
const BroadcastTopic = "WhatsApp broadcast list"
|
||||
const UnnamedBroadcastName = "Unnamed broadcast list"
|
||||
const PrivateChatTopic = "WhatsApp private chat"
|
||||
|
||||
func (wa *WhatsAppClient) wrapDMInfo(jid types.JID) *bridgev2.ChatInfo {
|
||||
info := &bridgev2.ChatInfo{
|
||||
Topic: ptr.Ptr(PrivateChatTopic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
TotalMemberCount: 2,
|
||||
OtherUserID: waid.MakeUserID(jid),
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(jid)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(wa.JID)},
|
||||
},
|
||||
PowerLevels: nil,
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
}
|
||||
if jid == wa.JID.ToNonAD() {
|
||||
// For chats with self, force-split the members so the user's own ghost is always in the room.
|
||||
info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
|
||||
"": {EventSender: bridgev2.EventSender{IsFromMe: true}},
|
||||
}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) wrapStatusBroadcastInfo() *bridgev2.ChatInfo {
|
||||
return &bridgev2.ChatInfo{
|
||||
Name: ptr.Ptr(StatusBroadcastName),
|
||||
Topic: ptr.Ptr(StatusBroadcastTopic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: false,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(wa.JID)},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
CanBackfill: false,
|
||||
}
|
||||
|
||||
jid, _ := types.ParseJID(string(portal.ID))
|
||||
if jid.Server == types.GroupServer {
|
||||
return &bridgev2.ChatInfo{}, nil
|
||||
}
|
||||
if networkid.UserLoginID(jid.User) != wa.UserLogin.ID {
|
||||
members.Members = append(members.Members, bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(&jid),
|
||||
Membership: event.MembershipJoin,
|
||||
})
|
||||
}
|
||||
|
||||
return &bridgev2.ChatInfo{
|
||||
Members: members,
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetUserInfo(_ context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
|
||||
jid := types.NewJID(string(ghost.ID), types.DefaultUserServer)
|
||||
const (
|
||||
nobodyPL = 99
|
||||
superAdminPL = 75
|
||||
adminPL = 50
|
||||
defaultPL = 0
|
||||
)
|
||||
|
||||
contact, err := wa.Client.Store.Contacts.GetContact(jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (wa *WhatsAppClient) wrapGroupInfo(info *types.GroupInfo) *bridgev2.ChatInfo {
|
||||
sendEventPL := defaultPL
|
||||
if info.IsAnnounce {
|
||||
sendEventPL = adminPL
|
||||
}
|
||||
|
||||
// make user info from contact info
|
||||
return wa.contactToUserInfo(jid, contact), nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) contactToUserInfo(jid types.JID, contact types.ContactInfo) *bridgev2.UserInfo {
|
||||
isBot := false
|
||||
ui := &bridgev2.UserInfo{
|
||||
IsBot: &isBot,
|
||||
Identifiers: []string{},
|
||||
metaChangePL := defaultPL
|
||||
if info.IsLocked {
|
||||
metaChangePL = adminPL
|
||||
}
|
||||
name := wa.Main.Config.FormatDisplayname(jid, contact)
|
||||
ui.Name = &name
|
||||
ui.Identifiers = append(ui.Identifiers, "tel:+"+jid.User)
|
||||
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
|
||||
Preview: false,
|
||||
IsCommunity: false,
|
||||
})
|
||||
|
||||
if avatar != nil && err == nil {
|
||||
ui.Avatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
wrapped := &bridgev2.ChatInfo{
|
||||
Name: ptr.Ptr(info.Name),
|
||||
Topic: ptr.Ptr(info.Topic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: !info.IsIncognito,
|
||||
TotalMemberCount: len(info.Participants),
|
||||
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
EventsDefault: &sendEventPL,
|
||||
StateDefault: ptr.Ptr(nobodyPL),
|
||||
Ban: ptr.Ptr(nobodyPL),
|
||||
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: metaChangePL,
|
||||
event.StateRoomAvatar: metaChangePL,
|
||||
event.StateTopic: metaChangePL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
Disappear: &database.DisappearingSetting{
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(info.DisappearingTimer) * time.Second,
|
||||
},
|
||||
}
|
||||
for _, pcp := range info.Participants {
|
||||
if pcp.JID.Server != types.DefaultUserServer {
|
||||
continue
|
||||
}
|
||||
member := bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(pcp.JID),
|
||||
Membership: event.MembershipJoin,
|
||||
}
|
||||
if pcp.IsSuperAdmin {
|
||||
member.PowerLevel = ptr.Ptr(superAdminPL)
|
||||
} else if pcp.IsAdmin {
|
||||
member.PowerLevel = ptr.Ptr(adminPL)
|
||||
} else {
|
||||
member.PowerLevel = ptr.Ptr(defaultPL)
|
||||
}
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
|
||||
}
|
||||
|
||||
return ui
|
||||
if !info.LinkedParentJID.IsEmpty() {
|
||||
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
|
||||
}
|
||||
if info.IsParent {
|
||||
wrapped.Type = ptr.Ptr(database.RoomTypeSpace)
|
||||
} else {
|
||||
wrapped.Type = ptr.Ptr(database.RoomTypeDefault)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
contacts, err := wa.Client.Store.Contacts.GetAllContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*bridgev2.ResolveIdentifierResponse, len(contacts))
|
||||
index := 0
|
||||
for jid := range contacts {
|
||||
contact := contacts[jid]
|
||||
contactResp := &bridgev2.ResolveIdentifierResponse{
|
||||
UserInfo: wa.contactToUserInfo(jid, contact),
|
||||
//Chat: wa.makeCreateDMResponse(contact), TODO implement
|
||||
func (wa *WhatsAppClient) wrapNewsletterInfo(info *types.NewsletterMetadata) *bridgev2.ChatInfo {
|
||||
ownPowerLevel := defaultPL
|
||||
var mutedUntil *time.Time
|
||||
if info.ViewerMeta != nil {
|
||||
switch info.ViewerMeta.Role {
|
||||
case types.NewsletterRoleAdmin:
|
||||
ownPowerLevel = adminPL
|
||||
case types.NewsletterRoleOwner:
|
||||
ownPowerLevel = superAdminPL
|
||||
}
|
||||
contactResp.UserID = waid.MakeUserID(&jid)
|
||||
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, contactResp.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost for %s: %w", jid, err)
|
||||
switch info.ViewerMeta.Mute {
|
||||
case types.NewsletterMuteOn:
|
||||
mutedUntil = &event.MutedForever
|
||||
case types.NewsletterMuteOff:
|
||||
mutedUntil = &bridgev2.Unmuted
|
||||
}
|
||||
contactResp.Ghost = ghost
|
||||
|
||||
resp[index] = contactResp
|
||||
index++
|
||||
}
|
||||
return resp, nil
|
||||
avatar := &bridgev2.Avatar{}
|
||||
if info.ThreadMeta.Picture != nil {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else if info.ThreadMeta.Preview.ID != "" {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
meta, err := wa.Client.GetNewsletterInfo(info.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch full res avatar info: %w", err)
|
||||
} else if meta.ThreadMeta.Picture == nil {
|
||||
return nil, fmt.Errorf("full res avatar info is missing")
|
||||
}
|
||||
return wa.Client.DownloadMediaWithPath(meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else {
|
||||
avatar.ID = "remove"
|
||||
avatar.Remove = true
|
||||
}
|
||||
return &bridgev2.ChatInfo{
|
||||
Name: ptr.Ptr(info.ThreadMeta.Name.Text),
|
||||
Topic: ptr.Ptr(info.ThreadMeta.Description.Text),
|
||||
Avatar: avatar,
|
||||
UserLocal: &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: mutedUntil,
|
||||
},
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
TotalMemberCount: info.ThreadMeta.SubscriberCount,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.JID): {
|
||||
EventSender: wa.makeEventSender(wa.JID),
|
||||
PowerLevel: &ownPowerLevel,
|
||||
},
|
||||
},
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
EventsDefault: ptr.Ptr(adminPL),
|
||||
StateDefault: ptr.Ptr(nobodyPL),
|
||||
Ban: ptr.Ptr(nobodyPL),
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: adminPL,
|
||||
event.StateRoomAvatar: adminPL,
|
||||
event.StateTopic: adminPL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,20 +6,15 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridge/status"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
@ -32,11 +27,11 @@ type respGetProxy struct {
|
|||
ProxyURL string `json:"proxy_url"`
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) getProxy(reason string) (string, error) {
|
||||
if wa.Main.Config.GetProxyURL == "" {
|
||||
return wa.Main.Config.Proxy, nil
|
||||
func (wa *WhatsAppConnector) getProxy(reason string) (string, error) {
|
||||
if wa.Config.GetProxyURL == "" {
|
||||
return wa.Config.Proxy, nil
|
||||
}
|
||||
parsed, err := url.Parse(wa.Main.Config.GetProxyURL)
|
||||
parsed, err := url.Parse(wa.Config.GetProxyURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse address: %w", err)
|
||||
}
|
||||
|
@ -62,81 +57,20 @@ func (wa *WhatsAppClient) getProxy(reason string) (string, error) {
|
|||
return respData.ProxyURL, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) MakeNewClient(log zerolog.Logger) {
|
||||
wa.Client = whatsmeow.NewClient(wa.Device, waLog.Zerolog(log))
|
||||
|
||||
if wa.Device.ID != nil {
|
||||
wa.Client.AddEventHandler(wa.handleWAEvent)
|
||||
wa.Client.EnableAutoReconnect = true
|
||||
} else {
|
||||
wa.Client.EnableAutoReconnect = false // no auto reconnect unless we are logged in
|
||||
func (wa *WhatsAppConnector) updateProxy(client *whatsmeow.Client, isLogin bool) error {
|
||||
if wa.Config.ProxyOnlyLogin && !isLogin {
|
||||
return nil
|
||||
}
|
||||
|
||||
wa.Client.AutomaticMessageRerequestFromPhone = true
|
||||
wa.Client.SetForceActiveDeliveryReceipts(wa.Main.Config.ForceActiveDeliveryReceipts)
|
||||
|
||||
//TODO: add tracking under PreRetryCallback and GetMessageForRetry (waiting till metrics/analytics are added)
|
||||
if wa.Main.Config.ProxyOnlyLogin || wa.Device.ID == nil {
|
||||
if proxy, err := wa.getProxy("login"); err != nil {
|
||||
wa.UserLogin.Log.Err(err).Msg("Failed to get proxy address")
|
||||
} else if err = wa.Client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{
|
||||
NoMedia: wa.Main.Config.ProxyOnlyLogin,
|
||||
}); err != nil {
|
||||
wa.UserLogin.Log.Err(err).Msg("Failed to set proxy address")
|
||||
}
|
||||
reason := "connect"
|
||||
if isLogin {
|
||||
reason = "login"
|
||||
}
|
||||
if wa.Main.Config.ProxyOnlyLogin {
|
||||
wa.Client.ToggleProxyOnlyForLogin(true)
|
||||
if proxy, err := wa.getProxy(reason); err != nil {
|
||||
return fmt.Errorf("failed to get proxy address: %w", err)
|
||||
} else if err = client.SetProxyAddress(proxy); err != nil {
|
||||
return fmt.Errorf("failed to set proxy address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.MessageKey {
|
||||
key := &waCommon.MessageKey{
|
||||
RemoteJID: ptr.Ptr(id.Chat.String()),
|
||||
ID: ptr.Ptr(id.ID),
|
||||
}
|
||||
if id.Sender.User == string(wa.UserLogin.ID) {
|
||||
key.FromMe = ptr.Ptr(true)
|
||||
}
|
||||
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer {
|
||||
key.Participant = ptr.Ptr(id.Sender.String())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) keyToMessageID(chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
|
||||
sender = sender.ToNonAD()
|
||||
var err error
|
||||
if !key.GetFromMe() {
|
||||
if key.GetParticipant() != "" {
|
||||
sender, err = types.ParseJID(key.GetParticipant())
|
||||
if err != nil {
|
||||
// TODO log somehow?
|
||||
return ""
|
||||
}
|
||||
if sender.Server == types.LegacyUserServer {
|
||||
sender.Server = types.DefaultUserServer
|
||||
}
|
||||
} else if chat.Server == types.DefaultUserServer {
|
||||
ownID := ptr.Val(wa.Device.ID).ToNonAD()
|
||||
if sender.User == ownID.User {
|
||||
sender = chat
|
||||
} else {
|
||||
sender = ownID
|
||||
}
|
||||
} else {
|
||||
// TODO log somehow?
|
||||
return ""
|
||||
}
|
||||
}
|
||||
remoteJID, err := types.ParseJID(key.GetRemoteJID())
|
||||
if err == nil && !remoteJID.IsEmpty() {
|
||||
// TODO use remote jid in other cases?
|
||||
if remoteJID.Server == types.GroupServer {
|
||||
chat = remoteJID
|
||||
}
|
||||
}
|
||||
return waid.MakeMessageID(chat, sender, key.GetID())
|
||||
return nil
|
||||
}
|
||||
|
||||
type WhatsAppClient struct {
|
||||
|
@ -144,38 +78,7 @@ type WhatsAppClient struct {
|
|||
UserLogin *bridgev2.UserLogin
|
||||
Client *whatsmeow.Client
|
||||
Device *store.Device
|
||||
}
|
||||
|
||||
var whatsappCaps = &bridgev2.NetworkRoomCapabilities{
|
||||
FormattedText: true,
|
||||
UserMentions: true,
|
||||
LocationMessages: true,
|
||||
Captions: true,
|
||||
Replies: true,
|
||||
Edits: true,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: 15 * time.Minute,
|
||||
Deletes: true,
|
||||
DeleteMaxAge: 48 * time.Hour,
|
||||
DefaultFileRestriction: &bridgev2.FileRestriction{
|
||||
// 100MB is the limit for videos by default.
|
||||
// HQ on images and videos can be enabled
|
||||
// Documents can do 2GB, TODO implementation
|
||||
MaxSize: 100 * 1024 * 1024,
|
||||
},
|
||||
//TODO: implement
|
||||
Files: map[event.MessageType]bridgev2.FileRestriction{},
|
||||
ReadReceipts: true,
|
||||
Reactions: true,
|
||||
ReactionCount: 1,
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetCapabilities(_ context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities {
|
||||
if portal.Receiver == wa.UserLogin.ID && portal.ID == networkid.PortalID(wa.UserLogin.ID) {
|
||||
// note to self mode, not implemented yet
|
||||
return nil
|
||||
}
|
||||
return whatsappCaps
|
||||
JID types.JID
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -211,6 +114,29 @@ func (wa *WhatsAppClient) RegisterPushNotifications(_ context.Context, pushType
|
|||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
|
||||
return userID == waid.MakeUserID(wa.JID)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Connect(ctx context.Context) error {
|
||||
if wa.Client == nil {
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
Error: WANotLoggedIn,
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
return nil
|
||||
}
|
||||
if err := wa.Main.updateProxy(wa.Client, false); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
|
||||
}
|
||||
return wa.Client.Connect()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
wa.Client.Disconnect()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
|
||||
if wa.Client == nil {
|
||||
return
|
||||
|
@ -221,44 +147,6 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
|
||||
if wa.Client == nil {
|
||||
return false
|
||||
}
|
||||
return userID == networkid.UserID(wa.Client.Store.ID.User)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Connect(_ context.Context) error {
|
||||
if wa.Client != nil {
|
||||
return wa.Client.Connect()
|
||||
} else {
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
Error: WANotLoggedIn,
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
wa.Client.Disconnect()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsLoggedIn() bool {
|
||||
if wa.Client == nil {
|
||||
return false
|
||||
}
|
||||
return wa.Client.IsLoggedIn()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) FullReconnect() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) canReconnect() bool { return false }
|
||||
|
||||
func (wa *WhatsAppClient) resetWADevice() {
|
||||
wa.Device = nil
|
||||
wa.UserLogin.Metadata.(*UserLoginMetadata).WADeviceID = 0
|
||||
return wa.Client != nil && wa.Client.IsLoggedIn()
|
||||
}
|
||||
|
|
|
@ -7,15 +7,21 @@ import (
|
|||
|
||||
up "go.mau.fi/util/configupgrade"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"gopkg.in/yaml.v3"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
type MediaRequestMethod string
|
||||
|
||||
const (
|
||||
MediaRequestMethodImmediate MediaRequestMethod = "immediate"
|
||||
MediaRequestMethodLocalTime = "local_time"
|
||||
)
|
||||
|
||||
//go:embed example-config.yaml
|
||||
var ExampleConfig string
|
||||
|
||||
type WhatsAppConfig struct {
|
||||
type Config struct {
|
||||
OSName string `yaml:"os_name"`
|
||||
BrowserName string `yaml:"browser_name"`
|
||||
|
||||
|
@ -25,7 +31,15 @@ type WhatsAppConfig struct {
|
|||
|
||||
DisplaynameTemplate string `yaml:"displayname_template"`
|
||||
|
||||
CallStartNotices bool `yaml:"call_start_notices"`
|
||||
CallStartNotices bool `yaml:"call_start_notices"`
|
||||
SendPresenceOnTyping bool `yaml:"send_presence_on_typing"`
|
||||
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
|
||||
DisableStatusBroadcastSend bool `yaml:"disable_status_broadcast_send"`
|
||||
MuteStatusBroadcast bool `yaml:"mute_status_broadcast"`
|
||||
StatusBroadcastTag event.RoomTag `yaml:"status_broadcast_tag"`
|
||||
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
|
||||
URLPreviews bool `yaml:"url_previews"`
|
||||
ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"`
|
||||
|
||||
HistorySync struct {
|
||||
RequestFullSync bool `yaml:"request_full_sync"`
|
||||
|
@ -43,25 +57,22 @@ type WhatsAppConfig struct {
|
|||
} `yaml:"media_requests"`
|
||||
} `yaml:"history_sync"`
|
||||
|
||||
UserAvatarSync bool `yaml:"user_avatar_sync"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
}
|
||||
|
||||
SendPresenceOnTyping bool `yaml:"send_presence_on_typing"`
|
||||
type umConfig Config
|
||||
|
||||
ArchiveTag event.RoomTag `yaml:"archive_tag"`
|
||||
PinnedTag event.RoomTag `yaml:"pinned_tag"`
|
||||
TagOnlyOnCreate bool `yaml:"tag_only_on_create"`
|
||||
func (c *Config) UnmarshalYAML(node *yaml.Node) error {
|
||||
err := node.Decode((*umConfig)(c))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
EnableStatusBroadcast bool `yaml:"enable_status_broadcast"`
|
||||
DisableStatusBroadcastSend bool `yaml:"disable_status_broadcast_send"`
|
||||
MuteStatusBroadcast bool `yaml:"mute_status_broadcast"`
|
||||
StatusBroadcastTag event.RoomTag `yaml:"status_broadcast_tag"`
|
||||
|
||||
WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"`
|
||||
|
||||
URLPreviews bool `yaml:"url_previews"`
|
||||
|
||||
ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"`
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeConfig(helper up.Helper) {
|
||||
|
@ -75,34 +86,22 @@ func upgradeConfig(helper up.Helper) {
|
|||
helper.Copy(up.Str, "displayname_template")
|
||||
|
||||
helper.Copy(up.Bool, "call_start_notices")
|
||||
|
||||
helper.Copy(up.Bool, "history_sync", "request_full_sync")
|
||||
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "days_limit")
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "size_mb_limit")
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "storage_quota_mb")
|
||||
|
||||
helper.Copy(up.Bool, "history_sync", "media_requests", "auto_request_media")
|
||||
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
||||
|
||||
helper.Copy(up.Bool, "user_avatar_sync")
|
||||
|
||||
helper.Copy(up.Bool, "send_presence_on_typing")
|
||||
|
||||
helper.Copy(up.Str, "archive_tag")
|
||||
helper.Copy(up.Str, "pinned_tag")
|
||||
helper.Copy(up.Bool, "tag_only_on_create")
|
||||
|
||||
helper.Copy(up.Bool, "enable_status_broadcast")
|
||||
helper.Copy(up.Bool, "disable_status_broadcast_send")
|
||||
helper.Copy(up.Bool, "mute_status_broadcast")
|
||||
helper.Copy(up.Str, "status_broadcast_tag")
|
||||
|
||||
helper.Copy(up.Bool, "whatsapp_thumbnail")
|
||||
|
||||
helper.Copy(up.Bool, "url_previews")
|
||||
|
||||
helper.Copy(up.Bool, "history_sync", "request_full_sync")
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "days_limit")
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "size_mb_limit")
|
||||
helper.Copy(up.Int, "history_sync", "full_sync_config", "storage_quota_mb")
|
||||
helper.Copy(up.Bool, "history_sync", "media_requests", "auto_request_media")
|
||||
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
||||
}
|
||||
|
||||
type DisplaynameParams struct {
|
||||
|
@ -110,7 +109,7 @@ type DisplaynameParams struct {
|
|||
Phone string
|
||||
}
|
||||
|
||||
func (c *WhatsAppConfig) FormatDisplayname(jid types.JID, contact types.ContactInfo) string {
|
||||
func (c *Config) FormatDisplayname(jid types.JID, contact types.ContactInfo) string {
|
||||
var nameBuf strings.Builder
|
||||
err := c.displaynameTemplate.Execute(&nameBuf, &DisplaynameParams{
|
||||
ContactInfo: contact,
|
||||
|
@ -123,5 +122,14 @@ func (c *WhatsAppConfig) FormatDisplayname(jid types.JID, contact types.ContactI
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) {
|
||||
return ExampleConfig, wa.Config, up.SimpleUpgrader(upgradeConfig)
|
||||
return ExampleConfig, &wa.Config, &up.StructUpgrader{
|
||||
SimpleUpgrader: up.SimpleUpgrader(upgradeConfig),
|
||||
Blocks: [][]string{
|
||||
{"proxy"},
|
||||
{"displayname_template"},
|
||||
{"call_start_notices"},
|
||||
{"history_sync"},
|
||||
},
|
||||
Base: ExampleConfig,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package connector
|
|||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
|
@ -19,7 +18,7 @@ import (
|
|||
|
||||
type WhatsAppConnector struct {
|
||||
Bridge *bridgev2.Bridge
|
||||
Config *WhatsAppConfig
|
||||
Config Config
|
||||
DeviceStore *sqlstore.Container
|
||||
MsgConv *msgconv.MessageConverter
|
||||
}
|
||||
|
@ -27,23 +26,8 @@ type WhatsAppConnector struct {
|
|||
var _ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil)
|
||||
var _ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil)
|
||||
|
||||
func NewConnector() *WhatsAppConnector {
|
||||
return &WhatsAppConnector{
|
||||
Config: &WhatsAppConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) SetMaxFileSize(_ int64) {
|
||||
println("SetMaxFileSize unimplemented")
|
||||
}
|
||||
|
||||
var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
||||
DisappearingMessages: true,
|
||||
AggressiveUpdateInfo: false,
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
|
||||
return WhatsAppGeneralCaps
|
||||
func (wa *WhatsAppConnector) SetMaxFileSize(maxSize int64) {
|
||||
wa.MsgConv.MaxFileSize = maxSize
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetName() bridgev2.BridgeName {
|
||||
|
@ -58,12 +42,6 @@ func (wa *WhatsAppConnector) GetName() bridgev2.BridgeName {
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
|
||||
var err error
|
||||
wa.Config.displaynameTemplate, err = template.New("displayname").Parse(wa.Config.DisplaynameTemplate)
|
||||
if err != nil {
|
||||
// TODO return error or do this later?
|
||||
panic(err)
|
||||
}
|
||||
wa.Bridge = bridge
|
||||
wa.MsgConv = msgconv.New(bridge)
|
||||
|
||||
|
@ -110,8 +88,7 @@ func (wa *WhatsAppConnector) Start(_ context.Context) error {
|
|||
func (wa *WhatsAppConnector) LoadUserLogin(_ context.Context, login *bridgev2.UserLogin) error {
|
||||
loginMetadata := login.Metadata.(*UserLoginMetadata)
|
||||
|
||||
jid := waid.ParseWAUserLoginID(login.ID)
|
||||
jid.Device = loginMetadata.WADeviceID
|
||||
jid := waid.ParseUserLoginID(login.ID, loginMetadata.WADeviceID)
|
||||
|
||||
device, err := wa.DeviceStore.GetDevice(jid)
|
||||
if err != nil {
|
||||
|
@ -122,12 +99,16 @@ func (wa *WhatsAppConnector) LoadUserLogin(_ context.Context, login *bridgev2.Us
|
|||
Main: wa,
|
||||
UserLogin: login,
|
||||
Device: device,
|
||||
JID: jid,
|
||||
}
|
||||
|
||||
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
|
||||
|
||||
if device != nil {
|
||||
w.MakeNewClient(log)
|
||||
w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log))
|
||||
w.Client.AddEventHandler(w.handleWAEvent)
|
||||
w.Client.AutomaticMessageRerequestFromPhone = true
|
||||
w.Client.SetForceActiveDeliveryReceipts(wa.Config.ForceActiveDeliveryReceipts)
|
||||
}
|
||||
|
||||
login.Client = w
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"go.mau.fi/util/jsontime"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) GetDBMetaTypes() database.MetaTypes {
|
||||
println("GetDBMetaTypes unimplemented")
|
||||
return database.MetaTypes{
|
||||
Ghost: nil,
|
||||
Message: nil,
|
||||
Reaction: nil,
|
||||
Portal: nil,
|
||||
UserLogin: func() any { return &UserLoginMetadata{} },
|
||||
Ghost: func() any {
|
||||
return &GhostMetadata{}
|
||||
},
|
||||
Message: nil,
|
||||
Reaction: nil,
|
||||
Portal: func() any {
|
||||
return &PortalMetadata{}
|
||||
},
|
||||
UserLogin: func() any {
|
||||
return &UserLoginMetadata{}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,3 +25,12 @@ type UserLoginMetadata struct {
|
|||
WADeviceID uint16 `json:"wa_device_id"`
|
||||
//TODO: Add phone last ping/seen
|
||||
}
|
||||
|
||||
type PortalMetadata struct {
|
||||
DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"`
|
||||
}
|
||||
|
||||
type GhostMetadata struct {
|
||||
AvatarFetchAttempted bool `json:"avatar_fetch_attempted,omitempty"`
|
||||
LastSync jsontime.Unix `json:"last_sync,omitempty"`
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
@ -15,12 +17,13 @@ import (
|
|||
|
||||
type WAMessageEvent struct {
|
||||
*events.Message
|
||||
portalKey networkid.PortalKey
|
||||
wa *WhatsAppClient
|
||||
wa *WhatsAppClient
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.RemoteMessage = (*WAMessageEvent)(nil)
|
||||
_ bridgev2.RemoteMessageWithTransactionID = (*WAMessageEvent)(nil)
|
||||
_ bridgev2.RemoteEventWithTimestamp = (*WAMessageEvent)(nil)
|
||||
_ bridgev2.RemoteEventThatMayCreatePortal = (*WAMessageEvent)(nil)
|
||||
_ bridgev2.RemoteReaction = (*WAMessageEvent)(nil)
|
||||
_ bridgev2.RemoteReactionRemove = (*WAMessageEvent)(nil)
|
||||
|
@ -29,13 +32,13 @@ var (
|
|||
)
|
||||
|
||||
func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
|
||||
// only change text and captions
|
||||
if len(existing) > 1 {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Got edit to message with multiple parts")
|
||||
}
|
||||
editedMsg := evt.Message.Message.GetProtocolMessage().GetEditedMessage()
|
||||
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, editedMsg, nil) // for media messages, it is better not to do this
|
||||
// TODO edits to media captions may not contain the media
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, editedMsg)
|
||||
return &bridgev2.ConvertedEdit{
|
||||
ModifiedParts: []*bridgev2.ConvertedEditPart{cm.Parts[0].ToEditPart(existing[0])},
|
||||
}, nil
|
||||
|
@ -71,11 +74,7 @@ func (evt *WAMessageEvent) GetTargetMessage() networkid.MessageID {
|
|||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetReactionEmoji() (string, networkid.EmojiID) {
|
||||
if reactionMsg := evt.Message.Message.GetReactionMessage(); reactionMsg != nil {
|
||||
return reactionMsg.GetText(), ""
|
||||
} else {
|
||||
return "", ""
|
||||
}
|
||||
return evt.Message.Message.GetReactionMessage().GetText(), ""
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetRemovedEmojiID() networkid.EmojiID {
|
||||
|
@ -88,16 +87,16 @@ func (evt *WAMessageEvent) ShouldCreatePortal() bool {
|
|||
|
||||
func (evt *WAMessageEvent) GetType() bridgev2.RemoteEventType {
|
||||
waMsg := evt.Message.Message
|
||||
if reactionMsg := waMsg.GetReactionMessage(); reactionMsg != nil {
|
||||
if reactionMsg.GetText() == "" {
|
||||
if waMsg.ReactionMessage != nil {
|
||||
if waMsg.ReactionMessage.GetText() == "" {
|
||||
return bridgev2.RemoteEventReactionRemove
|
||||
}
|
||||
return bridgev2.RemoteEventReaction
|
||||
} else if protocolMsg := waMsg.GetProtocolMessage(); protocolMsg != nil {
|
||||
protocolType := protocolMsg.GetType()
|
||||
if protocolType == 0 { // REVOKE (message deletes)
|
||||
} else if waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.Type != nil {
|
||||
switch *waMsg.ProtocolMessage.Type {
|
||||
case waE2E.ProtocolMessage_REVOKE:
|
||||
return bridgev2.RemoteEventMessageRemove
|
||||
} else if protocolType == 14 { // Message edits
|
||||
case waE2E.ProtocolMessage_MESSAGE_EDIT:
|
||||
return bridgev2.RemoteEventEdit
|
||||
}
|
||||
}
|
||||
|
@ -105,21 +104,30 @@ func (evt *WAMessageEvent) GetType() bridgev2.RemoteEventType {
|
|||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetPortalKey() networkid.PortalKey {
|
||||
return evt.portalKey
|
||||
return evt.wa.makeWAPortalKey(evt.Info.Chat)
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) AddLogContext(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("message_id", evt.Info.ID).Uint64("sender_id", evt.Info.Sender.UserInt())
|
||||
return c.Str("message_id", evt.Info.ID).Stringer("sender_id", evt.Info.Sender)
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetTimestamp() time.Time {
|
||||
return evt.Info.Timestamp
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetSender() bridgev2.EventSender {
|
||||
return evt.wa.makeEventSender(&evt.Info.Sender)
|
||||
return evt.wa.makeEventSender(evt.Info.Sender)
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) GetID() networkid.MessageID {
|
||||
return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID)
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
||||
return evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, evt.Message.Message, &evt.Message.Info), nil
|
||||
func (evt *WAMessageEvent) GetTransactionID() networkid.TransactionID {
|
||||
// TODO for newsletter messages, there's a different transaction ID
|
||||
return networkid.TransactionID(evt.GetID())
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
||||
return evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, evt.Message.Message), nil
|
||||
}
|
||||
|
|
|
@ -21,13 +21,40 @@ displayname_template: "{{or .FullName .BusinessName .PushName .Phone}} (WA)"
|
|||
|
||||
# Should incoming calls send a message to the Matrix room?
|
||||
call_start_notices: 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
|
||||
# users to see when the whatsapp user on the other side is typing during a conversation.
|
||||
send_presence_on_typing: false
|
||||
# Should WhatsApp status messages be bridged into a Matrix room?
|
||||
# Disabling this won't affect already created status broadcast rooms.
|
||||
enable_status_broadcast: true
|
||||
# Should sending WhatsApp status messages be allowed?
|
||||
# This can cause issues if the user has lots of contacts, so it's disabled by default.
|
||||
disable_status_broadcast_send: true
|
||||
# Should the status broadcast room be muted and moved into low priority by default?
|
||||
# This is only applied when creating the room, the user can unmute it later.
|
||||
mute_status_broadcast: true
|
||||
# Tag to apply to the status broadcast room.
|
||||
status_broadcast_tag: m.lowpriority
|
||||
# Should the bridge use thumbnails from WhatsApp?
|
||||
# They're disabled by default due to very low resolution.
|
||||
whatsapp_thumbnail: false
|
||||
# Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview,
|
||||
# and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews`
|
||||
# key in the event content even if this is disabled.
|
||||
url_previews: false
|
||||
# Should the bridge always send "active" delivery receipts (two gray ticks on WhatsApp)
|
||||
# even if the user isn't marked as online (e.g. when presence bridging isn't enabled)?
|
||||
#
|
||||
# By default, the bridge acts like WhatsApp web, which only sends active delivery
|
||||
# receipts when it's in the foreground.
|
||||
force_active_delivery_receipts: false
|
||||
|
||||
# Settings for handling history sync payloads.
|
||||
history_sync:
|
||||
# Should the bridge request a full sync from the phone when logging in?
|
||||
# This bumps the size of history syncs from 3 months to 1 year.
|
||||
request_full_sync: false
|
||||
|
||||
# Configuration parameters that are sent to the phone along with the request full sync flag.
|
||||
# By default, (when the values are null or 0), the config isn't sent at all.
|
||||
full_sync_config:
|
||||
|
@ -38,7 +65,6 @@ history_sync:
|
|||
size_mb_limit: null
|
||||
# This is presumably the local storage quota, which may affect what the phone includes in the history sync blob.
|
||||
storage_quota_mb: null
|
||||
|
||||
# Settings for media requests. If the media expired, then it will not be on the WA servers.
|
||||
# Media can always be requested by reacting with the ♻️ (recycle) emoji.
|
||||
# These settings determine if the media requests should be done automatically during or after backfill.
|
||||
|
@ -52,48 +78,3 @@ history_sync:
|
|||
request_local_time: 120
|
||||
# Maximum number of media request responses to handle in parallel per user.
|
||||
max_async_handle: 2
|
||||
|
||||
# Should puppet avatars be fetched from the server even if an avatar is already set?
|
||||
user_avatar_sync: 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
|
||||
# users to see when the whatsapp user on the other side is typing during a conversation.
|
||||
send_presence_on_typing: false
|
||||
|
||||
# When using double puppeting, should the archived chats be moved to a specific tag in Matrix?
|
||||
# Note that WhatsApp un-archives chats when a message is received, which will also be mirrored to Matrix.
|
||||
# This can be set to a tag (e.g. m.lowpriority), or null to disable.
|
||||
archive_tag: null
|
||||
# Same as above, but for pinned chats. The favorite tag is called m.favourite
|
||||
pinned_tag: null
|
||||
# Should mute status and tags only be bridged when the portal room is created?
|
||||
tag_only_on_create: true
|
||||
|
||||
# Should WhatsApp status messages be bridged into a Matrix room?
|
||||
# Disabling this won't affect already created status broadcast rooms.
|
||||
enable_status_broadcast: true
|
||||
# Should sending WhatsApp status messages be allowed?
|
||||
# This can cause issues if the user has lots of contacts, so it's disabled by default.
|
||||
disable_status_broadcast_send: true
|
||||
# Should the status broadcast room be muted and moved into low priority by default?
|
||||
# This is only applied when creating the room, the user can unmute it later.
|
||||
mute_status_broadcast: true
|
||||
# Tag to apply to the status broadcast room.
|
||||
status_broadcast_tag: m.lowpriority
|
||||
|
||||
# Should the bridge use thumbnails from WhatsApp?
|
||||
# They're disabled by default due to very low resolution.
|
||||
whatsapp_thumbnail: false
|
||||
|
||||
# Should the bridge detect URLs in outgoing messages, ask the homeserver to generate a preview,
|
||||
# and send it to WhatsApp? URL previews can always be sent using the `com.beeper.linkpreviews`
|
||||
# key in the event content even if this is disabled.
|
||||
url_previews: false
|
||||
|
||||
# Should the bridge always send "active" delivery receipts (two gray ticks on WhatsApp)
|
||||
# even if the user isn't marked as online (e.g. when presence bridging isn't enabled)?
|
||||
#
|
||||
# By default, the bridge acts like WhatsApp web, which only sends active delivery
|
||||
# receipts when it's in the foreground.
|
||||
force_active_delivery_receipts: false
|
||||
|
|
|
@ -32,11 +32,12 @@ func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2
|
|||
return nil, fmt.Errorf("failed to convert message: %w", err)
|
||||
}
|
||||
messageID := wa.Client.GenerateMessageID()
|
||||
chatJID, err := types.ParseJID(string(msg.Portal.ID))
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
senderJID := wa.Device.ID
|
||||
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, messageID)
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
||||
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, whatsmeow.SendRequestExtra{
|
||||
ID: messageID,
|
||||
})
|
||||
|
@ -45,10 +46,11 @@ func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2
|
|||
}
|
||||
return &bridgev2.MatrixMessageResponse{
|
||||
DB: &database.Message{
|
||||
ID: waid.MakeMessageID(chatJID, senderJID.ToNonAD(), messageID),
|
||||
ID: wrappedMsgID,
|
||||
SenderID: networkid.UserID(wa.UserLogin.ID),
|
||||
Timestamp: resp.Timestamp,
|
||||
},
|
||||
RemovePending: networkid.TransactionID(wrappedMsgID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -66,7 +68,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
return nil, err
|
||||
}
|
||||
|
||||
portalJID, err := types.ParseJID(string(msg.Portal.ID))
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -89,7 +91,7 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
|
|||
return err
|
||||
}
|
||||
|
||||
portalJID, err := types.ParseJID(string(msg.Portal.ID))
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -109,24 +111,32 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
|
|||
|
||||
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
|
||||
log := zerolog.Ctx(ctx)
|
||||
editID := wa.Client.GenerateMessageID()
|
||||
|
||||
messageID, err := waid.ParseMessageID(edit.EditTarget.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portalJID, err := types.ParseJID(string(edit.Portal.ID))
|
||||
portalJID, err := waid.ParsePortalID(edit.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//TODO: DO CONVERSION VIA msgconv FUNC
|
||||
//TODO: IMPLEMENT MEDIA CAPTION EDITS
|
||||
//TODO: PRESERVE MEDIA
|
||||
editMessage := wa.Client.BuildEdit(portalJID, messageID.ID, &waE2E.Message{
|
||||
Conversation: proto.String(edit.Content.Body),
|
||||
})
|
||||
waMsg, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, edit.Portal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert message: %w", err)
|
||||
}
|
||||
convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
|
||||
if edit.OrigSender == nil {
|
||||
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp)
|
||||
}
|
||||
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, editMessage)
|
||||
//wrappedMsgID := waid.MakeMessageID(portalJID, wa.JID, messageID)
|
||||
//edit.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, convertedEdit, whatsmeow.SendRequestExtra{
|
||||
ID: editID,
|
||||
})
|
||||
log.Trace().Any("response", resp).Msg("WhatsApp edit response")
|
||||
return err
|
||||
}
|
||||
|
@ -138,7 +148,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
return err
|
||||
}
|
||||
|
||||
portalJID, err := types.ParseJID(string(msg.Portal.ID))
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -150,47 +160,80 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReadReceipt(_ context.Context, receipt *bridgev2.MatrixReadReceipt) error {
|
||||
if receipt.ExactMessage == nil {
|
||||
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
|
||||
if !receipt.ReadUpTo.After(receipt.LastRead) {
|
||||
return nil
|
||||
}
|
||||
messageID, err := waid.ParseMessageID(receipt.ExactMessage.ID)
|
||||
if receipt.LastRead.IsZero() {
|
||||
receipt.LastRead = receipt.ReadUpTo.Add(-5 * time.Second)
|
||||
}
|
||||
portalJID, err := waid.ParsePortalID(receipt.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = wa.Client.MarkRead([]types.MessageID{messageID.ID}, time.Now(), messageID.Chat, messageID.Sender)
|
||||
messages, err := receipt.Portal.Bridge.DB.Message.GetMessagesBetweenTimeQuery(ctx, receipt.Portal.PortalKey, receipt.LastRead, receipt.ReadUpTo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get messages to mark as read: %w", err)
|
||||
} else if len(messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
log := zerolog.Ctx(ctx)
|
||||
log.Trace().
|
||||
Time("last_read", receipt.LastRead).
|
||||
Time("read_up_to", receipt.ReadUpTo).
|
||||
Int("message_count", len(messages)).
|
||||
Msg("Handling read receipt")
|
||||
messagesToRead := make(map[types.JID][]string)
|
||||
for _, msg := range messages {
|
||||
parsed, err := waid.ParseMessageID(msg.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if msg.SenderID == networkid.UserID(wa.UserLogin.ID) {
|
||||
continue
|
||||
}
|
||||
var key types.JID
|
||||
// In group chats, group receipts by sender. In DMs, just use blank key (no participant field).
|
||||
if parsed.Sender != parsed.Chat {
|
||||
key = parsed.Sender
|
||||
}
|
||||
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
|
||||
}
|
||||
for messageSender, ids := range messagesToRead {
|
||||
err = wa.Client.MarkRead(ids, receipt.Receipt.Timestamp, portalJID, messageSender)
|
||||
if err != nil {
|
||||
log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read")
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixTyping(_ context.Context, msg *bridgev2.MatrixTyping) error {
|
||||
portalJID, err := types.ParseJID(string(msg.Portal.ID))
|
||||
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var chatPresence types.ChatPresence
|
||||
var mediaPresence types.ChatPresenceMedia
|
||||
if msg.IsTyping {
|
||||
chatPresence = "composing"
|
||||
chatPresence = types.ChatPresenceComposing
|
||||
} else {
|
||||
chatPresence = "paused"
|
||||
chatPresence = types.ChatPresencePaused
|
||||
}
|
||||
switch msg.Type {
|
||||
case bridgev2.TypingTypeText:
|
||||
mediaPresence = ""
|
||||
mediaPresence = types.ChatPresenceMediaText
|
||||
case bridgev2.TypingTypeRecordingMedia:
|
||||
mediaPresence = "audio"
|
||||
mediaPresence = types.ChatPresenceMediaAudio
|
||||
case bridgev2.TypingTypeUploadingMedia:
|
||||
return nil
|
||||
}
|
||||
|
||||
err = wa.Client.SendChatPresence(portalJID, chatPresence, mediaPresence)
|
||||
|
||||
if wa.Main.Config.SendPresenceOnTyping {
|
||||
err = wa.Client.SendPresence(types.PresenceAvailable)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Warn().Err(err).Msg("Failed to set presence on typing")
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing")
|
||||
}
|
||||
}
|
||||
return err
|
||||
return wa.Client.SendChatPresence(portalJID, chatPresence, mediaPresence)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -16,10 +15,37 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
WADisconnected status.BridgeStateErrorCode = "wa-transient-disconnect"
|
||||
WAPermanentError status.BridgeStateErrorCode = "wa-unknown-permanent-error"
|
||||
WALoggedOut status.BridgeStateErrorCode = "wa-logged-out"
|
||||
WAMainDeviceGone status.BridgeStateErrorCode = "wa-main-device-gone"
|
||||
WAUnknownLogout status.BridgeStateErrorCode = "wa-unknown-logout"
|
||||
WANotConnected status.BridgeStateErrorCode = "wa-not-connected"
|
||||
WAConnecting status.BridgeStateErrorCode = "wa-connecting"
|
||||
WAKeepaliveTimeout status.BridgeStateErrorCode = "wa-keepalive-timeout"
|
||||
WAPhoneOffline status.BridgeStateErrorCode = "wa-phone-offline"
|
||||
WAConnectionFailed status.BridgeStateErrorCode = "wa-connection-failed"
|
||||
WADisconnected status.BridgeStateErrorCode = "wa-transient-disconnect"
|
||||
WAStreamReplaced status.BridgeStateErrorCode = "wa-stream-replaced"
|
||||
WAStreamError status.BridgeStateErrorCode = "wa-stream-error"
|
||||
WAClientOutdated status.BridgeStateErrorCode = "wa-client-outdated"
|
||||
WATemporaryBan status.BridgeStateErrorCode = "wa-temporary-ban"
|
||||
)
|
||||
|
||||
func init() {
|
||||
status.BridgeStateHumanErrors.Update(status.BridgeStateErrorMap{
|
||||
WALoggedOut: "You were logged out from another device. Relogin to continue using the bridge.",
|
||||
WAMainDeviceGone: "Your phone was logged out from WhatsApp. Relogin to continue using the bridge.",
|
||||
WAUnknownLogout: "You were logged out for an unknown reason. Relogin to continue using the bridge.",
|
||||
WANotConnected: "You're not connected to WhatsApp",
|
||||
WAConnecting: "Reconnecting to WhatsApp...",
|
||||
WAKeepaliveTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
|
||||
WAPhoneOffline: "Your phone hasn't been seen in over 12 days. The bridge is currently connected, but will get disconnected if you don't open the app soon.",
|
||||
WAConnectionFailed: "Connecting to the WhatsApp web servers failed.",
|
||||
WADisconnected: "Disconnected from WhatsApp. Trying to reconnect.",
|
||||
WAClientOutdated: "Connect failure: 405 client outdated. Bridge must be updated.",
|
||||
WAStreamReplaced: "Stream replaced: the bridge was started in another location.",
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) {
|
||||
log := wa.UserLogin.Log
|
||||
|
||||
|
@ -29,23 +55,14 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) {
|
|||
Any("info", evt.Info).
|
||||
Any("payload", evt.Message).
|
||||
Msg("Received WhatsApp message")
|
||||
|
||||
portalKey, ok := wa.makeWAPortalKey(evt.Info.Chat)
|
||||
if !ok {
|
||||
log.Warn().Stringer("chat", evt.Info.Chat).Msg("Ignoring WhatsApp message with unknown chat JID")
|
||||
if evt.Info.Chat.Server == types.HiddenUserServer || evt.Info.Sender.Server == types.HiddenUserServer {
|
||||
return
|
||||
}
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &WAMessageEvent{
|
||||
Message: evt,
|
||||
portalKey: portalKey,
|
||||
wa: wa,
|
||||
Message: evt,
|
||||
wa: wa,
|
||||
})
|
||||
case *events.Receipt:
|
||||
portalKey, ok := wa.makeWAPortalKey(evt.Chat)
|
||||
if !ok {
|
||||
log.Warn().Stringer("chat", evt.Chat).Msg("Ignoring WhatsApp receipt with unknown chat JID")
|
||||
return
|
||||
}
|
||||
var evtType bridgev2.RemoteEventType
|
||||
switch evt.Type {
|
||||
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
|
||||
|
@ -63,88 +80,74 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) {
|
|||
}
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.Receipt{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: evtType,
|
||||
LogContext: nil,
|
||||
PortalKey: portalKey,
|
||||
Sender: wa.makeEventSender(&evt.Sender),
|
||||
Timestamp: evt.Timestamp,
|
||||
Type: evtType,
|
||||
PortalKey: wa.makeWAPortalKey(evt.Chat),
|
||||
Sender: wa.makeEventSender(evt.Sender),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
Targets: targets,
|
||||
})
|
||||
case *events.ChatPresence:
|
||||
portalKey, ok := wa.makeWAPortalKey(evt.Chat)
|
||||
if !ok {
|
||||
log.Warn().Stringer("chat", evt.Chat).Msg("Ignoring WhatsApp receipt with unknown chat JID")
|
||||
return
|
||||
typingType := bridgev2.TypingTypeText
|
||||
timeout := 15 * time.Second
|
||||
if evt.Media == types.ChatPresenceMediaAudio {
|
||||
typingType = bridgev2.TypingTypeRecordingMedia
|
||||
}
|
||||
Type := bridgev2.TypingTypeText
|
||||
Timeout := 1000
|
||||
if evt.Media == "audio" {
|
||||
Type = bridgev2.TypingTypeRecordingMedia
|
||||
}
|
||||
if evt.State == "paused" {
|
||||
Timeout = 0
|
||||
if evt.State == types.ChatPresencePaused {
|
||||
timeout = 0
|
||||
}
|
||||
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.Typing{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventTyping,
|
||||
LogContext: nil,
|
||||
PortalKey: portalKey,
|
||||
Sender: wa.makeEventSender(&evt.Sender),
|
||||
PortalKey: wa.makeWAPortalKey(evt.Chat),
|
||||
Sender: wa.makeEventSender(evt.Sender),
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Timeout: time.Duration(Timeout),
|
||||
Type: Type,
|
||||
Timeout: timeout,
|
||||
Type: typingType,
|
||||
})
|
||||
case *events.Connected:
|
||||
log.Debug().Msg("Connected to WhatsApp socket")
|
||||
state := status.BridgeState{StateEvent: status.StateConnected}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
case *events.Disconnected:
|
||||
log.Debug().Msg("Disconnected from WhatsApp socket")
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateTransientDisconnect,
|
||||
Error: WADisconnected,
|
||||
// Don't send the normal transient disconnect state if we're already in a different transient disconnect state.
|
||||
// TODO remove this if/when the phone offline state is moved to a sub-state of CONNECTED
|
||||
if wa.UserLogin.BridgeState.GetPrev().Error != WAPhoneOffline && wa.PhoneRecentlySeen(false) {
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WADisconnected})
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
case events.PermanentDisconnect:
|
||||
switch e := evt.(type) {
|
||||
case *events.LoggedOut:
|
||||
if e.Reason == events.ConnectFailureLoggedOut && !e.OnConnect && wa.canReconnect() {
|
||||
wa.resetWADevice()
|
||||
log.Debug().Msg("Doing full reconnect after WhatsApp 401 error")
|
||||
go wa.FullReconnect()
|
||||
}
|
||||
case *events.ConnectFailure:
|
||||
if e.Reason == events.ConnectFailureNotFound {
|
||||
if wa.Client != nil {
|
||||
wa.Client.Disconnect()
|
||||
err := wa.Device.Delete()
|
||||
if err != nil {
|
||||
log.Error().Msg(fmt.Sprintf("Error deleting device %s", err.Error()))
|
||||
return
|
||||
}
|
||||
wa.resetWADevice()
|
||||
wa.Client = nil
|
||||
}
|
||||
log.Debug().Msg("Reconnecting e2ee client after WhatsApp 415 error")
|
||||
go func() {
|
||||
err := wa.Connect(context.Background())
|
||||
if err != nil {
|
||||
log.Error().Msg(fmt.Sprintf("Error connecting %s", err.Error()))
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
case *events.StreamError:
|
||||
var message string
|
||||
if evt.Code != "" {
|
||||
message = fmt.Sprintf("Unknown stream error with code %s", evt.Code)
|
||||
} else if children := evt.Raw.GetChildren(); len(children) > 0 {
|
||||
message = fmt.Sprintf("Unknown stream error (contains %s node)", children[0].Tag)
|
||||
} else {
|
||||
message = "Unknown stream error"
|
||||
}
|
||||
|
||||
state := status.BridgeState{
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: WAPermanentError,
|
||||
Message: evt.PermanentDisconnectDescription(),
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
Error: WAStreamError,
|
||||
Message: message,
|
||||
})
|
||||
case *events.StreamReplaced:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAStreamReplaced})
|
||||
case *events.ConnectFailure:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: status.BridgeStateErrorCode(fmt.Sprintf("wa-connect-failure-%d", evt.Reason)),
|
||||
Message: fmt.Sprintf("Unknown connection failure: %s (%s)", evt.Reason, evt.Message),
|
||||
})
|
||||
case *events.ClientOutdated:
|
||||
wa.UserLogin.Log.Error().Msg("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.")
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Error: WAClientOutdated})
|
||||
case *events.TemporaryBan:
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
Error: WATemporaryBan,
|
||||
Message: evt.String(),
|
||||
})
|
||||
default:
|
||||
log.Debug().Type("event_type", rawEvt).Msg("Unhandled WhatsApp event")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
@ -8,25 +10,68 @@ import (
|
|||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) makeWAPortalKey(chatJID types.JID) (key networkid.PortalKey, ok bool) {
|
||||
key.ID = waid.MakeWAPortalID(chatJID)
|
||||
func (wa *WhatsAppClient) makeWAPortalKey(chatJID types.JID) (key networkid.PortalKey) {
|
||||
key.ID = waid.MakePortalID(chatJID)
|
||||
switch chatJID.Server {
|
||||
case types.DefaultUserServer: //TODO: LID support + other types?
|
||||
|
||||
key.Receiver = wa.UserLogin.ID // does this also apply for groups ?!?!
|
||||
case types.GroupServer:
|
||||
fallthrough //TODO: HANDLE
|
||||
default:
|
||||
return
|
||||
case types.DefaultUserServer, types.HiddenUserServer, types.BroadcastServer:
|
||||
key.Receiver = wa.UserLogin.ID
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) makeEventSender(id *types.JID) bridgev2.EventSender {
|
||||
func (wa *WhatsAppClient) makeEventSender(id types.JID) bridgev2.EventSender {
|
||||
return bridgev2.EventSender{
|
||||
IsFromMe: waid.MakeUserLoginID(id) == wa.UserLogin.ID,
|
||||
Sender: waid.MakeUserID(id),
|
||||
SenderLogin: waid.MakeUserLoginID(id),
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.MessageKey {
|
||||
key := &waCommon.MessageKey{
|
||||
RemoteJID: ptr.Ptr(id.Chat.String()),
|
||||
ID: ptr.Ptr(id.ID),
|
||||
}
|
||||
if id.Sender.User == string(wa.UserLogin.ID) {
|
||||
key.FromMe = ptr.Ptr(true)
|
||||
}
|
||||
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer {
|
||||
key.Participant = ptr.Ptr(id.Sender.String())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) keyToMessageID(chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
|
||||
sender = sender.ToNonAD()
|
||||
var err error
|
||||
if !key.GetFromMe() {
|
||||
if key.GetParticipant() != "" {
|
||||
sender, err = types.ParseJID(key.GetParticipant())
|
||||
if err != nil {
|
||||
// TODO log somehow?
|
||||
return ""
|
||||
}
|
||||
if sender.Server == types.LegacyUserServer {
|
||||
sender.Server = types.DefaultUserServer
|
||||
}
|
||||
} else if chat.Server == types.DefaultUserServer {
|
||||
ownID := ptr.Val(wa.Device.ID).ToNonAD()
|
||||
if sender.User == ownID.User {
|
||||
sender = chat
|
||||
} else {
|
||||
sender = ownID
|
||||
}
|
||||
} else {
|
||||
// TODO log somehow?
|
||||
return ""
|
||||
}
|
||||
}
|
||||
remoteJID, err := types.ParseJID(key.GetRemoteJID())
|
||||
if err == nil && !remoteJID.IsEmpty() {
|
||||
// TODO use remote jid in other cases?
|
||||
if remoteJID.Server == types.GroupServer {
|
||||
chat = remoteJID
|
||||
}
|
||||
}
|
||||
return waid.MakeMessageID(chat, sender, key.GetID())
|
||||
}
|
||||
|
|
|
@ -3,229 +3,165 @@ package connector
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) GetLoginFlows() []bridgev2.LoginFlow {
|
||||
return []bridgev2.LoginFlow{
|
||||
{
|
||||
Name: "QR",
|
||||
Description: "Scan a QR code to pair the bridge to your WhatsApp account",
|
||||
ID: "qr",
|
||||
},
|
||||
{
|
||||
Name: "Pairing code",
|
||||
Description: "Input your phone number to get a pairing code, to pair the bridge to your WhatsApp account",
|
||||
ID: "pairing-code",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
|
||||
if flowID == "qr" {
|
||||
return &QRLogin{User: user, Main: wa}, nil
|
||||
} else if flowID == "pairing-code" {
|
||||
return &PairingCodeLogin{User: user, Main: wa}, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid login flow ID")
|
||||
}
|
||||
}
|
||||
|
||||
func makeQRChan(connector *WhatsAppConnector, user *bridgev2.User) (client *whatsmeow.Client, qrChan <-chan whatsmeow.QRChannelItem, cancelFunc context.CancelFunc, err error) {
|
||||
log := connector.Bridge.Log.With().
|
||||
Str("action", "login").
|
||||
Stringer("user_id", user.MXID).
|
||||
Logger()
|
||||
qrCtx, cancel := context.WithCancel(log.WithContext(context.Background()))
|
||||
cancelFunc = cancel
|
||||
|
||||
device := connector.DeviceStore.NewDevice()
|
||||
|
||||
wa := &WhatsAppClient{
|
||||
Main: connector,
|
||||
Device: device,
|
||||
}
|
||||
|
||||
wa.MakeNewClient(log)
|
||||
|
||||
client = wa.Client
|
||||
|
||||
qrChan, err = client.GetQRChannel(qrCtx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
err = client.Connect()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeUserLogin(ctx context.Context, user *bridgev2.User, client *whatsmeow.Client) (ul *bridgev2.UserLogin, err error) {
|
||||
newLoginID := waid.MakeUserLoginID(client.Store.ID)
|
||||
ul, err = user.NewLogin(ctx, &database.UserLogin{
|
||||
ID: newLoginID,
|
||||
RemoteName: client.Store.PushName,
|
||||
Metadata: &UserLoginMetadata{
|
||||
WADeviceID: client.Store.ID.Device,
|
||||
},
|
||||
}, &bridgev2.NewLoginParams{
|
||||
DeleteOnConflict: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
type QRLogin struct {
|
||||
User *bridgev2.User
|
||||
Main *WhatsAppConnector
|
||||
Client *whatsmeow.Client
|
||||
cancelChan context.CancelFunc
|
||||
QRChan <-chan whatsmeow.QRChannelItem
|
||||
|
||||
AccData *store.Device
|
||||
}
|
||||
|
||||
var _ bridgev2.LoginProcessDisplayAndWait = (*QRLogin)(nil)
|
||||
|
||||
func (qr *QRLogin) Cancel() {
|
||||
qr.cancelChan()
|
||||
qr.Client.Disconnect()
|
||||
}
|
||||
|
||||
const (
|
||||
LoginStepQR = "fi.mau.whatsapp.login.qr"
|
||||
LoginStepComplete = "fi.mau.whatsapp.login.complete"
|
||||
LoginStepPairingCodeInput = "fi.mau.whatsapp.login.pairing.code"
|
||||
LoginStepPairingCode = "fi.mau.whatsapp.login.pairing.code"
|
||||
LoginStepIDQR = "fi.mau.whatsapp.login.qr"
|
||||
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
|
||||
LoginStepIDCode = "fi.mau.whatsapp.login.code"
|
||||
LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
|
||||
|
||||
LoginFlowIDQR = "qr"
|
||||
LoginFlowIDPhone = "phone"
|
||||
)
|
||||
|
||||
func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
client, qrChan, cancelFunc, err := makeQRChan(qr.Main, qr.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
qr.QRChan = qrChan
|
||||
qr.cancelChan = cancelFunc
|
||||
qr.Client = client
|
||||
var resp whatsmeow.QRChannelItem
|
||||
func (wa *WhatsAppConnector) GetLoginFlows() []bridgev2.LoginFlow {
|
||||
return []bridgev2.LoginFlow{{
|
||||
Name: "QR",
|
||||
Description: "Scan a QR code to pair the bridge to your WhatsApp account",
|
||||
ID: LoginFlowIDQR,
|
||||
}, {
|
||||
Name: "Pairing code",
|
||||
Description: "Input your phone number to get a pairing code, to pair the bridge to your WhatsApp account",
|
||||
ID: LoginFlowIDPhone,
|
||||
}}
|
||||
}
|
||||
|
||||
select {
|
||||
case resp = <-qr.QRChan:
|
||||
if resp.Error != nil {
|
||||
return nil, resp.Error
|
||||
} else if resp.Event != "code" {
|
||||
return nil, fmt.Errorf("invalid QR channel event: %s", resp.Event)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
qr.Cancel()
|
||||
return nil, ctx.Err()
|
||||
var (
|
||||
ErrLoginClientOutdated = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.CLIENT_OUTDATED",
|
||||
Err: "Got client outdated error while waiting for QRs. The bridge must be updated to continue.",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
ErrLoginMultideviceNotEnabled = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.MULTIDEVICE_NOT_ENABLED",
|
||||
Err: "Please enable WhatsApp web multidevice and scan the QR code again.",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
ErrLoginTimeout = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.LOGIN_TIMEOUT",
|
||||
Err: "Entering code or scanning QR timed out. Please try again.",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
ErrUnexpectedDisconnect = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.LOGIN_UNEXPECTED_EVENT",
|
||||
Err: "Unexpected event while waiting for login",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
)
|
||||
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepQR,
|
||||
Instructions: "Scan the QR code on the WhatsApp mobile app to log in",
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeQR,
|
||||
Data: resp.Code,
|
||||
},
|
||||
func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
return &WALogin{
|
||||
User: user,
|
||||
Main: wa,
|
||||
PhoneCode: flowID == LoginFlowIDPhone,
|
||||
Log: user.Log.With().
|
||||
Str("action", "login").
|
||||
Bool("phone_code", flowID == LoginFlowIDPhone).
|
||||
Logger(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if qr.QRChan == nil {
|
||||
return nil, fmt.Errorf("login not started")
|
||||
}
|
||||
type WALogin struct {
|
||||
User *bridgev2.User
|
||||
Main *WhatsAppConnector
|
||||
Client *whatsmeow.Client
|
||||
Log zerolog.Logger
|
||||
PhoneCode bool
|
||||
|
||||
select {
|
||||
case resp := <-qr.QRChan:
|
||||
if resp.Event == "code" {
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepQR,
|
||||
Instructions: "Scan the QR code on the WhatsApp mobile app to log in",
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeQR,
|
||||
Data: resp.Code,
|
||||
},
|
||||
}, nil
|
||||
} else if resp.Event != "success" {
|
||||
qr.Cancel()
|
||||
return nil, fmt.Errorf("did not pair properly, err: %s", resp.Event)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
qr.Cancel()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
QRs []string
|
||||
StartTime time.Time
|
||||
LoginError error
|
||||
LoginSuccess *events.PairSuccess
|
||||
WaitForQRs exsync.Event
|
||||
LoginComplete exsync.Event
|
||||
PrevQRIndex atomic.Int32
|
||||
|
||||
defer qr.Cancel()
|
||||
|
||||
ul, err := makeUserLogin(ctx, qr.User, qr.Client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user login: %w", err)
|
||||
}
|
||||
|
||||
err = ul.Client.Connect(ul.Log.WithContext(context.Background()))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect after login: %w", err)
|
||||
}
|
||||
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
StepID: LoginStepComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", ul.RemoteName, ul.ID),
|
||||
CompleteParams: &bridgev2.LoginCompleteParams{
|
||||
UserLoginID: ul.ID,
|
||||
UserLogin: ul,
|
||||
},
|
||||
}, nil
|
||||
Closed atomic.Bool
|
||||
EventHandlerID uint32
|
||||
}
|
||||
|
||||
type PairingCodeLogin struct {
|
||||
User *bridgev2.User
|
||||
Main *WhatsAppConnector
|
||||
Client *whatsmeow.Client
|
||||
cancelChan context.CancelFunc
|
||||
QRChan <-chan whatsmeow.QRChannelItem
|
||||
var (
|
||||
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
|
||||
)
|
||||
|
||||
AccData *store.Device
|
||||
}
|
||||
const LoginConnectWait = 15 * time.Second
|
||||
|
||||
var _ bridgev2.LoginProcessUserInput = (*PairingCodeLogin)(nil)
|
||||
var _ bridgev2.LoginProcessDisplayAndWait = (*PairingCodeLogin)(nil)
|
||||
|
||||
func (pc *PairingCodeLogin) Cancel() {
|
||||
pc.Client.Disconnect()
|
||||
pc.cancelChan()
|
||||
go func() {
|
||||
for range pc.QRChan {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (pc *PairingCodeLogin) SubmitUserInput(_ context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
|
||||
phoneNumber := input["phone_number"]
|
||||
if phoneNumber == "" {
|
||||
return nil, fmt.Errorf("invalid or missing phone number")
|
||||
}
|
||||
pairingCode, err := pc.Client.PairPhone(phoneNumber, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
if err != nil {
|
||||
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
device := wl.Main.DeviceStore.NewDevice()
|
||||
wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log))
|
||||
wl.EventHandlerID = wl.Client.AddEventHandler(wl.handleEvent)
|
||||
if err := wl.Main.updateProxy(wl.Client, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if wl.PhoneCode {
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeUserInput,
|
||||
StepID: LoginStepIDPhoneNumber,
|
||||
UserInputParams: &bridgev2.LoginUserInputParams{
|
||||
Fields: []bridgev2.LoginInputDataField{{
|
||||
Type: bridgev2.LoginInputFieldTypePhoneNumber,
|
||||
ID: "phone_number",
|
||||
Name: "Phone number",
|
||||
Description: "Your WhatsApp phone number in international format",
|
||||
}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
err := wl.Client.Connect()
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for QR login")
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
|
||||
defer cancel()
|
||||
err = wl.WaitForQRs.Wait(ctx)
|
||||
if err != nil {
|
||||
wl.Log.Warn().Err(err).Msg("Timed out waiting for first QR")
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
}
|
||||
return makeQRStep(wl.QRs[0]), nil
|
||||
}
|
||||
|
||||
func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
|
||||
defer cancel()
|
||||
err := wl.Client.Connect()
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login")
|
||||
return nil, err
|
||||
}
|
||||
err = wl.WaitForQRs.Wait(ctx)
|
||||
if err != nil {
|
||||
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
|
||||
return nil, fmt.Errorf("failed to wait for connection: %w", err)
|
||||
}
|
||||
pairingCode, err := wl.Client.PairPhone(input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to request phone code login")
|
||||
return nil, err
|
||||
}
|
||||
wl.Log.Debug().Msg("Phone code login started")
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepPairingCode,
|
||||
Instructions: "Input the pairing code on the WhatsApp mobile app to log in",
|
||||
StepID: LoginStepIDCode,
|
||||
Instructions: "Input the pairing code in the WhatsApp mobile app to log in",
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeCode,
|
||||
Data: pairingCode,
|
||||
|
@ -233,77 +169,146 @@ func (pc *PairingCodeLogin) SubmitUserInput(_ context.Context, input map[string]
|
|||
}, nil
|
||||
}
|
||||
|
||||
var onlyNumberRegex = regexp.MustCompile(`\D+`)
|
||||
|
||||
func (pc *PairingCodeLogin) Start(_ context.Context) (*bridgev2.LoginStep, error) {
|
||||
client, qrChan, cancelFunc, err := makeQRChan(pc.Main, pc.User)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pc.QRChan = qrChan
|
||||
pc.cancelChan = cancelFunc
|
||||
pc.Client = client
|
||||
|
||||
func makeQRStep(qr string) *bridgev2.LoginStep {
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeUserInput,
|
||||
StepID: LoginStepPairingCodeInput,
|
||||
UserInputParams: &bridgev2.LoginUserInputParams{
|
||||
Fields: []bridgev2.LoginInputDataField{
|
||||
{
|
||||
Type: bridgev2.LoginInputFieldTypePhoneNumber,
|
||||
ID: "phone_number",
|
||||
Name: "phone number",
|
||||
Description: "An international phone number format without symbols",
|
||||
Validate: func(s string) (string, error) {
|
||||
return onlyNumberRegex.ReplaceAllString(s, ""), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepIDQR,
|
||||
Instructions: "Scan the QR code with the WhatsApp mobile app to log in",
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeQR,
|
||||
Data: qr,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PairingCodeLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
var qrIntervals = []time.Duration{1 * time.Minute, 20 * time.Second, 20 * time.Second, 20 * time.Second, 20 * time.Second, 20 * time.Second}
|
||||
|
||||
select {
|
||||
case resp := <-pc.QRChan:
|
||||
if resp.Event == "code" {
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeDisplayAndWait,
|
||||
StepID: LoginStepPairingCode,
|
||||
DisplayAndWaitParams: &bridgev2.LoginDisplayAndWaitParams{
|
||||
Type: bridgev2.LoginDisplayTypeNothing,
|
||||
},
|
||||
}, nil
|
||||
} else if resp.Event != "success" {
|
||||
pc.Cancel()
|
||||
return nil, fmt.Errorf("did not pair properly, err: %s", resp.Event)
|
||||
func (wl *WALogin) getQRIndex() (currentIndex int, timeUntilNext time.Duration) {
|
||||
timeSinceStart := time.Since(wl.StartTime)
|
||||
var sum time.Duration
|
||||
for i, interval := range qrIntervals {
|
||||
if timeSinceStart > sum+interval {
|
||||
sum += interval
|
||||
} else {
|
||||
return i, interval - (timeSinceStart - sum)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
pc.Cancel()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return -1, 0
|
||||
}
|
||||
|
||||
func (wl *WALogin) handleEvent(rawEvt any) {
|
||||
if wl.Closed.Load() {
|
||||
return
|
||||
}
|
||||
switch evt := rawEvt.(type) {
|
||||
case *events.QR:
|
||||
wl.Log.Debug().Int("code_count", len(evt.Codes)).Msg("Received QR codes")
|
||||
wl.QRs = evt.Codes
|
||||
wl.StartTime = time.Now()
|
||||
wl.WaitForQRs.Set()
|
||||
return
|
||||
case *events.QRScannedWithoutMultidevice:
|
||||
wl.Log.Error().Msg("QR code scanned without multidevice enabled")
|
||||
wl.LoginError = ErrLoginMultideviceNotEnabled
|
||||
case *events.ClientOutdated:
|
||||
wl.Log.Error().Msg("Got client outdated error")
|
||||
wl.LoginError = ErrLoginClientOutdated
|
||||
case *events.PairSuccess:
|
||||
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
|
||||
wl.LoginSuccess = evt
|
||||
case *events.PairError:
|
||||
wl.Log.Error().Any("event_data", evt).Msg("Got pair error event")
|
||||
wl.LoginError = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.PAIR_ERROR",
|
||||
Err: evt.Error.Error(),
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
case *events.Disconnected:
|
||||
wl.Log.Warn().Msg("Got disconnected event (login timed out)")
|
||||
wl.LoginError = ErrLoginTimeout
|
||||
case *events.Connected, *events.ConnectFailure, *events.LoggedOut, *events.TemporaryBan:
|
||||
wl.Log.Warn().Any("event_data", evt).Type("event_type", evt).Msg("Got unexpected disconnect event")
|
||||
wl.LoginError = ErrUnexpectedDisconnect
|
||||
default:
|
||||
wl.Log.Warn().Type("event_type", evt).Msg("Got unexpected event")
|
||||
return
|
||||
}
|
||||
wl.LoginComplete.Set()
|
||||
}
|
||||
|
||||
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if wl.PhoneCode {
|
||||
err := wl.LoginComplete.Wait(ctx)
|
||||
if err != nil {
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
prevIndex := int(wl.PrevQRIndex.Load())
|
||||
currentIndex, timeUntilNext := wl.getQRIndex()
|
||||
nextIndex := currentIndex + 1
|
||||
logEvt := wl.Log.Debug().
|
||||
Int("prev_index", prevIndex).
|
||||
Int("current_index", currentIndex)
|
||||
if currentIndex > prevIndex {
|
||||
logEvt.Msg("Returning new QR immediately")
|
||||
wl.PrevQRIndex.Store(int32(currentIndex))
|
||||
return makeQRStep(wl.QRs[currentIndex]), nil
|
||||
}
|
||||
logEvt.Int("next_index", nextIndex).Stringer("time_until_next", timeUntilNext).Msg("Waiting for next QR")
|
||||
select {
|
||||
case <-time.After(timeUntilNext):
|
||||
if nextIndex < 0 || nextIndex >= len(wl.QRs) {
|
||||
wl.Log.Debug().Msg("No more QRs to return")
|
||||
wl.Cancel()
|
||||
return nil, ErrLoginTimeout
|
||||
}
|
||||
wl.PrevQRIndex.Store(int32(nextIndex))
|
||||
return makeQRStep(wl.QRs[nextIndex]), nil
|
||||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
wl.Cancel()
|
||||
}
|
||||
}
|
||||
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed")
|
||||
if wl.LoginError != nil {
|
||||
return nil, wl.LoginError
|
||||
}
|
||||
|
||||
defer pc.Cancel()
|
||||
|
||||
ul, err := makeUserLogin(ctx, pc.User, pc.Client)
|
||||
newLoginID := waid.MakeUserLoginID(wl.LoginSuccess.ID)
|
||||
ul, err := wl.User.NewLogin(ctx, &database.UserLogin{
|
||||
ID: newLoginID,
|
||||
RemoteName: "+" + wl.LoginSuccess.ID.User,
|
||||
Metadata: &UserLoginMetadata{
|
||||
WADeviceID: wl.LoginSuccess.ID.Device,
|
||||
},
|
||||
}, &bridgev2.NewLoginParams{
|
||||
DeleteOnConflict: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user login: %w", err)
|
||||
}
|
||||
|
||||
err = ul.Client.Connect(ul.Log.WithContext(context.Background()))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect after login: %w", err)
|
||||
}
|
||||
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
StepID: LoginStepComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s / %s", ul.RemoteName, ul.ID),
|
||||
StepID: LoginStepIDComplete,
|
||||
Instructions: fmt.Sprintf("Successfully logged in as %s", ul.RemoteName),
|
||||
CompleteParams: &bridgev2.LoginCompleteParams{
|
||||
UserLoginID: ul.ID,
|
||||
UserLogin: ul,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wl *WALogin) Cancel() {
|
||||
wl.Closed.Store(true)
|
||||
wl.Client.RemoveEventHandler(wl.EventHandlerID)
|
||||
wl.Client.Disconnect()
|
||||
}
|
||||
|
|
6
pkg/connector/phoneping.go
Normal file
6
pkg/connector/phoneping.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package connector
|
||||
|
||||
func (wa *WhatsAppClient) PhoneRecentlySeen(doPing bool) bool {
|
||||
// TODO implement
|
||||
return true
|
||||
}
|
|
@ -2,10 +2,12 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
|
@ -13,12 +15,11 @@ import (
|
|||
|
||||
var (
|
||||
_ bridgev2.IdentifierResolvingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.ContactListingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
var (
|
||||
LooksLikeEmailErr = fmt.Errorf("WhatsApp only supports phone numbers as user identifiers. Number looks like email")
|
||||
NotLoggedInErr = fmt.Errorf("User is not logged in to WhatsApp. No session")
|
||||
NoResponseErr = fmt.Errorf("Didn't get a response to checking if the number is on WhatsApp. Error checking number")
|
||||
ErrInputLooksLikeEmail = bridgev2.WrapRespErr(errors.New("WhatsApp only supports phone numbers as user identifiers. Number looks like email"), mautrix.MInvalidParam)
|
||||
)
|
||||
|
||||
func looksEmaily(str string) bool {
|
||||
|
@ -31,34 +32,59 @@ func looksEmaily(str string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ValidateIdentifer(number string) (types.JID, error) {
|
||||
func (wa *WhatsAppClient) validateIdentifer(number string) (types.JID, error) {
|
||||
if strings.HasSuffix(number, "@"+types.DefaultUserServer) {
|
||||
jid, _ := types.ParseJID(number)
|
||||
number = "+" + jid.User
|
||||
}
|
||||
if looksEmaily(number) {
|
||||
return types.EmptyJID, LooksLikeEmailErr
|
||||
} else if !wa.Client.IsLoggedIn() {
|
||||
return types.EmptyJID, NotLoggedInErr
|
||||
return types.EmptyJID, ErrInputLooksLikeEmail
|
||||
} else if wa.Client == nil || !wa.Client.IsLoggedIn() {
|
||||
return types.EmptyJID, bridgev2.ErrNotLoggedIn
|
||||
} else if resp, err := wa.Client.IsOnWhatsApp([]string{number}); err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("Failed to check if number is on WhatsApp: %v", err)
|
||||
return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err)
|
||||
} else if len(resp) == 0 {
|
||||
return types.EmptyJID, NoResponseErr
|
||||
return types.EmptyJID, fmt.Errorf("the server did not respond to the query")
|
||||
} else if !resp[0].IsIn {
|
||||
return types.EmptyJID, fmt.Errorf("The server said +%s is not on WhatsApp", resp[0].JID.User)
|
||||
return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound)
|
||||
} else {
|
||||
return resp[0].JID, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ResolveIdentifier(_ context.Context, identifier string, _ bool) (*bridgev2.ResolveIdentifierResponse, error) {
|
||||
jid, err := wa.ValidateIdentifer(identifier)
|
||||
|
||||
func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier string, startChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
|
||||
jid, err := wa.validateIdentifer(identifier)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse identifier: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost: %w", err)
|
||||
}
|
||||
|
||||
return &bridgev2.ResolveIdentifierResponse{
|
||||
UserID: waid.MakeUserID(&jid),
|
||||
Ghost: ghost,
|
||||
UserID: waid.MakeUserID(jid),
|
||||
Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
contacts, err := wa.Client.Store.Contacts.GetAllContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*bridgev2.ResolveIdentifierResponse, len(contacts))
|
||||
i := 0
|
||||
for jid, contactInfo := range contacts {
|
||||
ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
|
||||
resp[i] = &bridgev2.ResolveIdentifierResponse{
|
||||
Ghost: ghost,
|
||||
UserID: waid.MakeUserID(jid),
|
||||
UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false),
|
||||
Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)},
|
||||
}
|
||||
i++
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
|
60
pkg/connector/userinfo.go
Normal file
60
pkg/connector/userinfo.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
||||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
|
||||
jid := waid.ParseUserID(ghost.ID)
|
||||
contact, err := wa.Client.Store.Contacts.GetContact(jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fetchAvatar := !ghost.Metadata.(*GhostMetadata).AvatarFetchAttempted
|
||||
return wa.contactToUserInfo(ctx, jid, contact, fetchAvatar), nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo {
|
||||
ui := &bridgev2.UserInfo{
|
||||
Name: ptr.Ptr(wa.Main.Config.FormatDisplayname(jid, contact)),
|
||||
IsBot: ptr.Ptr(jid.IsBot()),
|
||||
Identifiers: []string{fmt.Sprintf("tel:%s", jid.User)},
|
||||
}
|
||||
if getAvatar {
|
||||
ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, markAvatarFetchAttempted)
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
|
||||
Preview: false,
|
||||
IsCommunity: false,
|
||||
})
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get avatar info")
|
||||
} else if avatar != nil {
|
||||
ui.Avatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return ui
|
||||
}
|
||||
|
||||
func markAvatarFetchAttempted(_ context.Context, ghost *bridgev2.Ghost) bool {
|
||||
meta := ghost.Metadata.(*GhostMetadata)
|
||||
if !meta.AvatarFetchAttempted {
|
||||
meta.AvatarFetchAttempted = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -17,6 +17,7 @@ import (
|
|||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/image/webp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
@ -195,7 +196,7 @@ func (mc *MessageConverter) constructTextMessage(ctx context.Context, content *e
|
|||
mentions := make([]string, 0)
|
||||
|
||||
parseCtx := format.NewContext(ctx)
|
||||
parseCtx.ReturnData["mentions"] = mentions
|
||||
parseCtx.ReturnData["mentions"] = &mentions
|
||||
parseCtx.ReturnData["portal"] = portal
|
||||
var text string
|
||||
if content.Format == event.FormatHTML {
|
||||
|
@ -223,13 +224,10 @@ func (mc *MessageConverter) convertPill(displayname, mxid, eventID string, ctx f
|
|||
var jid types.JID
|
||||
ghost, err := mc.Bridge.GetGhostByMXID(ctx.Ctx, id.UserID(mxid))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx.Ctx).Err(err).Str("mxid", mxid).Msg("Failed to get user for mention")
|
||||
zerolog.Ctx(ctx.Ctx).Err(err).Str("mxid", mxid).Msg("Failed to get ghost for mention")
|
||||
return displayname
|
||||
} else if ghost != nil {
|
||||
jid, err = types.ParseJID(string(ghost.ID))
|
||||
if jid.String() == "" || err != nil {
|
||||
return displayname
|
||||
}
|
||||
jid = waid.ParseUserID(ghost.ID)
|
||||
} else if user, err := mc.Bridge.GetExistingUserByMXID(ctx.Ctx, id.UserID(mxid)); err != nil {
|
||||
zerolog.Ctx(ctx.Ctx).Err(err).Str("mxid", mxid).Msg("Failed to get user for mention")
|
||||
return displayname
|
||||
|
@ -239,12 +237,12 @@ func (mc *MessageConverter) convertPill(displayname, mxid, eventID string, ctx f
|
|||
if login == nil {
|
||||
return displayname
|
||||
}
|
||||
jid = waid.ParseWAUserLoginID(login.ID)
|
||||
jid = waid.ParseUserLoginID(login.ID, 0)
|
||||
} else {
|
||||
return displayname
|
||||
}
|
||||
mentions := ctx.ReturnData["mentions"].([]string)
|
||||
mentions = append(mentions, jid.String())
|
||||
mentions := ctx.ReturnData["mentions"].(*[]string)
|
||||
*mentions = append(*mentions, jid.String())
|
||||
return fmt.Sprintf("@%s", jid.User)
|
||||
}
|
||||
|
||||
|
@ -419,10 +417,17 @@ func getAudioInfo(content *event.MessageEventContent) (output []byte, Duration u
|
|||
if waveform != nil {
|
||||
return nil, uint32(audioInfo.Duration / 1000)
|
||||
}
|
||||
output = make([]byte, len(waveform))
|
||||
|
||||
for i, part := range waveform {
|
||||
output[i] = byte(part / 4)
|
||||
maxVal := slices.Max(waveform)
|
||||
output = make([]byte, len(waveform))
|
||||
if maxVal < 256 {
|
||||
for i, part := range waveform {
|
||||
output[i] = byte(part)
|
||||
}
|
||||
} else {
|
||||
for i, part := range waveform {
|
||||
output[i] = min(byte(part/4), 255)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
|
@ -13,11 +14,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
_ "golang.org/x/image/webp"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
@ -32,110 +35,55 @@ type MediaMessage interface {
|
|||
GetMimetype() string
|
||||
}
|
||||
|
||||
func getAudioMessageInfo(audioMessage *waE2E.AudioMessage) (waveform []int, duration int) {
|
||||
if audioMessage.Waveform != nil {
|
||||
waveform = make([]int, len(audioMessage.Waveform))
|
||||
maxWave := 0
|
||||
for i, part := range audioMessage.Waveform {
|
||||
waveform[i] = int(part)
|
||||
if waveform[i] > maxWave {
|
||||
maxWave = waveform[i]
|
||||
}
|
||||
}
|
||||
multiplier := 0
|
||||
if maxWave > 0 {
|
||||
multiplier = 1024 / maxWave
|
||||
}
|
||||
if multiplier > 32 {
|
||||
multiplier = 32
|
||||
}
|
||||
for i := range waveform {
|
||||
waveform[i] *= multiplier
|
||||
}
|
||||
}
|
||||
duration = int(audioMessage.GetSeconds() * 1000)
|
||||
return
|
||||
type MediaInfo struct {
|
||||
event.FileInfo
|
||||
Waveform []int
|
||||
IsGif bool
|
||||
Caption string
|
||||
MsgType event.MessageType
|
||||
ContextInfo *waE2E.ContextInfo
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user networkid.UserID) (id.UserID, string, error) {
|
||||
ghost, err := mc.Bridge.GetGhostByID(ctx, user)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get ghost by ID: %w", err)
|
||||
}
|
||||
login := mc.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(user))
|
||||
if login != nil {
|
||||
return login.UserMXID, ghost.Name, nil
|
||||
}
|
||||
return ghost.Intent.GetMXID(), ghost.Name, nil
|
||||
}
|
||||
|
||||
type ExtraMediaInfo struct {
|
||||
Waveform []int
|
||||
Duration int
|
||||
|
||||
IsGif bool
|
||||
|
||||
Caption string
|
||||
|
||||
MsgType event.MessageType
|
||||
}
|
||||
|
||||
func getMediaMessageFileInfo(msg *waE2E.Message) (message MediaMessage, info event.FileInfo, contextInfo *waE2E.ContextInfo, extraInfo ExtraMediaInfo) {
|
||||
message = nil
|
||||
info = event.FileInfo{}
|
||||
extraInfo = ExtraMediaInfo{}
|
||||
|
||||
contextInfo = &waE2E.ContextInfo{}
|
||||
func getMediaMessageFileInfo(msg *waE2E.Message) (message MediaMessage, info MediaInfo) {
|
||||
if msg.GetAudioMessage() != nil {
|
||||
info.MsgType = event.MsgAudio
|
||||
message = msg.AudioMessage
|
||||
|
||||
info.Duration = int(msg.AudioMessage.GetSeconds() * 1000)
|
||||
|
||||
waveform, duration := getAudioMessageInfo(msg.AudioMessage)
|
||||
extraInfo.Waveform = waveform
|
||||
extraInfo.Duration = duration
|
||||
|
||||
extraInfo.MsgType = event.MsgAudio
|
||||
info.Waveform = exslices.CastFunc(msg.AudioMessage.Waveform, func(from byte) int { return int(from) })
|
||||
} else if msg.GetDocumentMessage() != nil {
|
||||
info.MsgType = event.MsgFile
|
||||
message = msg.DocumentMessage
|
||||
|
||||
extraInfo.Caption = msg.DocumentMessage.GetCaption()
|
||||
extraInfo.MsgType = event.MsgFile
|
||||
info.Caption = msg.DocumentMessage.GetCaption()
|
||||
} else if msg.GetImageMessage() != nil {
|
||||
info.MsgType = event.MsgImage
|
||||
message = msg.ImageMessage
|
||||
|
||||
info.Width = int(msg.ImageMessage.GetWidth())
|
||||
info.Height = int(msg.ImageMessage.GetHeight())
|
||||
|
||||
extraInfo.Caption = msg.DocumentMessage.GetCaption()
|
||||
extraInfo.MsgType = event.MsgImage
|
||||
info.Caption = msg.ImageMessage.GetCaption()
|
||||
} else if msg.GetStickerMessage() != nil {
|
||||
message = msg.StickerMessage
|
||||
|
||||
info.Width = int(msg.StickerMessage.GetWidth())
|
||||
info.Height = int(msg.StickerMessage.GetHeight())
|
||||
extraInfo.MsgType = event.MessageType(event.EventSticker.Type)
|
||||
} else if msg.GetVideoMessage() != nil {
|
||||
info.MsgType = event.MsgVideo
|
||||
message = msg.VideoMessage
|
||||
|
||||
info.Duration = int(msg.VideoMessage.GetSeconds() * 1000)
|
||||
info.Width = int(msg.VideoMessage.GetWidth())
|
||||
info.Height = int(msg.VideoMessage.GetHeight())
|
||||
|
||||
extraInfo.Caption = msg.DocumentMessage.GetCaption()
|
||||
if msg.VideoMessage.GetGifPlayback() {
|
||||
extraInfo.IsGif = true
|
||||
}
|
||||
extraInfo.MsgType = event.MsgVideo
|
||||
info.Caption = msg.VideoMessage.GetCaption()
|
||||
info.IsGif = msg.VideoMessage.GetGifPlayback()
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if message != nil {
|
||||
info.Size = int(message.GetFileLength())
|
||||
|
||||
info.MimeType = message.GetMimetype()
|
||||
|
||||
contextInfo = message.GetContextInfo()
|
||||
}
|
||||
info.Size = int(message.GetFileLength())
|
||||
info.MimeType = message.GetMimetype()
|
||||
info.ContextInfo = message.GetContextInfo()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -168,54 +116,45 @@ func convertContactMessage(ctx context.Context, intent bridgev2.MatrixAPI, porta
|
|||
return cmp, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) parseMessageContextInfo(ctx context.Context, cm *bridgev2.ConvertedMessage, contextInfo *waE2E.ContextInfo, msgInfo types.MessageInfo) *bridgev2.ConvertedMessage {
|
||||
for _, part := range cm.Parts {
|
||||
content := part.Content
|
||||
content.Mentions = &event.Mentions{
|
||||
UserIDs: make([]id.UserID, 0),
|
||||
}
|
||||
|
||||
if mentions := contextInfo.GetMentionedJID(); len(mentions) > 0 {
|
||||
content.Format = event.FormatHTML
|
||||
content.FormattedBody = event.TextToHTML(content.Body)
|
||||
for _, jid := range mentions {
|
||||
parsed, err := types.ParseJID(jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to parse mentioned JID")
|
||||
continue
|
||||
}
|
||||
mxid, displayname, err := mc.getBasicUserInfo(ctx, waid.MakeUserID(&parsed))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to get user info")
|
||||
continue
|
||||
}
|
||||
content.Mentions.UserIDs = append(content.Mentions.UserIDs, mxid)
|
||||
mentionText := "@" + parsed.User
|
||||
content.Body = strings.ReplaceAll(content.Body, mentionText, displayname)
|
||||
content.FormattedBody = strings.ReplaceAll(content.FormattedBody, mentionText, fmt.Sprintf(`<a href="%s">%s</a>`, mxid.URI().MatrixToURL(), event.TextToHTML(displayname)))
|
||||
}
|
||||
}
|
||||
|
||||
if qm := contextInfo.GetQuotedMessage(); qm != nil {
|
||||
pcp, _ := types.ParseJID(contextInfo.GetParticipant())
|
||||
chat, _ := types.ParseJID(contextInfo.GetRemoteJID())
|
||||
if chat.String() == "" {
|
||||
chat = msgInfo.Chat
|
||||
}
|
||||
|
||||
cm.ReplyTo = &networkid.MessageOptionalPartID{
|
||||
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()),
|
||||
}
|
||||
}
|
||||
func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user networkid.UserID) (id.UserID, string, error) {
|
||||
ghost, err := mc.Bridge.GetGhostByID(ctx, user)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get ghost by ID: %w", err)
|
||||
}
|
||||
login := mc.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(user))
|
||||
if login != nil {
|
||||
return login.UserMXID, ghost.Name, nil
|
||||
}
|
||||
return ghost.Intent.GetMXID(), ghost.Name, nil
|
||||
}
|
||||
|
||||
return cm
|
||||
func (mc *MessageConverter) addMentions(ctx context.Context, mentionedJID []string, into *event.MessageEventContent) {
|
||||
if len(mentionedJID) == 0 {
|
||||
return
|
||||
}
|
||||
into.EnsureHasHTML()
|
||||
for _, jid := range mentionedJID {
|
||||
parsed, err := types.ParseJID(jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to parse mentioned JID")
|
||||
continue
|
||||
}
|
||||
mxid, displayname, err := mc.getBasicUserInfo(ctx, waid.MakeUserID(parsed))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to get user info")
|
||||
continue
|
||||
}
|
||||
into.Mentions.UserIDs = append(into.Mentions.UserIDs, mxid)
|
||||
mentionText := "@" + parsed.User
|
||||
into.Body = strings.ReplaceAll(into.Body, mentionText, displayname)
|
||||
into.FormattedBody = strings.ReplaceAll(into.FormattedBody, mentionText, fmt.Sprintf(`<a href="%s">%s</a>`, mxid.URI().MatrixToURL(), html.EscapeString(displayname)))
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
||||
ctx context.Context,
|
||||
message MediaMessage,
|
||||
fileInfo event.FileInfo,
|
||||
fileInfo *event.FileInfo,
|
||||
client *whatsmeow.Client,
|
||||
intent bridgev2.MatrixAPI,
|
||||
portal *bridgev2.Portal,
|
||||
|
@ -230,19 +169,17 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
||||
}
|
||||
|
||||
cmp := &bridgev2.ConvertedMessagePart{
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
Body: fileName,
|
||||
FileName: fileName,
|
||||
Info: fileInfo,
|
||||
URL: mxc,
|
||||
Info: &fileInfo,
|
||||
File: file,
|
||||
},
|
||||
Extra: make(map[string]any),
|
||||
}
|
||||
|
||||
return cmp, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertLocationMessage(ctx context.Context, intent bridgev2.MatrixAPI, portal *bridgev2.Portal, msg *waE2E.LocationMessage) *bridgev2.ConvertedMessagePart {
|
||||
|
@ -273,8 +210,8 @@ func convertLocationMessage(ctx context.Context, intent bridgev2.MatrixAPI, port
|
|||
|
||||
if len(msg.GetJPEGThumbnail()) > 0 {
|
||||
thumbnailMime := http.DetectContentType(msg.GetJPEGThumbnail())
|
||||
uploadedThumbnail, _, _ := intent.UploadMedia(ctx, portal.MXID, msg.GetJPEGThumbnail(), "thumb.jpeg", thumbnailMime)
|
||||
if uploadedThumbnail != "" {
|
||||
thumbnailURL, thumbnailFile, err := intent.UploadMedia(ctx, portal.MXID, msg.GetJPEGThumbnail(), "thumb.jpeg", thumbnailMime)
|
||||
if err == nil {
|
||||
cfg, _, _ := image.DecodeConfig(bytes.NewReader(msg.GetJPEGThumbnail()))
|
||||
content.Info = &event.FileInfo{
|
||||
ThumbnailInfo: &event.FileInfo{
|
||||
|
@ -283,7 +220,8 @@ func convertLocationMessage(ctx context.Context, intent bridgev2.MatrixAPI, port
|
|||
Height: cfg.Height,
|
||||
MimeType: thumbnailMime,
|
||||
},
|
||||
ThumbnailURL: uploadedThumbnail,
|
||||
ThumbnailURL: thumbnailURL,
|
||||
ThumbnailFile: thumbnailFile,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -294,79 +232,98 @@ func convertLocationMessage(ctx context.Context, intent bridgev2.MatrixAPI, port
|
|||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Portal, client *whatsmeow.Client, intent bridgev2.MatrixAPI, message *waE2E.Message, msgInfo *types.MessageInfo) *bridgev2.ConvertedMessage {
|
||||
cm := &bridgev2.ConvertedMessage{
|
||||
Parts: make([]*bridgev2.ConvertedMessagePart, 0),
|
||||
func makeMediaFailure(mediaType string) *bridgev2.ConvertedMessagePart {
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: fmt.Sprintf("Failed to bridge %s, please view it on the WhatsApp app", mediaType),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
media, fileInfo, contextInfo, extraInfo := getMediaMessageFileInfo(message)
|
||||
func (mc *MessageConverter) ToMatrix(ctx context.Context, portal *bridgev2.Portal, client *whatsmeow.Client, intent bridgev2.MatrixAPI, message *waE2E.Message) *bridgev2.ConvertedMessage {
|
||||
var part *bridgev2.ConvertedMessagePart
|
||||
var contextInfo *waE2E.ContextInfo
|
||||
var err error
|
||||
media, info := getMediaMessageFileInfo(message)
|
||||
if media != nil {
|
||||
cmp, err := mc.reuploadWhatsAppAttachment(ctx, media, fileInfo, client, intent, portal)
|
||||
contextInfo = info.ContextInfo
|
||||
part, err = mc.reuploadWhatsAppAttachment(ctx, media, &info.FileInfo, client, intent, portal)
|
||||
if err != nil {
|
||||
// TODO: THROW SOME ERROR HERE!?
|
||||
return cm
|
||||
}
|
||||
part = makeMediaFailure("attachment")
|
||||
} else {
|
||||
part.Content.MsgType = info.MsgType
|
||||
if message.StickerMessage != nil {
|
||||
part.Type = event.EventSticker
|
||||
}
|
||||
|
||||
if extraInfo.MsgType != "" {
|
||||
cmp.Content.MsgType = extraInfo.MsgType
|
||||
}
|
||||
|
||||
if extraInfo.Waveform != nil {
|
||||
cmp.Content.MSC3245Voice = &event.MSC3245Voice{}
|
||||
cmp.Content.MSC1767Audio = &event.MSC1767Audio{
|
||||
Duration: extraInfo.Duration,
|
||||
Waveform: extraInfo.Waveform,
|
||||
if info.Waveform != nil {
|
||||
part.Content.MSC3245Voice = &event.MSC3245Voice{}
|
||||
part.Content.MSC1767Audio = &event.MSC1767Audio{
|
||||
Duration: info.Duration,
|
||||
Waveform: info.Waveform,
|
||||
}
|
||||
}
|
||||
if info.Caption != "" {
|
||||
part.Content.Body = info.Caption
|
||||
}
|
||||
if info.IsGif {
|
||||
part.Extra["info"] = map[string]any{
|
||||
"fi.mau.gif": true,
|
||||
"fi.mau.loop": true,
|
||||
"fi.mau.autoplay": true,
|
||||
"fi.mau.hide_controls": true,
|
||||
"fi.mau.no_audio": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
if extraInfo.Caption != "" {
|
||||
cmp.Content.Body = extraInfo.Caption
|
||||
}
|
||||
if extraInfo.IsGif {
|
||||
cmp.Extra["info"] = map[string]any{
|
||||
"fi.mau.gif": true,
|
||||
"fi.mau.loop": true,
|
||||
"fi.mau.autoplay": true,
|
||||
"fi.mau.hide_controls": true,
|
||||
"fi.mau.no_audio": true,
|
||||
}
|
||||
}
|
||||
cm.Parts = append(cm.Parts, cmp)
|
||||
} else if location := message.GetLocationMessage(); location != nil {
|
||||
cmp := convertLocationMessage(ctx, intent, portal, location)
|
||||
|
||||
part = convertLocationMessage(ctx, intent, portal, location)
|
||||
contextInfo = location.GetContextInfo()
|
||||
cm.Parts = append(cm.Parts, cmp)
|
||||
} else if contacts := message.GetContactMessage(); contacts != nil {
|
||||
cmp, err := convertContactMessage(ctx, intent, portal, contacts)
|
||||
part, err = convertContactMessage(ctx, intent, portal, contacts)
|
||||
if err != nil {
|
||||
// TODO: THROW SOME ERROR HERE!?
|
||||
return cm
|
||||
part = makeMediaFailure("contact message")
|
||||
}
|
||||
|
||||
contextInfo = contacts.GetContextInfo()
|
||||
cm.Parts = append(cm.Parts, cmp)
|
||||
} else {
|
||||
cmp := &bridgev2.ConvertedMessagePart{
|
||||
part = &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
},
|
||||
}
|
||||
if extendedText := message.GetExtendedTextMessage(); extendedText != nil {
|
||||
cmp.Content.Body = extendedText.GetText()
|
||||
part.Content.Body = extendedText.GetText()
|
||||
contextInfo = extendedText.GetContextInfo()
|
||||
} else if conversation := message.GetConversation(); conversation != "" {
|
||||
cmp.Content.Body = conversation
|
||||
part.Content.Body = conversation
|
||||
contextInfo = nil
|
||||
} else {
|
||||
cmp.Content.MsgType = event.MsgNotice
|
||||
cmp.Content.Body = "Unknown message type, please view it on the WhatsApp app"
|
||||
part.Content.MsgType = event.MsgNotice
|
||||
part.Content.Body = "Unknown message type, please view it on the WhatsApp app"
|
||||
}
|
||||
cm.Parts = append(cm.Parts, cmp)
|
||||
}
|
||||
// TODO lots of message types missing
|
||||
|
||||
if contextInfo != nil && msgInfo != nil {
|
||||
cm = mc.parseMessageContextInfo(ctx, cm, contextInfo, *msgInfo)
|
||||
part.Content.Mentions = &event.Mentions{}
|
||||
mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content)
|
||||
|
||||
cm := &bridgev2.ConvertedMessage{
|
||||
Parts: []*bridgev2.ConvertedMessagePart{part},
|
||||
Disappear: database.DisappearingSetting{},
|
||||
}
|
||||
if contextInfo.GetStanzaID() != "" {
|
||||
pcp, _ := types.ParseJID(contextInfo.GetParticipant())
|
||||
chat, _ := types.ParseJID(contextInfo.GetRemoteJID())
|
||||
if chat.IsEmpty() {
|
||||
chat, _ = waid.ParsePortalID(portal.ID)
|
||||
}
|
||||
cm.ReplyTo = &networkid.MessageOptionalPartID{
|
||||
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()),
|
||||
}
|
||||
}
|
||||
|
||||
return cm
|
||||
|
|
|
@ -24,7 +24,6 @@ import (
|
|||
type MessageConverter struct {
|
||||
Bridge *bridgev2.Bridge
|
||||
MaxFileSize int64
|
||||
AsyncFiles bool
|
||||
HTMLParser *format.HTMLParser
|
||||
}
|
||||
|
||||
|
|
|
@ -8,25 +8,19 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
)
|
||||
|
||||
/*func ParseWAPortalID(portal networkid.PortalID, server string) types.JID {
|
||||
return types.JID{
|
||||
User: string(portal),
|
||||
Server: server,
|
||||
}
|
||||
}*/
|
||||
|
||||
func MakeWAPortalID(jid types.JID) networkid.PortalID {
|
||||
func MakePortalID(jid types.JID) networkid.PortalID {
|
||||
return networkid.PortalID(jid.ToNonAD().String())
|
||||
}
|
||||
|
||||
func ParseWAUserLoginID(user networkid.UserLoginID) types.JID {
|
||||
return types.JID{
|
||||
Server: types.DefaultUserServer,
|
||||
User: string(user),
|
||||
func ParsePortalID(portal networkid.PortalID) (types.JID, error) {
|
||||
parsed, err := types.ParseJID(string(portal))
|
||||
if err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("invalid portal ID: %w", err)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func MakeUserID(user *types.JID) networkid.UserID {
|
||||
func MakeUserID(user types.JID) networkid.UserID {
|
||||
return networkid.UserID(user.User)
|
||||
}
|
||||
|
||||
|
@ -34,8 +28,16 @@ func ParseUserID(user networkid.UserID) types.JID {
|
|||
return types.NewJID(string(user), types.DefaultUserServer)
|
||||
}
|
||||
|
||||
func MakeUserLoginID(user *types.JID) networkid.UserLoginID {
|
||||
return networkid.UserLoginID(MakeUserID(user))
|
||||
func MakeUserLoginID(user types.JID) networkid.UserLoginID {
|
||||
return networkid.UserLoginID(user.User)
|
||||
}
|
||||
|
||||
func ParseUserLoginID(user networkid.UserLoginID, deviceID uint16) types.JID {
|
||||
return types.JID{
|
||||
Server: types.DefaultUserServer,
|
||||
User: string(user),
|
||||
Device: deviceID,
|
||||
}
|
||||
}
|
||||
|
||||
func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.MessageID {
|
||||
|
|
Loading…
Add table
Reference in a new issue