Merge pull request 'Merge upstream' (#1) from MirrorHub/mautrix-whatsapp:master into master

Reviewed-on: #1
This commit is contained in:
Timo Ley 2024-02-09 19:53:02 +00:00
commit 805d84776a
42 changed files with 1386 additions and 1044 deletions

View file

@ -5,13 +5,19 @@ on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go-version: ["1.20", "1.21"]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v3
uses: actions/setup-go@v5
with:
go-version: "1.20"
go-version: ${{ matrix.go-version }}
cache: true
- name: Install libolm
run: sudo apt-get install libolm-dev libolm3

1
.gitignore vendored
View file

@ -10,3 +10,4 @@
*.log
/mautrix-whatsapp
/start

1
.idea/icon.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.216 175.552"><defs><linearGradient id="b" x1="85.915" x2="86.535" y1="32.567" y2="137.092" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#57d163"/><stop offset="1" stop-color="#23b33a"/></linearGradient><filter id="a" width="1.115" height="1.114" x="-.057" y="-.057" color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="3.531"/></filter></defs><path fill="#b3b3b3" d="m54.532 138.45 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.523h.023c33.707 0 61.139-27.426 61.153-61.135.006-16.335-6.349-31.696-17.895-43.251A60.75 60.75 0 0 0 87.94 25.983c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.558zm-40.811 23.544L24.16 123.88c-6.438-11.154-9.825-23.808-9.821-36.772.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954zm0 0" filter="url(#a)"/><path fill="#fff" d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z"/><path fill="url(#linearGradient1780)" d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z"/><path fill="url(#b)" d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.313-6.179 22.558 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.517 31.126 8.523h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.928z"/><path fill="#fff" fill-rule="evenodd" d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647"/></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
@ -13,3 +13,8 @@ repos:
hooks:
- id: go-imports-repo
- id: go-vet-repo-mod
- repo: https://github.com/beeper/pre-commit-go
rev: v0.2.2
hooks:
- id: zerolog-ban-msgf

View file

@ -1,6 +1,72 @@
# v0.8.6 (unreleased)
# v0.10.5 (2023-12-16)
* Added support for sending media to channels.
* Fixed voting in polls (seems to have broken due to a server-side change).
* Improved memory usage for bridges with lots of portals.
# v0.10.4 (2023-11-16)
* Added support for channels in `join` and `open` commands.
* Added initial bridging of channel admin to room admin status.
* Fixed panic when trying to send message in a portal which has a relaybot set
if the relaybot user gets logged out of WhatsApp.
# v0.10.3 (2023-10-16)
* Added basic support for channels.
* Added default mime type for outgoing attachments when the origin Matrix
client forgets to specify the mime type.
* Fixed legacy backfill creating portals for chats without messages.
* Updated libwebp version used for encoding.
# v0.10.2 (security update)
* Stopped using libwebp for decoding webps.
# v0.10.1 (2023-09-16)
* Added support for double puppeting with arbitrary `as_token`s.
See [docs](https://docs.mau.fi/bridges/general/double-puppeting.html#appservice-method-new) for more info.
* Added retrying for media downloads when WhatsApp servers break and start
returning 429s and 503s.
* Fixed logging in with 8-letter code.
* Fixed syncing community announcement groups.
* Changed "Incoming call" message to explicitly say you have to open WhatsApp
on your phone to answer.
# v0.10.0 (2023-08-16)
* Bumped minimum Go version to 1.20.
* Added automatic re-requesting of undecryptable WhatsApp messages from primary
device.
* Added support for round video messages.
* Added support for logging in by entering a 8-letter code on the phone instead
of scanning a QR code.
* Note: due to a server-side change, code login may only work when `os_name`
and `browser_name` in the config are set in a specific way. This is fixed
in v0.10.1.
# v0.9.0 (2023-07-16)
* Removed MSC2716 support.
* Added legacy backfill support.
* Updated Docker image to Alpine 3.18.
* Changed all ogg audio messages from WhatsApp to be bridged as voice messages
to Matrix, as WhatsApp removes the voice message flag when forwarding for
some reason.
* Added Prometheus metric for WhatsApp connection failures
(thanks to [@Half-Shot] in [#620]).
[#620]: https://github.com/mautrix/whatsapp/pull/620
# v0.8.6 (2023-06-16)
* Implemented intentional mentions for outgoing messages.
* Added support for appservice websockets.
* Added additional index on message table to make bridging outgoing read
receipts and messages faster in chats with lots of messages.
* Fixed handling WhatsApp poll messages that only allow one choice.
* Fixed bridging new groups immediately when they're created.
# v0.8.5 (2023-05-16)

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine3.17 AS builder
FROM golang:1-alpine3.19 AS builder
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
@ -6,7 +6,7 @@ COPY . /build
WORKDIR /build
RUN go build -o /usr/bin/mautrix-whatsapp
FROM alpine:3.17
FROM alpine:3.19
ENV UID=1337 \
GID=1337

View file

@ -1,4 +1,4 @@
FROM alpine:3.17
FROM alpine:3.19
ENV UID=1337 \
GID=1337

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine3.17
FROM golang:1-alpine3.18
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev bash jq yq curl

View file

@ -26,27 +26,26 @@ import (
"maunium.net/go/mautrix/id"
)
const SegmentURL = "https://api.segment.io/v1/track"
type SegmentClient struct {
type AnalyticsClient struct {
url string
key string
userID string
log log.Logger
client http.Client
}
var Segment SegmentClient
var Analytics AnalyticsClient
func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
func (sc *AnalyticsClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
var buf bytes.Buffer
var segmentUserID string
if Segment.userID != "" {
segmentUserID = Segment.userID
var analyticsUserID string
if Analytics.userID != "" {
analyticsUserID = Analytics.userID
} else {
segmentUserID = userID.String()
analyticsUserID = userID.String()
}
err := json.NewEncoder(&buf).Encode(map[string]interface{}{
"userId": segmentUserID,
"userId": analyticsUserID,
"event": event,
"properties": properties,
})
@ -54,7 +53,7 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return err
}
req, err := http.NewRequest("POST", SegmentURL, &buf)
req, err := http.NewRequest(http.MethodPost, sc.url, &buf)
if err != nil {
return err
}
@ -70,11 +69,11 @@ func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties ma
return nil
}
func (sc *SegmentClient) IsEnabled() bool {
func (sc *AnalyticsClient) IsEnabled() bool {
return len(sc.key) > 0
}
func (sc *SegmentClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
func (sc *AnalyticsClient) Track(userID id.UserID, event string, properties ...map[string]interface{}) {
if !sc.IsEnabled() {
return
} else if len(properties) > 1 {

View file

@ -65,7 +65,7 @@ func (user *User) HandleBackfillRequestsLoop(backfillTypes []database.BackfillTy
req := user.BackfillQueue.GetNextBackfill(user.MXID, backfillTypes, waitForBackfillTypes, reCheckChannel)
user.log.Infofln("Handling backfill request %s", req)
conv := user.bridge.DB.HistorySync.GetConversation(user.MXID, req.Portal)
conv := user.bridge.DB.HistorySync.GetConversation(user.MXID, *req.Portal)
if conv == nil {
user.log.Debugfln("Could not find history sync conversation data for %s", req.Portal.String())
req.MarkDone()

View file

@ -23,6 +23,7 @@ import (
"fmt"
"html"
"math"
"regexp"
"sort"
"strconv"
"strings"
@ -41,8 +42,6 @@ import (
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database"
)
type WrappedCommandEvent struct {
@ -71,7 +70,6 @@ func (br *WABridge) RegisterCommands() {
cmdPing,
cmdDeletePortal,
cmdDeleteAllPortals,
cmdBackfill,
cmdList,
cmdSearch,
cmdOpen,
@ -116,7 +114,7 @@ func fnSetRelay(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge")
} else {
ce.Portal.RelayUserID = ce.User.MXID
ce.Portal.Update(nil)
@ -138,7 +136,7 @@ func fnUnsetRelay(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.Relay.Enabled {
ce.Reply("Relay mode is not enabled on this instance of the bridge")
} else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin {
ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge")
ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge")
} else {
ce.Portal.RelayUserID = ""
ce.Portal.Update(nil)
@ -240,18 +238,32 @@ func fnJoin(ce *WrappedCommandEvent) {
if len(ce.Args) == 0 {
ce.Reply("**Usage:** `join <invite link>`")
return
} else if !strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
ce.Reply("That doesn't look like a WhatsApp invite link")
return
}
jid, err := ce.User.Client.JoinGroupWithLink(ce.Args[0])
if err != nil {
ce.Reply("Failed to join group: %v", err)
return
if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
jid, err := ce.User.Client.JoinGroupWithLink(ce.Args[0])
if err != nil {
ce.Reply("Failed to join group: %v", err)
return
}
ce.Log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
ce.Reply("Successfully joined group `%s`, the portal should be created momentarily", jid)
} else if strings.HasPrefix(ce.Args[0], whatsmeow.NewsletterLinkPrefix) {
info, err := ce.User.Client.GetNewsletterInfoWithInvite(ce.Args[0])
if err != nil {
ce.Reply("Failed to get channel info: %v", err)
return
}
err = ce.User.Client.FollowNewsletter(info.ID)
if err != nil {
ce.Reply("Failed to follow channel: %v", err)
return
}
ce.Log.Debugln("%s successfully followed channel %s", ce.User.MXID, info.ID)
ce.Reply("Successfully followed channel `%s`, the portal should be created momentarily", info.ID)
} else {
ce.Reply("That doesn't look like a WhatsApp invite link")
}
ce.Log.Debugln("%s successfully joined group %s", ce.User.MXID, jid)
ce.Reply("Successfully joined group `%s`, the portal should be created momentarily", jid)
}
func tryDecryptEvent(crypto bridge.Crypto, evt *event.Event) (json.RawMessage, error) {
@ -382,7 +394,7 @@ func fnCreate(ce *WrappedCommandEvent) {
}
// TODO check m.space.parent to create rooms directly in communities
messageID := whatsmeow.GenerateMessageID()
messageID := ce.User.Client.GenerateMessageID()
ce.Log.Infofln("Creating group for %s with name %s and participants %+v (create key: %s)", ce.RoomID, roomNameEvent.Name, participants, messageID)
ce.User.createKeyDedup = messageID
resp, err := ce.User.Client.CreateGroup(whatsmeow.ReqCreateGroup{
@ -432,11 +444,16 @@ var cmdLogin = &commands.FullHandler{
Func: wrapCommand(fnLogin),
Name: "login",
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Link the bridge to your WhatsApp account as a web client.",
Section: commands.HelpSectionAuth,
Description: "Link the bridge to your WhatsApp account as a web client. " +
"The phone number parameter is optional: if provided, the bridge will create a 8-character login code " +
"that can be used instead of the QR code.",
Args: "[_phone number_]",
},
}
var looksLikeAPhoneRegex = regexp.MustCompile(`^\+[0-9]+$`)
func fnLogin(ce *WrappedCommandEvent) {
if ce.User.Session != nil {
if ce.User.IsConnected() {
@ -447,13 +464,33 @@ func fnLogin(ce *WrappedCommandEvent) {
return
}
var phoneNumber string
if len(ce.Args) > 0 {
phoneNumber = strings.TrimSpace(strings.Join(ce.Args, " "))
if !looksLikeAPhoneRegex.MatchString(phoneNumber) {
ce.Reply("When specifying a phone number, it must be provided in international format without spaces or other extra characters")
return
}
}
qrChan, err := ce.User.Login(context.Background())
if err != nil {
ce.User.log.Errorf("Failed to log in:", err)
ce.ZLog.Err(err).Msg("Failed to start login")
ce.Reply("Failed to log in: %v", err)
return
}
if phoneNumber != "" {
pairingCode, err := ce.User.Client.PairPhone(phoneNumber, true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil {
ce.ZLog.Err(err).Msg("Failed to start phone code login")
ce.Reply("Failed to start phone code login: %v", err)
go ce.User.DeleteConnection()
return
}
ce.Reply("Scan the code below or enter the following code on your phone to log in: **%s**", pairingCode)
}
var qrEventID id.EventID
for item := range qrChan {
switch item.Event {
@ -461,7 +498,7 @@ func fnLogin(ce *WrappedCommandEvent) {
jid := ce.User.Client.Store.ID
ce.Reply("Successfully logged in as +%s (device #%d)", jid.User, jid.Device)
case whatsmeow.QRChannelTimeout.Event:
ce.Reply("QR code timed out. Please restart the login.")
ce.Reply("Login timed out. Please restart the login.")
case whatsmeow.QRChannelErrUnexpectedEvent.Event:
ce.Reply("Failed to log in: unexpected connection event from server")
case whatsmeow.QRChannelClientOutdated.Event:
@ -474,7 +511,9 @@ func fnLogin(ce *WrappedCommandEvent) {
qrEventID = ce.User.sendQR(ce, item.Code, qrEventID)
}
}
_, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
if qrEventID != "" {
_, _ = ce.Bot.RedactEvent(ce.RoomID, qrEventID)
}
}
func (user *User) sendQR(ce *WrappedCommandEvent, code string, prevEvent id.EventID) id.EventID {
@ -536,12 +575,7 @@ func fnLogout(ce *WrappedCommandEvent) {
return
}
puppet := ce.Bridge.GetPuppetByJID(ce.User.JID)
if puppet.CustomMXID != "" {
err := puppet.SwitchCustomMXID("", "")
if err != nil {
ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err)
}
}
puppet.ClearCustomMXID()
err := ce.User.Client.Logout()
if err != nil {
ce.User.log.Warnln("Error while logging out:", err)
@ -787,46 +821,6 @@ func fnDeleteAllPortals(ce *WrappedCommandEvent) {
}()
}
var cmdBackfill = &commands.FullHandler{
Func: wrapCommand(fnBackfill),
Name: "backfill",
Help: commands.HelpMeta{
Section: HelpSectionPortalManagement,
Description: "Backfill all messages the portal.",
Args: "[_batch size_] [_batch delay_]",
},
RequiresPortal: true,
}
func fnBackfill(ce *WrappedCommandEvent) {
if !ce.Bridge.Config.Bridge.HistorySync.Backfill {
ce.Reply("Backfill is not enabled for this bridge.")
return
}
batchSize := 100
batchDelay := 5
if len(ce.Args) >= 1 {
var err error
batchSize, err = strconv.Atoi(ce.Args[0])
if err != nil || batchSize < 1 {
ce.Reply("\"%s\" isn't a valid batch size", ce.Args[0])
return
}
}
if len(ce.Args) >= 2 {
var err error
batchDelay, err = strconv.Atoi(ce.Args[0])
if err != nil || batchSize < 0 {
ce.Reply("\"%s\" isn't a valid batch delay", ce.Args[1])
return
}
}
backfillMessages := ce.Portal.bridge.DB.Backfill.NewWithValues(ce.User.MXID, database.BackfillImmediate, 0, &ce.Portal.Key, nil, batchSize, -1, batchDelay)
backfillMessages.Insert()
ce.User.BackfillQueue.ReCheck()
}
func matchesQuery(str string, query string) bool {
if query == "" {
return true
@ -1018,23 +1012,37 @@ func fnOpen(ce *WrappedCommandEvent) {
} else {
jid = types.NewJID(ce.Args[0], types.GroupServer)
}
if jid.Server != types.GroupServer || (!strings.ContainsRune(jid.User, '-') && len(jid.User) < 15) {
if (jid.Server != types.GroupServer && jid.Server != types.NewsletterServer) || (!strings.ContainsRune(jid.User, '-') && len(jid.User) < 15) {
ce.Reply("That does not look like a group JID")
return
}
info, err := ce.User.Client.GetGroupInfo(jid)
if err != nil {
ce.Reply("Failed to get group info: %v", err)
return
var err error
var groupInfo *types.GroupInfo
var newsletterMetadata *types.NewsletterMetadata
switch jid.Server {
case types.GroupServer:
groupInfo, err = ce.User.Client.GetGroupInfo(jid)
if err != nil {
ce.Reply("Failed to get group info: %v", err)
return
}
jid = groupInfo.JID
case types.NewsletterServer:
newsletterMetadata, err = ce.User.Client.GetNewsletterInfo(jid)
if err != nil {
ce.Reply("Failed to get channel info: %v", err)
return
}
jid = newsletterMetadata.ID
}
ce.Log.Debugln("Importing", jid, "for", ce.User.MXID)
portal := ce.User.GetPortalByJID(info.JID)
portal := ce.User.GetPortalByJID(jid)
if len(portal.MXID) > 0 {
portal.UpdateMatrixRoom(ce.User, info)
portal.UpdateMatrixRoom(ce.User, groupInfo, newsletterMetadata)
ce.Reply("Portal room synced.")
} else {
err = portal.CreateMatrixRoom(ce.User, info, true, true)
err = portal.CreateMatrixRoom(ce.User, groupInfo, newsletterMetadata, true, true)
if err != nil {
ce.Reply("Failed to create room: %v", err)
} else {

View file

@ -57,17 +57,16 @@ type BridgeConfig struct {
IdentityChangeNotices bool `yaml:"identity_change_notices"`
HistorySync struct {
CreatePortals bool `yaml:"create_portals"`
Backfill bool `yaml:"backfill"`
Backfill bool `yaml:"backfill"`
DoublePuppetBackfill bool `yaml:"double_puppet_backfill"`
RequestFullSync bool `yaml:"request_full_sync"`
FullSyncConfig struct {
RequestFullSync bool `yaml:"request_full_sync"`
FullSyncConfig struct {
DaysLimit uint32 `yaml:"days_limit"`
SizeLimit uint32 `yaml:"size_mb_limit"`
StorageQuota uint32 `yaml:"storage_quota_mb"`
}
MaxInitialConversations int `yaml:"max_initial_conversations"`
MessageCount int `yaml:"message_count"`
UnreadHoursThreshold int `yaml:"unread_hours_threshold"`
Immediate struct {
@ -86,18 +85,14 @@ type BridgeConfig struct {
UserAvatarSync bool `yaml:"user_avatar_sync"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
SyncDirectChatList bool `yaml:"sync_direct_chat_list"`
SyncManualMarkedUnread bool `yaml:"sync_manual_marked_unread"`
DefaultBridgeReceipts bool `yaml:"default_bridge_receipts"`
DefaultBridgePresence bool `yaml:"default_bridge_presence"`
SendPresenceOnTyping bool `yaml:"send_presence_on_typing"`
ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"`
DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"`
DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"`
PrivateChatPortalMeta string `yaml:"private_chat_portal_meta"`
ParallelMemberSync bool `yaml:"parallel_member_sync"`
@ -116,6 +111,7 @@ type BridgeConfig struct {
FederateRooms bool `yaml:"federate_rooms"`
URLPreviews bool `yaml:"url_previews"`
CaptionInMessage bool `yaml:"caption_in_message"`
BeeperGalleries bool `yaml:"beeper_galleries"`
ExtEvPolls bool `yaml:"extev_polls"`
CrossRoomReplies bool `yaml:"cross_room_replies"`
DisableReplyFallbacks bool `yaml:"disable_reply_fallbacks"`
@ -140,8 +136,9 @@ type BridgeConfig struct {
Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"`
Provisioning struct {
Prefix string `yaml:"prefix"`
SharedSecret string `yaml:"shared_secret"`
Prefix string `yaml:"prefix"`
SharedSecret string `yaml:"shared_secret"`
DebugEndpoints bool `yaml:"debug_endpoints"`
} `yaml:"provisioning"`
Permissions bridgeconfig.PermissionConfig `yaml:"permissions"`
@ -152,6 +149,10 @@ type BridgeConfig struct {
displaynameTemplate *template.Template `yaml:"-"`
}
func (bc BridgeConfig) GetDoublePuppetConfig() bridgeconfig.DoublePuppetConfig {
return bc.DoublePuppetConfig
}
func (bc BridgeConfig) GetEncryptionConfig() bridgeconfig.EncryptionConfig {
return bc.Encryption
}

View file

@ -24,8 +24,11 @@ import (
type Config struct {
*bridgeconfig.BaseConfig `yaml:",inline"`
SegmentKey string `yaml:"segment_key"`
SegmentUserID string `yaml:"segment_user_id"`
Analytics struct {
Host string `yaml:"host"`
Token string `yaml:"token"`
UserID string `yaml:"user_id"`
}
Metrics struct {
Enabled bool `yaml:"enabled"`
@ -42,18 +45,6 @@ type Config struct {
func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool {
_, homeserver, _ := userID.Parse()
_, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver]
_, hasSecret := config.Bridge.DoublePuppetConfig.SharedSecretMap[homeserver]
return hasSecret
}
func (config *Config) CanDoublePuppetBackfill(userID id.UserID) bool {
if !config.Bridge.HistorySync.DoublePuppetBackfill {
return false
}
_, homeserver, _ := userID.Parse()
// Batch sending can only use local users, so don't allow double puppets on other servers.
if homeserver != config.Homeserver.Domain && config.Homeserver.Software != bridgeconfig.SoftwareHungry {
return false
}
return true
}

View file

@ -19,16 +19,17 @@ package config
import (
"strings"
up "go.mau.fi/util/configupgrade"
"go.mau.fi/util/random"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/util"
up "maunium.net/go/mautrix/util/configupgrade"
)
func DoUpgrade(helper *up.Helper) {
bridgeconfig.Upgrader.DoUpgrade(helper)
helper.Copy(up.Str|up.Null, "segment_key")
helper.Copy(up.Str|up.Null, "segment_user_id")
helper.Copy(up.Str|up.Null, "analytics", "host")
helper.Copy(up.Str|up.Null, "analytics", "token")
helper.Copy(up.Str|up.Null, "analytics", "user_id")
helper.Copy(up.Bool, "metrics", "enabled")
helper.Copy(up.Str, "metrics", "listen")
@ -45,9 +46,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Int, "bridge", "portal_message_buffer")
helper.Copy(up.Bool, "bridge", "call_start_notices")
helper.Copy(up.Bool, "bridge", "identity_change_notices")
helper.Copy(up.Bool, "bridge", "history_sync", "create_portals")
helper.Copy(up.Bool, "bridge", "history_sync", "backfill")
helper.Copy(up.Bool, "bridge", "history_sync", "double_puppet_backfill")
helper.Copy(up.Bool, "bridge", "history_sync", "request_full_sync")
helper.Copy(up.Int|up.Null, "bridge", "history_sync", "full_sync_config", "days_limit")
helper.Copy(up.Int|up.Null, "bridge", "history_sync", "full_sync_config", "size_mb_limit")
@ -56,15 +55,14 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Str, "bridge", "history_sync", "media_requests", "request_method")
helper.Copy(up.Int, "bridge", "history_sync", "media_requests", "request_local_time")
helper.Copy(up.Int, "bridge", "history_sync", "max_initial_conversations")
helper.Copy(up.Int, "bridge", "history_sync", "message_count")
helper.Copy(up.Int, "bridge", "history_sync", "unread_hours_threshold")
helper.Copy(up.Int, "bridge", "history_sync", "immediate", "worker_count")
helper.Copy(up.Int, "bridge", "history_sync", "immediate", "max_events")
helper.Copy(up.List, "bridge", "history_sync", "deferred")
helper.Copy(up.Bool, "bridge", "user_avatar_sync")
helper.Copy(up.Bool, "bridge", "bridge_matrix_leave")
helper.Copy(up.Bool, "bridge", "sync_with_custom_puppets")
helper.Copy(up.Bool, "bridge", "sync_direct_chat_list")
helper.Copy(up.Bool, "bridge", "default_bridge_receipts")
helper.Copy(up.Bool, "bridge", "default_bridge_presence")
helper.Copy(up.Bool, "bridge", "send_presence_on_typing")
helper.Copy(up.Bool, "bridge", "force_active_delivery_receipts")
@ -105,6 +103,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "crash_on_stream_replaced")
helper.Copy(up.Bool, "bridge", "url_previews")
helper.Copy(up.Bool, "bridge", "caption_in_message")
helper.Copy(up.Bool, "bridge", "beeper_galleries")
if intPolls, ok := helper.Get(up.Int, "bridge", "extev_polls"); ok {
val := "false"
if intPolls != "0" {
@ -135,6 +134,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_prev_on_new_session")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_on_device_delete")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "periodically_delete_expired")
helper.Copy(up.Bool, "bridge", "encryption", "delete_keys", "delete_outdated_inbound")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "receive")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "send")
helper.Copy(up.Str, "bridge", "encryption", "verification_levels", "share")
@ -160,10 +160,11 @@ func DoUpgrade(helper *up.Helper) {
} else {
helper.Copy(up.Str, "bridge", "provisioning", "prefix")
}
helper.Copy(up.Bool, "bridge", "provisioning", "debug_endpoints")
if secret, ok := helper.Get(up.Str, "appservice", "provisioning", "shared_secret"); ok && secret != "generate" {
helper.Set(up.Str, secret, "bridge", "provisioning", "shared_secret")
} else if secret, ok = helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
sharedSecret := util.RandomString(64)
sharedSecret := random.String(64)
helper.Set(up.Str, sharedSecret, "bridge", "provisioning", "shared_secret")
} else {
helper.Copy(up.Str, "bridge", "provisioning", "shared_secret")
@ -181,7 +182,7 @@ var SpacedBlocks = [][]string{
{"appservice", "database"},
{"appservice", "id"},
{"appservice", "as_token"},
{"segment_key"},
{"analytics"},
{"metrics"},
{"whatsapp"},
{"bridge"},

View file

@ -17,262 +17,74 @@
package main
import (
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"time"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var (
ErrNoCustomMXID = errors.New("no custom mxid set")
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
)
func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
prevCustomMXID := puppet.CustomMXID
if puppet.customIntent != nil {
puppet.stopSyncing()
}
puppet.CustomMXID = mxid
puppet.AccessToken = accessToken
puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
puppet.Update()
err := puppet.StartCustomMXID(false)
if err != nil {
return err
}
if len(prevCustomMXID) > 0 {
delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID)
}
if len(puppet.CustomMXID) > 0 {
puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
}
puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts
puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID)
puppet.Update()
// TODO leave rooms with default puppet
return nil
}
func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
_, homeserver, _ := mxid.Parse()
puppet.log.Debugfln("Logging into %s with shared secret", mxid)
loginSecret := puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
if err != nil {
return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
func (puppet *Puppet) ClearCustomMXID() {
save := puppet.CustomMXID != "" || puppet.AccessToken != ""
puppet.bridge.puppetsLock.Lock()
if puppet.CustomMXID != "" && puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] == puppet {
delete(puppet.bridge.puppetsByCustomMXID, puppet.CustomMXID)
}
req := mautrix.ReqLogin{
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
DeviceID: "WhatsApp Bridge",
InitialDeviceDisplayName: "WhatsApp Bridge",
}
if loginSecret == "appservice" {
client.AccessToken = puppet.bridge.AS.Registration.AppToken
req.Type = mautrix.AuthTypeAppservice
} else {
mac := hmac.New(sha512.New, []byte(loginSecret))
mac.Write([]byte(mxid))
req.Password = hex.EncodeToString(mac.Sum(nil))
req.Type = mautrix.AuthTypePassword
}
resp, err := client.Login(&req)
if err != nil {
return "", err
}
return resp.AccessToken, nil
}
func (br *WABridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
_, homeserver, err := mxid.Parse()
if err != nil {
return nil, err
}
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
if !found {
if homeserver == br.AS.HomeserverDomain {
homeserverURL = ""
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
resp, err := mautrix.DiscoverClientAPI(homeserver)
if err != nil {
return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
}
homeserverURL = resp.Homeserver.BaseURL
br.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid)
} else {
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
}
}
return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
}
func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
if len(puppet.CustomMXID) == 0 {
return nil, ErrNoCustomMXID
}
client, err := puppet.bridge.newDoublePuppetClient(puppet.CustomMXID, puppet.AccessToken)
if err != nil {
return nil, err
}
client.Syncer = puppet
client.Store = puppet
ia := puppet.bridge.AS.NewIntentAPI("custom")
ia.Client = client
ia.Localpart, _, _ = puppet.CustomMXID.Parse()
ia.UserID = puppet.CustomMXID
ia.IsCustomPuppet = true
return ia, nil
}
func (puppet *Puppet) clearCustomMXID() {
puppet.bridge.puppetsLock.Unlock()
puppet.CustomMXID = ""
puppet.AccessToken = ""
puppet.customIntent = nil
puppet.customUser = nil
if save {
puppet.Update()
}
}
func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
if len(puppet.CustomMXID) == 0 {
puppet.clearCustomMXID()
return nil
}
intent, err := puppet.newCustomIntent()
newIntent, newAccessToken, err := puppet.bridge.DoublePuppet.Setup(puppet.CustomMXID, puppet.AccessToken, reloginOnFail)
if err != nil {
puppet.clearCustomMXID()
puppet.ClearCustomMXID()
return err
}
resp, err := intent.Whoami()
if err != nil {
if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) {
puppet.clearCustomMXID()
return err
}
intent.AccessToken = puppet.AccessToken
} else if resp.UserID != puppet.CustomMXID {
puppet.clearCustomMXID()
return ErrMismatchingMXID
puppet.bridge.puppetsLock.Lock()
puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
puppet.bridge.puppetsLock.Unlock()
if puppet.AccessToken != newAccessToken {
puppet.AccessToken = newAccessToken
puppet.Update()
}
puppet.customIntent = intent
puppet.customIntent = newIntent
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
puppet.startSyncing()
return nil
}
func (puppet *Puppet) startSyncing() {
if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
func (user *User) tryAutomaticDoublePuppeting() {
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
return
}
go func() {
puppet.log.Debugln("Starting syncing...")
puppet.customIntent.SyncPresence = "offline"
err := puppet.customIntent.Sync()
if err != nil {
puppet.log.Errorln("Fatal error syncing:", err)
}
}()
}
func (puppet *Puppet) stopSyncing() {
if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
user.zlog.Debug().Msg("Checking if double puppeting needs to be enabled")
puppet := user.bridge.GetPuppetByJID(user.JID)
if len(puppet.CustomMXID) > 0 {
user.zlog.Debug().Msg("User already has double-puppeting enabled")
// Custom puppet already enabled
return
}
puppet.customIntent.StopSync()
}
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
if !puppet.customUser.IsLoggedIn() {
puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
return nil
}
for roomID, events := range resp.Rooms.Join {
for _, evt := range events.Ephemeral.Events {
evt.RoomID = roomID
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
continue
}
switch evt.Type {
case event.EphemeralEventReceipt:
if puppet.EnableReceipts {
go puppet.bridge.MatrixHandler.HandleReceipt(evt)
}
case event.EphemeralEventTyping:
go puppet.bridge.MatrixHandler.HandleTyping(evt)
}
}
}
if puppet.EnablePresence {
for _, evt := range resp.Presence.Events {
if evt.Sender != puppet.CustomMXID {
continue
}
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
continue
}
go puppet.bridge.HandlePresence(evt)
}
}
return nil
}
func (puppet *Puppet) tryRelogin(cause error, action string) bool {
if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
return false
}
puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
puppet.CustomMXID = user.MXID
puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
err := puppet.StartCustomMXID(true)
if err != nil {
puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
return false
}
puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
puppet.AccessToken = accessToken
return true
}
func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
puppet.log.Warnln("Sync error:", err)
if errors.Is(err, mautrix.MUnknownToken) {
if !puppet.tryRelogin(err, "syncing") {
return 0, err
}
puppet.customIntent.AccessToken = puppet.AccessToken
return 0, nil
}
return 10 * time.Second, nil
}
func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
everything := []event.Type{{Type: "*"}}
return &mautrix.Filter{
Presence: mautrix.FilterPart{
Senders: []id.UserID{puppet.CustomMXID},
Types: []event.Type{event.EphemeralEventPresence},
},
AccountData: mautrix.FilterPart{NotTypes: everything},
Room: mautrix.RoomFilter{
Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
IncludeLeave: false,
AccountData: mautrix.FilterPart{NotTypes: everything},
State: mautrix.FilterPart{NotTypes: everything},
Timeline: mautrix.FilterPart{NotTypes: everything},
},
user.zlog.Warn().Err(err).Msg("Failed to login with shared secret")
} else {
// TODO leave rooms with default puppet
user.zlog.Debug().Msg("Successfully automatically enabled custom puppet")
}
}
func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {}
func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() }
func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {}
func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" }
func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch }
func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil }

View file

@ -25,10 +25,10 @@ import (
"sync"
"time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
)
type BackfillType int

View file

@ -23,10 +23,10 @@ import (
"github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/util/dbutil"
"maunium.net/go/mautrix-whatsapp/database/upgrades"
)

View file

@ -21,10 +21,10 @@ import (
"errors"
"time"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
)
type DisappearingMessageQuery struct {

View file

@ -22,15 +22,13 @@ import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
waProto "go.mau.fi/whatsmeow/binary/proto"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/util/dbutil"
waProto "go.mau.fi/whatsmeow/binary/proto"
"google.golang.org/protobuf/proto"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
)
type HistorySyncQuery struct {
@ -166,7 +164,7 @@ func (hsc *HistorySyncConversation) Scan(row dbutil.Scannable) *HistorySyncConve
return hsc
}
func (hsq *HistorySyncQuery) GetNMostRecentConversations(userID id.UserID, n int) (conversations []*HistorySyncConversation) {
func (hsq *HistorySyncQuery) GetRecentConversations(userID id.UserID, n int) (conversations []*HistorySyncConversation) {
nPtr := &n
// Negative limit on SQLite means unlimited, but Postgres prefers a NULL limit.
if n < 0 && hsq.db.Dialect == dbutil.Postgres {
@ -183,7 +181,7 @@ func (hsq *HistorySyncQuery) GetNMostRecentConversations(userID id.UserID, n int
return
}
func (hsq *HistorySyncQuery) GetConversation(userID id.UserID, portalKey *PortalKey) (conversation *HistorySyncConversation) {
func (hsq *HistorySyncQuery) GetConversation(userID id.UserID, portalKey PortalKey) (conversation *HistorySyncConversation) {
rows, err := hsq.db.Query(getConversationByPortal, userID, portalKey.JID, portalKey.Receiver)
defer rows.Close()
if err != nil || rows == nil {
@ -323,3 +321,27 @@ func (hsq *HistorySyncQuery) DeleteAllMessagesForPortal(userID id.UserID, portal
hsq.log.Warnfln("Failed to delete historical messages for %s/%s: %v", userID, portalKey.JID, err)
}
}
func (hsq *HistorySyncQuery) ConversationHasMessages(userID id.UserID, portalKey PortalKey) (exists bool) {
err := hsq.db.QueryRow(`
SELECT EXISTS(
SELECT 1 FROM history_sync_message
WHERE user_mxid=$1 AND conversation_id=$2
)
`, userID, portalKey.JID).Scan(&exists)
if err != nil {
hsq.log.Warnfln("Failed to check if any messages are stored for %s/%s: %v", userID, portalKey.JID, err)
}
return
}
func (hsq *HistorySyncQuery) DeleteConversation(userID id.UserID, jid string) {
// This will also clear history_sync_message as there's a foreign key constraint
_, err := hsq.db.Exec(`
DELETE FROM history_sync_conversation
WHERE user_mxid=$1 AND conversation_id=$2
`, userID, jid)
if err != nil {
hsq.log.Warnfln("Failed to delete historical messages for %s/%s: %v", userID, jid, err)
}
}

View file

@ -21,10 +21,10 @@ import (
"errors"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/util/dbutil"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
)
type MediaBackfillRequestStatus int

View file

@ -19,15 +19,15 @@ package database
import (
"database/sql"
"errors"
"fmt"
"strings"
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/whatsmeow/types"
)
type MessageQuery struct {
@ -134,12 +134,13 @@ const (
type MessageType string
const (
MsgUnknown MessageType = ""
MsgFake MessageType = "fake"
MsgNormal MessageType = "message"
MsgReaction MessageType = "reaction"
MsgEdit MessageType = "edit"
MsgMatrixPoll MessageType = "matrix-poll"
MsgUnknown MessageType = ""
MsgFake MessageType = "fake"
MsgNormal MessageType = "message"
MsgReaction MessageType = "reaction"
MsgEdit MessageType = "edit"
MsgMatrixPoll MessageType = "matrix-poll"
MsgBeeperGallery MessageType = "beeper-gallery"
)
type Message struct {
@ -156,6 +157,8 @@ type Message struct {
Type MessageType
Error MessageErrorType
GalleryPart int
BroadcastListJID types.JID
}
@ -167,6 +170,8 @@ func (msg *Message) IsFakeJID() bool {
return strings.HasPrefix(msg.JID, "FAKE::") || msg.JID == string(msg.MXID)
}
const fakeGalleryMXIDFormat = "com.beeper.gallery::%d:%s"
func (msg *Message) Scan(row dbutil.Scannable) *Message {
var ts int64
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.SenderMXID, &ts, &msg.Sent, &msg.Type, &msg.Error, &msg.BroadcastListJID)
@ -176,6 +181,12 @@ func (msg *Message) Scan(row dbutil.Scannable) *Message {
}
return nil
}
if strings.HasPrefix(msg.MXID.String(), "com.beeper.gallery::") {
_, err = fmt.Sscanf(msg.MXID.String(), fakeGalleryMXIDFormat, &msg.GalleryPart, &msg.MXID)
if err != nil {
msg.log.Errorln("Parsing gallery MXID failed:", err)
}
}
if ts != 0 {
msg.Timestamp = time.Unix(ts, 0)
}
@ -191,11 +202,15 @@ func (msg *Message) Insert(txn dbutil.Execable) {
if msg.Sender.IsEmpty() {
sender = ""
}
mxid := msg.MXID.String()
if msg.GalleryPart != 0 {
mxid = fmt.Sprintf(fakeGalleryMXIDFormat, msg.GalleryPart, mxid)
}
_, err := txn.Exec(`
INSERT INTO message
(chat_jid, chat_receiver, jid, mxid, sender, sender_mxid, timestamp, sent, type, error, broadcast_list_jid)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, sender, msg.SenderMXID, msg.Timestamp.Unix(), msg.Sent, msg.Type, msg.Error, msg.BroadcastListJID)
`, msg.Chat.JID, msg.Chat.Receiver, msg.JID, mxid, sender, msg.SenderMXID, msg.Timestamp.Unix(), msg.Sent, msg.Type, msg.Error, msg.BroadcastListJID)
if err != nil {
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
}

View file

@ -21,7 +21,7 @@ import (
"strings"
"github.com/lib/pq"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/util/dbutil"
)
func scanPollOptionMapping(rows dbutil.Rows) (id string, hashArr [32]byte, err error) {

View file

@ -21,12 +21,11 @@ import (
"fmt"
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/whatsmeow/types"
)
type PortalKey struct {
@ -35,7 +34,7 @@ type PortalKey struct {
}
func NewPortalKey(jid, receiver types.JID) PortalKey {
if jid.Server == types.GroupServer {
if jid.Server == types.GroupServer || jid.Server == types.NewsletterServer {
receiver = jid
} else if jid.Server == types.LegacyUserServer {
jid.Server = types.DefaultUserServer

View file

@ -20,12 +20,11 @@ import (
"database/sql"
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/whatsmeow/types"
)
type PuppetQuery struct {

View file

@ -20,12 +20,11 @@ import (
"database/sql"
"errors"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/whatsmeow/types"
)
type ReactionQuery struct {

View file

@ -1,4 +1,4 @@
-- v0 -> v56 (compatible with v45+): Latest revision
-- v0 -> v57 (compatible with v45+): Latest revision
CREATE TABLE "user" (
mxid TEXT PRIMARY KEY,
@ -82,6 +82,8 @@ CREATE TABLE message (
FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) ON DELETE CASCADE
);
CREATE INDEX message_timestamp_idx ON message (chat_jid, chat_receiver, timestamp);
CREATE TABLE poll_option_id (
msg_mxid TEXT,
opt_id TEXT,

View file

@ -0,0 +1,2 @@
-- v57 (compatible with v45+): Add index for message timestamp to make read receipt handling faster
CREATE INDEX message_timestamp_idx ON message (chat_jid, chat_receiver, timestamp);

View file

@ -20,7 +20,7 @@ import (
"embed"
"errors"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/util/dbutil"
)
var Table dbutil.UpgradeTable

View file

@ -21,12 +21,11 @@ import (
"sync"
"time"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"go.mau.fi/whatsmeow/types"
)
type UserQuery struct {
@ -123,14 +122,16 @@ func (user *User) usernamePtr() *string {
func (user *User) agentPtr() *uint8 {
if !user.JID.IsEmpty() {
return &user.JID.Agent
zero := uint8(0)
return &zero
}
return nil
}
func (user *User) devicePtr() *uint8 {
if !user.JID.IsEmpty() {
return &user.JID.Device
device := uint8(user.JID.Device)
return &device
}
return nil
}

View file

@ -20,9 +20,10 @@ import (
"fmt"
"time"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"maunium.net/go/mautrix-whatsapp/database"
)

View file

@ -65,7 +65,6 @@ appservice:
# Whether or not to receive ephemeral events via appservice transactions.
# Requires MSC2409 support (i.e. Synapse 1.22+).
# You should disable bridge -> sync_with_custom_puppets when this is enabled.
ephemeral_events: true
# Should incoming events be handled asynchronously?
@ -77,10 +76,14 @@ appservice:
as_token: "This value is generated when generating the registration"
hs_token: "This value is generated when generating the registration"
# Segment API key to track some events, like provisioning API login and encryption errors.
segment_key: null
# Optional user_id to use when sending Segment events. If null, defaults to using mxID.
segment_user_id: null
# Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors.
analytics:
# Hostname of the tracking server. The path is hardcoded to /v1/track
host: api.segment.io
# API key to send with tracking requests. Tracking is disabled if this is null.
token: null
# Optional user ID for tracking events. If null, defaults to using Matrix user ID.
user_id: null
# Prometheus config.
metrics:
@ -95,7 +98,7 @@ whatsapp:
os_name: Mautrix-WhatsApp bridge
# Browser name that determines the logo shown in the mobile app.
# Must be "unknown" for a generic icon or a valid browser name if you want a specific icon.
# List of valid browser names: https://github.com/tulir/whatsmeow/blob/8b34d886d543b72e5f4699cf5b2797f68d598f78/binary/proto/def.proto#L38-L51
# List of valid browser names: https://github.com/tulir/whatsmeow/blob/efc632c008604016ddde63bfcfca8de4e5304da9/binary/proto/def.proto#L43-L64
browser_name: unknown
# Bridge config
@ -127,20 +130,14 @@ bridge:
portal_message_buffer: 128
# Settings for handling history sync payloads.
history_sync:
# Enable backfilling history sync payloads from WhatsApp using batch sending?
# This requires a server with MSC2716 support, which is currently an experimental feature in synapse.
# It can be enabled by setting experimental_features -> msc2716_enabled to true in homeserver.yaml.
# Note that prior to Synapse 1.49, there were some bugs with the implementation, especially if using event persistence workers.
# There are also still some issues in Synapse's federation implementation.
backfill: false
# Should the bridge create portals for chats in the history sync payload?
# This has no effect unless backfill is enabled.
create_portals: true
# Use double puppets for backfilling?
# In order to use this, the double puppets must be in the appservice's user ID namespace
# (because the bridge can't use the double puppet access token with batch sending).
# This only affects double puppets on the local server, double puppets on other servers will never be used.
double_puppet_backfill: false
# Enable backfilling history sync payloads from WhatsApp?
backfill: true
# The maximum number of initial conversations that should be synced.
# Other conversations will be backfilled on demand when receiving a message or when initiating a direct chat.
max_initial_conversations: -1
# Maximum number of messages to backfill in each conversation.
# Set to -1 to disable limit.
message_count: 50
# 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
@ -154,51 +151,43 @@ bridge:
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.
media_requests:
# Should expired media be automatically requested from the server as
# part of the backfill process?
auto_request_media: true
# Whether to request the media immediately after the media message
# is backfilled ("immediate") or at a specific time of the day
# ("local_time").
request_method: immediate
# If request_method is "local_time", what time should the requests
# be sent (in minutes after midnight)?
request_local_time: 120
# The maximum number of initial conversations that should be synced.
# Other conversations will be backfilled on demand when the start PM
# provisioning endpoint is used or when a message comes in from that
# chat.
max_initial_conversations: -1
# If this value is greater than 0, then if the conversation's last
# message was more than this number of hours ago, then the conversation
# will automatically be marked it as read.
# Conversations that have a last message that is less than this number
# of hours ago will have their unread status synced from WhatsApp.
# If this value is greater than 0, then if the conversation's last message was more than
# this number of hours ago, then the conversation will automatically be marked it as read.
# Conversations that have a last message that is less than this number of hours ago will
# have their unread status synced from WhatsApp.
unread_hours_threshold: 0
# Settings for immediate backfills. These backfills should generally be
# small and their main purpose is to populate each of the initial chats
# (as configured by max_initial_conversations) with a few messages so
# that you can continue conversations without loosing context.
###############################################################################
# The settings below are only applicable for backfilling using batch sending, #
# which is no longer supported in Synapse. #
###############################################################################
# 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.
media_requests:
# Should expired media be automatically requested from the server as part of the backfill process?
auto_request_media: true
# Whether to request the media immediately after the media message is backfilled ("immediate")
# or at a specific time of the day ("local_time").
request_method: immediate
# If request_method is "local_time", what time should the requests be sent (in minutes after midnight)?
request_local_time: 120
# Settings for immediate backfills. These backfills should generally be small and their main purpose is
# to populate each of the initial chats (as configured by max_initial_conversations) with a few messages
# so that you can continue conversations without losing context.
immediate:
# The number of concurrent backfill workers to create for immediate
# backfills. Note that using more than one worker could cause the
# room list to jump around since there are no guarantees about the
# order in which the backfills will complete.
# The number of concurrent backfill workers to create for immediate backfills.
# Note that using more than one worker could cause the room list to jump around
# since there are no guarantees about the order in which the backfills will complete.
worker_count: 1
# The maximum number of events to backfill initially.
max_events: 10
# Settings for deferred backfills. The purpose of these backfills are
# to fill in the rest of the chat history that was not covered by the
# immediate backfills. These backfills generally should happen at a
# slower pace so as not to overload the homeserver.
# Each deferred backfill config should define a "stage" of backfill
# (i.e. the last week of messages). The fields are as follows:
# Settings for deferred backfills. The purpose of these backfills are to fill in the rest of
# the chat history that was not covered by the immediate backfills.
# These backfills generally should happen at a slower pace so as not to overload the homeserver.
# Each deferred backfill config should define a "stage" of backfill (i.e. the last week of messages).
# The fields are as follows:
# - start_days_ago: the number of days ago to start backfilling from.
# To indicate the start of time, use -1. For example, for a week ago, use 7.
# - max_batch_events: the number of events to send per batch.
@ -220,12 +209,11 @@ bridge:
- start_days_ago: -1
max_batch_events: 500
batch_delay: 10
# Should puppet avatars be fetched from the server even if an avatar is already set?
user_avatar_sync: true
# Should Matrix users leaving groups be bridged to WhatsApp?
bridge_matrix_leave: true
# Should the bridge sync with double puppeting to receive EDUs that aren't normally sent to appservices.
sync_with_custom_puppets: false
# Should the bridge update the m.direct account data event when double puppeting is enabled.
# Note that updating the m.direct event is not atomic (except with mautrix-asmux)
# and is therefore prone to race conditions.
@ -236,9 +224,8 @@ bridge:
# com.famedly.marked_unread room account data.
sync_manual_marked_unread: true
# When double puppeting is enabled, users can use `!wa toggle` to change whether
# presence and read receipts are bridged. These settings set the default values.
# presence is bridged. This setting sets the default value.
# Existing users won't be affected when these are changed.
default_bridge_receipts: true
default_bridge_presence: true
# Send the presence as "available" to whatsapp when users start typing on a portal.
# This works as a workaround for homeservers that do not support presence, and allows
@ -317,6 +304,8 @@ bridge:
# Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552.
# This is currently not supported in most clients.
caption_in_message: false
# Send galleries as a single event? This is not an MSC (yet).
beeper_galleries: false
# Should polls be sent using MSC3381 event types?
extev_polls: false
# Should cross-chat replies from WhatsApp be bridged? Most servers and clients don't support this.
@ -385,6 +374,10 @@ bridge:
delete_on_device_delete: false
# Periodically delete megolm sessions when 2x max_age has passed since receiving the session.
periodically_delete_expired: false
# Delete inbound megolm sessions that don't have the received_at field used for
# automatic ratcheting and expired session deletion. This is meant as a migration
# to delete old keys prior to the bridge update.
delete_outdated_inbound: false
# What level of device verification should be required from users?
#
# Valid levels:
@ -431,6 +424,8 @@ bridge:
# Shared secret for authentication. If set to "generate", a random secret will be generated,
# or if set to "disable", the provisioning API will be disabled.
shared_secret: generate
# Enable debug API at /debug with provisioning authentication.
debug_endpoints: false
# Permissions for using the bridge.
# Permitted values:

View file

@ -141,6 +141,9 @@ func (formatter *Formatter) ParseWhatsApp(roomID id.RoomID, content *event.Messa
continue
} else if jid.Server == types.LegacyUserServer {
jid.Server = types.DefaultUserServer
} else if jid.Server != types.DefaultUserServer {
// TODO lid support?
continue
}
mxid, displayname := formatter.getMatrixInfoByJID(roomID, jid)
number := "@" + jid.User

51
go.mod
View file

@ -1,24 +1,25 @@
module maunium.net/go/mautrix-whatsapp
go 1.19
go 1.20
require (
github.com/chai2010/webp v1.1.1
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.17
github.com/prometheus/client_golang v1.15.1
github.com/rs/zerolog v1.29.1
github.com/mattn/go-sqlite3 v1.14.19
github.com/prometheus/client_golang v1.17.0
github.com/rs/zerolog v1.31.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tidwall/gjson v1.14.4
go.mau.fi/whatsmeow v0.0.0-20230608204524-7aedaa1de108
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea
golang.org/x/image v0.7.0
golang.org/x/net v0.10.0
google.golang.org/protobuf v1.30.0
github.com/tidwall/gjson v1.17.0
go.mau.fi/util v0.2.1
go.mau.fi/webp v0.1.0
go.mau.fi/whatsmeow v0.0.0-20231216213200-9d803dd92735
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
golang.org/x/image v0.14.0
golang.org/x/net v0.19.0
google.golang.org/protobuf v1.31.0
maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.15.3-0.20230609124302-54a73ab22ef9
maunium.net/go/mautrix v0.16.2
)
require (
@ -28,30 +29,22 @@ require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark v1.6.0 // indirect
go.mau.fi/libsignal v0.1.0 // indirect
go.mau.fi/zeroconfig v0.1.2 // indirect
golang.org/x/crypto v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/crypto v0.16.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.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
)
// Exclude some things that cause go.sum to explode
exclude (
cloud.google.com/go v0.65.0
github.com/prometheus/client_golang v1.12.1
google.golang.org/appengine v1.6.6
)

124
go.sum
View file

@ -5,15 +5,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@ -28,99 +25,74 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
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/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/whatsmeow v0.0.0-20230608204524-7aedaa1de108 h1:kDOgPHj0urv2vsXyE8dt+KgyC18jxzQW48HGXa53pzc=
go.mau.fi/whatsmeow v0.0.0-20230608204524-7aedaa1de108/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE=
go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw=
go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c=
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-20231216213200-9d803dd92735 h1:+teJYCOK6M4Kn2TYCj29levhHVwnJTmgCtEXLtgwQtM=
go.mau.fi/whatsmeow v0.0.0-20231216213200-9d803dd92735/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea h1:vLCWI/yYrdEHyN2JzIzPO3aaQJHQdp89IZBA/+azVC4=
golang.org/x/exp v0.0.0-20230510235704-dd950f8aeaea/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
@ -131,5 +103,5 @@ 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/maulogger/v2 v2.4.1 h1:N7zSdd0mZkB2m2JtFUsiGTQQAdP0YeFWT7YMc80yAL8=
maunium.net/go/maulogger/v2 v2.4.1/go.mod h1:omPuYwYBILeVQobz8uO3XC8DIRuEb5rXYlQSuqrbCho=
maunium.net/go/mautrix v0.15.3-0.20230609124302-54a73ab22ef9 h1:MixixPn9FMv99V/6wdwDB18HR/1m69JDAc4VcEar8Wc=
maunium.net/go/mautrix v0.15.3-0.20230609124302-54a73ab22ef9/go.mod h1:h4NwfKqE4YxGTLSgn/gawKzXAb2sF4qx8agL6QEFtGg=
maunium.net/go/mautrix v0.16.2 h1:a6GUJXNWsTEOO8VE4dROBfCIfPp50mqaqzv7KPzChvg=
maunium.net/go/mautrix v0.16.2/go.mod h1:YL4l4rZB46/vj/ifRMEjcibbvHjgxHftOF1SgmruLu4=

View file

@ -20,19 +20,19 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"time"
"maunium.net/go/mautrix/util/variationselector"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"go.mau.fi/util/variationselector"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/dbutil"
"maunium.net/go/mautrix-whatsapp/config"
"maunium.net/go/mautrix-whatsapp/database"
@ -60,25 +60,28 @@ func (user *User) handleHistorySyncsLoop() {
return
}
// Start the backfill queue.
user.BackfillQueue = &BackfillQueue{
BackfillQuery: user.bridge.DB.Backfill,
reCheckChannels: []chan bool{},
log: user.log.Sub("BackfillQueue"),
batchSend := user.bridge.SpecVersions.Supports(mautrix.BeeperFeatureBatchSending)
if batchSend {
// Start the backfill queue.
user.BackfillQueue = &BackfillQueue{
BackfillQuery: user.bridge.DB.Backfill,
reCheckChannels: []chan bool{},
log: user.log.Sub("BackfillQueue"),
}
forwardAndImmediate := []database.BackfillType{database.BackfillImmediate, database.BackfillForward}
// Immediate backfills can be done in parallel
for i := 0; i < user.bridge.Config.Bridge.HistorySync.Immediate.WorkerCount; i++ {
go user.HandleBackfillRequestsLoop(forwardAndImmediate, []database.BackfillType{})
}
// Deferred backfills should be handled synchronously so as not to
// overload the homeserver. Users can configure their backfill stages
// to be more or less aggressive with backfilling at this stage.
go user.HandleBackfillRequestsLoop([]database.BackfillType{database.BackfillDeferred}, forwardAndImmediate)
}
forwardAndImmediate := []database.BackfillType{database.BackfillImmediate, database.BackfillForward}
// Immediate backfills can be done in parallel
for i := 0; i < user.bridge.Config.Bridge.HistorySync.Immediate.WorkerCount; i++ {
go user.HandleBackfillRequestsLoop(forwardAndImmediate, []database.BackfillType{})
}
// Deferred backfills should be handled synchronously so as not to
// overload the homeserver. Users can configure their backfill stages
// to be more or less aggressive with backfilling at this stage.
go user.HandleBackfillRequestsLoop([]database.BackfillType{database.BackfillDeferred}, forwardAndImmediate)
if user.bridge.Config.Bridge.HistorySync.MediaRequests.AutoRequestMedia &&
user.bridge.Config.Bridge.HistorySync.MediaRequests.RequestMethod == config.MediaRequestMethodLocalTime {
go user.dailyMediaRequestLoop()
@ -92,9 +95,13 @@ func (user *User) handleHistorySyncsLoop() {
if evt == nil {
return
}
user.handleHistorySync(user.BackfillQueue, evt.Data)
user.storeHistorySync(evt.Data)
case <-user.enqueueBackfillsTimer.C:
user.enqueueAllBackfills()
if batchSend {
user.enqueueAllBackfills()
} else {
user.backfillAll()
}
}
}
}
@ -102,7 +109,7 @@ func (user *User) handleHistorySyncsLoop() {
const EnqueueBackfillsDelay = 30 * time.Second
func (user *User) enqueueAllBackfills() {
nMostRecent := user.bridge.DB.HistorySync.GetNMostRecentConversations(user.MXID, user.bridge.Config.Bridge.HistorySync.MaxInitialConversations)
nMostRecent := user.bridge.DB.HistorySync.GetRecentConversations(user.MXID, user.bridge.Config.Bridge.HistorySync.MaxInitialConversations)
if len(nMostRecent) > 0 {
user.log.Infofln("%v has passed since the last history sync blob, enqueueing backfills for %d chats", EnqueueBackfillsDelay, len(nMostRecent))
// Find the portals for all the conversations.
@ -125,6 +132,82 @@ func (user *User) enqueueAllBackfills() {
}
}
func (user *User) backfillAll() {
conversations := user.bridge.DB.HistorySync.GetRecentConversations(user.MXID, -1)
if len(conversations) > 0 {
user.zlog.Info().
Int("conversation_count", len(conversations)).
Msg("Probably received all history sync blobs, now backfilling conversations")
limit := user.bridge.Config.Bridge.HistorySync.MaxInitialConversations
bridgedCount := 0
// Find the portals for all the conversations.
for _, conv := range conversations {
jid, err := types.ParseJID(conv.ConversationID)
if err != nil {
user.zlog.Warn().Err(err).
Str("conversation_id", conv.ConversationID).
Msg("Failed to parse chat JID in history sync")
continue
}
portal := user.GetPortalByJID(jid)
if portal.MXID != "" {
user.zlog.Debug().
Str("portal_jid", portal.Key.JID.String()).
Msg("Chat already has a room, deleting messages from database")
user.bridge.DB.HistorySync.DeleteConversation(user.MXID, portal.Key.JID.String())
bridgedCount++
} else if !user.bridge.DB.HistorySync.ConversationHasMessages(user.MXID, portal.Key) {
user.zlog.Debug().Str("portal_jid", portal.Key.JID.String()).Msg("Skipping chat with no messages in history sync")
user.bridge.DB.HistorySync.DeleteConversation(user.MXID, portal.Key.JID.String())
} else if limit < 0 || bridgedCount < limit {
bridgedCount++
err = portal.CreateMatrixRoom(user, nil, nil, true, true)
if err != nil {
user.zlog.Err(err).Msg("Failed to create Matrix room for backfill")
}
}
}
}
}
func (portal *Portal) legacyBackfill(user *User) {
defer portal.latestEventBackfillLock.Unlock()
// This should only be called from CreateMatrixRoom which locks latestEventBackfillLock before creating the room.
if portal.latestEventBackfillLock.TryLock() {
panic("legacyBackfill() called without locking latestEventBackfillLock")
}
// TODO use portal.zlog instead of user.zlog
log := user.zlog.With().
Str("portal_jid", portal.Key.JID.String()).
Str("action", "legacy backfill").
Logger()
conv := user.bridge.DB.HistorySync.GetConversation(user.MXID, portal.Key)
messages := user.bridge.DB.HistorySync.GetMessagesBetween(user.MXID, portal.Key.JID.String(), nil, nil, portal.bridge.Config.Bridge.HistorySync.MessageCount)
log.Debug().Int("message_count", len(messages)).Msg("Got messages to backfill from database")
for i := len(messages) - 1; i >= 0; i-- {
msgEvt, err := user.Client.ParseWebMessage(portal.Key.JID, messages[i])
if err != nil {
log.Warn().Err(err).
Int("msg_index", i).
Str("msg_id", messages[i].GetKey().GetId()).
Uint64("msg_time_seconds", messages[i].GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
continue
}
portal.handleMessage(user, msgEvt, true)
}
if conv != nil {
isUnread := conv.MarkedAsUnread || conv.UnreadCount > 0
isTooOld := user.bridge.Config.Bridge.HistorySync.UnreadHoursThreshold > 0 && conv.LastMessageTimestamp.Before(time.Now().Add(time.Duration(-user.bridge.Config.Bridge.HistorySync.UnreadHoursThreshold)*time.Hour))
shouldMarkAsRead := !isUnread || isTooOld
if shouldMarkAsRead {
user.markSelfReadFull(portal)
}
}
log.Debug().Msg("Backfill complete, deleting leftover messages from database")
user.bridge.DB.HistorySync.DeleteConversation(user.MXID, portal.Key.JID.String())
}
func (user *User) dailyMediaRequestLoop() {
// Calculate when to do the first set of media retry requests
now := time.Now()
@ -175,8 +258,8 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
portal.backfillLock.Lock()
defer portal.backfillLock.Unlock()
if !user.shouldCreatePortalForHistorySync(conv, portal) {
return
if len(portal.MXID) > 0 && !user.bridge.AS.StateStore.IsInRoom(portal.MXID, user.MXID) {
portal.ensureUserInvited(user)
}
backfillState := user.bridge.DB.Backfill.GetBackfillState(user.MXID, &portal.Key)
@ -186,19 +269,17 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
backfillState.SetProcessingBatch(true)
defer backfillState.SetProcessingBatch(false)
var forwardPrevID id.EventID
var timeEnd *time.Time
var isLatestEvents, shouldMarkAsRead, shouldAtomicallyMarkAsRead bool
var forward, shouldMarkAsRead bool
portal.latestEventBackfillLock.Lock()
if req.BackfillType == database.BackfillForward {
// TODO this overrides the TimeStart set when enqueuing the backfill
// maybe the enqueue should instead include the prev event ID
lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key)
forwardPrevID = lastMessage.MXID
start := lastMessage.Timestamp.Add(1 * time.Second)
req.TimeStart = &start
// Sending events at the end of the room (= latest events)
isLatestEvents = true
forward = true
} else {
firstMessage := portal.bridge.DB.Message.GetFirstInChat(portal.Key)
if firstMessage != nil {
@ -207,10 +288,10 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
user.log.Debugfln("Limiting backfill to end at %v", end)
} else {
// Portal is empty -> events are latest
isLatestEvents = true
forward = true
}
}
if !isLatestEvents {
if !forward {
// We'll use normal batch sending, so no need to keep blocking new message processing
portal.latestEventBackfillLock.Unlock()
} else {
@ -221,7 +302,6 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
isUnread := conv.MarkedAsUnread || conv.UnreadCount > 0
isTooOld := user.bridge.Config.Bridge.HistorySync.UnreadHoursThreshold > 0 && conv.LastMessageTimestamp.Before(time.Now().Add(time.Duration(-user.bridge.Config.Bridge.HistorySync.UnreadHoursThreshold)*time.Hour))
shouldMarkAsRead = !isUnread || isTooOld
shouldAtomicallyMarkAsRead = shouldMarkAsRead && user.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry
}
allMsgs := user.bridge.DB.HistorySync.GetMessagesBetween(user.MXID, conv.ConversationID, req.TimeStart, timeEnd, req.MaxTotalEvents)
@ -243,7 +323,7 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
if len(portal.MXID) == 0 {
user.log.Debugln("Creating portal for", portal.Key.JID, "as part of history sync handling")
err := portal.CreateMatrixRoom(user, nil, true, false)
err := portal.CreateMatrixRoom(user, nil, nil, true, false)
if err != nil {
user.log.Errorfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err)
return
@ -280,7 +360,6 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
user.log.Infofln("Backfilling %d messages in %s, %d messages at a time (queue ID: %d)", len(allMsgs), portal.Key.JID, req.MaxBatchEvents, req.QueueID)
toBackfill := allMsgs[0:]
var insertionEventIds []id.EventID
for len(toBackfill) > 0 {
var msgs []*waProto.WebMessageInfo
if len(toBackfill) <= req.MaxBatchEvents || req.MaxBatchEvents < 0 {
@ -294,19 +373,10 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
if len(msgs) > 0 {
time.Sleep(time.Duration(req.BatchDelay) * time.Second)
user.log.Debugfln("Backfilling %d messages in %s (queue ID: %d)", len(msgs), portal.Key.JID, req.QueueID)
resp := portal.backfill(user, msgs, req.BackfillType == database.BackfillForward, isLatestEvents, shouldAtomicallyMarkAsRead, forwardPrevID)
if resp != nil && (resp.BaseInsertionEventID != "" || !isLatestEvents) {
insertionEventIds = append(insertionEventIds, resp.BaseInsertionEventID)
}
portal.backfill(user, msgs, forward, shouldMarkAsRead)
}
}
user.log.Debugfln("Finished backfilling %d messages in %s (queue ID: %d)", len(allMsgs), portal.Key.JID, req.QueueID)
if len(insertionEventIds) > 0 {
portal.sendPostBackfillDummy(
time.Unix(int64(allMsgs[0].GetMessageTimestamp()), 0),
insertionEventIds[0])
}
user.log.Debugfln("Deleting %d history sync messages after backfilling (queue ID: %d)", len(allMsgs), req.QueueID)
err := user.bridge.DB.HistorySync.DeleteMessages(user.MXID, conv.ConversationID, allMsgs)
if err != nil {
user.log.Warnfln("Failed to delete %d history sync messages after backfilling (queue ID: %d): %v", len(allMsgs), req.QueueID, err)
@ -332,38 +402,14 @@ func (user *User) backfillInChunks(req *database.Backfill, conv *database.Histor
backfillState.Upsert()
portal.updateBackfillStatus(backfillState)
}
if isLatestEvents && !shouldAtomicallyMarkAsRead {
if shouldMarkAsRead {
user.markSelfReadFull(portal)
} else if conv.MarkedAsUnread && user.bridge.Config.Bridge.SyncManualMarkedUnread {
user.markUnread(portal, true)
}
}
}
func (user *User) shouldCreatePortalForHistorySync(conv *database.HistorySyncConversation, portal *Portal) bool {
if len(portal.MXID) > 0 {
if !user.bridge.AS.StateStore.IsInRoom(portal.MXID, user.MXID) {
portal.ensureUserInvited(user)
}
// Portal exists, let backfill continue
return true
} else if !user.bridge.Config.Bridge.HistorySync.CreatePortals {
user.log.Debugfln("Not creating portal for %s: creating rooms from history sync is disabled", portal.Key.JID)
return false
} else {
// Portal doesn't exist, but should be created
return true
}
}
func (user *User) handleHistorySync(backfillQueue *BackfillQueue, evt *waProto.HistorySync) {
func (user *User) storeHistorySync(evt *waProto.HistorySync) {
if evt == nil || evt.SyncType == nil {
return
}
log := user.bridge.ZLog.With().
Str("method", "User.handleHistorySync").
Str("method", "User.storeHistorySync").
Str("user_id", user.MXID.String()).
Str("sync_type", evt.GetSyncType().String()).
Uint32("chunk_order", evt.GetChunkOrder()).
@ -401,28 +447,38 @@ func (user *User) handleHistorySync(backfillQueue *BackfillQueue, evt *waProto.H
} else if jid.Server == types.BroadcastServer {
log.Debug().Str("chat_jid", jid.String()).Msg("Skipping broadcast list in history sync")
continue
} else if jid.Server == types.HiddenUserServer {
log.Debug().Str("chat_jid", jid.String()).Msg("Skipping hidden user JID chat in history sync")
continue
}
totalMessageCount += len(conv.GetMessages())
portal := user.GetPortalByJID(jid)
log := log.With().
Str("chat_jid", portal.Key.JID.String()).
Str("chat_jid", jid.String()).
Int("msg_count", len(conv.GetMessages())).
Logger()
historySyncConversation := user.bridge.DB.HistorySync.NewConversationWithValues(
user.MXID,
conv.GetId(),
&portal.Key,
getConversationTimestamp(conv),
conv.GetMuteEndTime(),
conv.GetArchived(),
conv.GetPinned(),
conv.GetDisappearingMode().GetInitiator(),
conv.GetEndOfHistoryTransferType(),
conv.EphemeralExpiration,
conv.GetMarkedAsUnread(),
conv.GetUnreadCount())
historySyncConversation.Upsert()
var portal *Portal
initPortal := func() {
if portal != nil {
return
}
portal = user.GetPortalByJID(jid)
historySyncConversation := user.bridge.DB.HistorySync.NewConversationWithValues(
user.MXID,
conv.GetId(),
&portal.Key,
getConversationTimestamp(conv),
conv.GetMuteEndTime(),
conv.GetArchived(),
conv.GetPinned(),
conv.GetDisappearingMode().GetInitiator(),
conv.GetEndOfHistoryTransferType(),
conv.EphemeralExpiration,
conv.GetMarkedAsUnread(),
conv.GetUnreadCount())
historySyncConversation.Upsert()
}
var minTime, maxTime time.Time
var minTimeIndex, maxTimeIndex int
@ -430,7 +486,7 @@ func (user *User) handleHistorySync(backfillQueue *BackfillQueue, evt *waProto.H
unsupportedTypes := 0
for i, rawMsg := range conv.GetMessages() {
// Don't store messages that will just be skipped.
msgEvt, err := user.Client.ParseWebMessage(portal.Key.JID, rawMsg.GetMessage())
msgEvt, err := user.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil {
log.Warn().Err(err).
Int("msg_index", i).
@ -449,16 +505,12 @@ func (user *User) handleHistorySync(backfillQueue *BackfillQueue, evt *waProto.H
}
msgType := getMessageType(msgEvt.Message)
if msgType == "unknown" || msgType == "ignore" || msgType == "unknown_protocol" {
if msgType == "unknown" || msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") || !containsSupportedMessage(msgEvt.Message) {
unsupportedTypes++
continue
}
// Don't store unsupported messages.
if !containsSupportedMessage(msgEvt.Message) {
unsupportedTypes++
continue
}
initPortal()
message, err := user.bridge.DB.HistorySync.NewMessageWithValues(user.MXID, conv.GetId(), msgEvt.Info.ID, rawMsg)
if err != nil {
@ -487,6 +539,14 @@ func (user *User) handleHistorySync(backfillQueue *BackfillQueue, evt *waProto.H
Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Bool("marked_unread", conv.GetMarkedAsUnread()).
Bool("archived", conv.GetArchived()).
Uint32("pinned", conv.GetPinned()).
Uint64("mute_end", conv.GetMuteEndTime()).
Uint32("unread_count", conv.GetUnreadCount()),
).
Msg("Saved messages from history sync conversation")
}
log.Info().
@ -560,69 +620,20 @@ func (portal *Portal) deterministicEventID(sender types.JID, messageID types.Mes
var (
PortalCreationDummyEvent = event.Type{Type: "fi.mau.dummy.portal_created", Class: event.MessageEventType}
PreBackfillDummyEvent = event.Type{Type: "fi.mau.dummy.pre_backfill", Class: event.MessageEventType}
HistorySyncMarker = event.Type{Type: "org.matrix.msc2716.marker", Class: event.MessageEventType}
BackfillStatusEvent = event.Type{Type: "com.beeper.backfill_status", Class: event.StateEventType}
)
func (portal *Portal) backfill(source *User, messages []*waProto.WebMessageInfo, isForward, isLatest, atomicMarkAsRead bool, prevEventID id.EventID) *mautrix.RespBatchSend {
var req mautrix.ReqBatchSend
func (portal *Portal) backfill(source *User, messages []*waProto.WebMessageInfo, isForward, atomicMarkAsRead bool) *mautrix.RespBeeperBatchSend {
var req mautrix.ReqBeeperBatchSend
var infos []*wrappedInfo
if !isForward {
if portal.FirstEventID != "" || portal.NextBatchID != "" {
req.PrevEventID = portal.FirstEventID
req.BatchID = portal.NextBatchID
} else {
portal.log.Warnfln("Can't backfill %d messages through %s to chat: first event ID not known", len(messages), source.MXID)
return nil
}
} else {
req.PrevEventID = prevEventID
}
req.BeeperNewMessages = isLatest && req.BatchID == ""
req.Forward = isForward
if atomicMarkAsRead {
req.BeeperMarkReadBy = source.MXID
req.MarkReadBy = source.MXID
}
beforeFirstMessageTimestampMillis := (int64(messages[len(messages)-1].GetMessageTimestamp()) * 1000) - 1
req.StateEventsAtStart = make([]*event.Event, 0)
addedMembers := make(map[id.UserID]struct{})
addMember := func(puppet *Puppet) {
if portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry {
// Hungryserv doesn't need state_events_at_start, it can figure out memberships automatically
return
} else if _, alreadyAdded := addedMembers[puppet.MXID]; alreadyAdded {
return
}
mxid := puppet.MXID.String()
content := event.MemberEventContent{
Membership: event.MembershipJoin,
Displayname: puppet.Displayname,
AvatarURL: puppet.AvatarURL.CUString(),
}
inviteContent := content
inviteContent.Membership = event.MembershipInvite
req.StateEventsAtStart = append(req.StateEventsAtStart, &event.Event{
Type: event.StateMember,
Sender: portal.MainIntent().UserID,
StateKey: &mxid,
Timestamp: beforeFirstMessageTimestampMillis,
Content: event.Content{Parsed: &inviteContent},
}, &event.Event{
Type: event.StateMember,
Sender: puppet.MXID,
StateKey: &mxid,
Timestamp: beforeFirstMessageTimestampMillis,
Content: event.Content{Parsed: &content},
})
addedMembers[puppet.MXID] = struct{}{}
}
portal.log.Infofln("Processing history sync with %d messages (forward: %t, latest: %t, prev: %s, batch: %s)", len(messages), isForward, isLatest, req.PrevEventID, req.BatchID)
portal.log.Infofln("Processing history sync with %d messages (forward: %t)", len(messages), isForward)
// The messages are ordered newest to oldest, so iterate them in reverse order.
for i := len(messages) - 1; i >= 0; i-- {
webMsg := messages[i]
@ -653,21 +664,14 @@ func (portal *Portal) backfill(source *User, messages []*waProto.WebMessageInfo,
if puppet == nil {
continue
}
intent := puppet.IntentFor(portal)
if intent.IsCustomPuppet && !portal.bridge.Config.CanDoublePuppetBackfill(puppet.CustomMXID) {
intent = puppet.DefaultIntent()
}
converted := portal.convertMessage(intent, source, &msgEvt.Info, msgEvt.Message, true)
converted := portal.convertMessage(puppet.IntentFor(portal), source, &msgEvt.Info, msgEvt.Message, true)
if converted == nil {
portal.log.Debugfln("Skipping unsupported message %s in backfill", msgEvt.Info.ID)
continue
}
if !intent.IsCustomPuppet && !portal.bridge.StateStore.IsInRoom(portal.MXID, puppet.MXID) {
addMember(puppet)
}
if converted.ReplyTo != nil {
portal.SetReply(converted.Content, converted.ReplyTo, true)
portal.SetReply(msgEvt.Info.ID, converted.Content, converted.ReplyTo, true)
}
err = portal.appendBatchEvents(source, converted, &msgEvt.Info, webMsg, &req.Events, &infos)
if err != nil {
@ -680,15 +684,7 @@ func (portal *Portal) backfill(source *User, messages []*waProto.WebMessageInfo,
return nil
}
if len(req.BatchID) == 0 || isForward {
portal.log.Debugln("Sending a dummy event to avoid forward extremity errors with backfill")
_, err := portal.MainIntent().SendMessageEvent(portal.MXID, PreBackfillDummyEvent, struct{}{})
if err != nil {
portal.log.Warnln("Error sending pre-backfill dummy event:", err)
}
}
resp, err := portal.MainIntent().BatchSend(portal.MXID, &req)
resp, err := portal.MainIntent().BeeperBatchSend(portal.MXID, &req)
if err != nil {
portal.log.Errorln("Error batch sending messages:", err)
return nil
@ -699,12 +695,7 @@ func (portal *Portal) backfill(source *User, messages []*waProto.WebMessageInfo,
return nil
}
// Do the following block in the transaction
{
portal.finishBatch(txn, resp.EventIDs, infos)
portal.NextBatchID = resp.NextBatchID
portal.Update(txn)
}
portal.finishBatch(txn, resp.EventIDs, infos)
err = txn.Commit()
if err != nil {
@ -779,19 +770,16 @@ func (portal *Portal) appendBatchEvents(source *User, converted *ConvertedMessag
*infoArray = append(*infoArray, nil)
}
}
// Sending reactions in the same batch requires deterministic event IDs, so only do it on hungryserv
if portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry {
for _, reaction := range raw.GetReactions() {
reactionEvent, reactionInfo := portal.wrapBatchReaction(source, reaction, mainEvt.ID, info.Timestamp)
if reactionEvent != nil {
*eventsArray = append(*eventsArray, reactionEvent)
*infoArray = append(*infoArray, &wrappedInfo{
MessageInfo: reactionInfo,
SenderMXID: reactionEvent.Sender,
ReactionTarget: info.ID,
Type: database.MsgReaction,
})
}
for _, reaction := range raw.GetReactions() {
reactionEvent, reactionInfo := portal.wrapBatchReaction(source, reaction, mainEvt.ID, info.Timestamp)
if reactionEvent != nil {
*eventsArray = append(*eventsArray, reactionEvent)
*infoArray = append(*infoArray, &wrappedInfo{
MessageInfo: reactionInfo,
SenderMXID: reactionEvent.Sender,
ReactionTarget: info.ID,
Type: database.MsgReaction,
})
}
}
return nil
@ -856,13 +844,8 @@ func (portal *Portal) wrapBatchEvent(info *types.MessageInfo, intent *appservice
return nil, err
}
intent.AddDoublePuppetValue(&wrappedContent)
var eventID id.EventID
if portal.bridge.Config.Homeserver.Software == bridgeconfig.SoftwareHungry {
eventID = portal.deterministicEventID(info.Sender, info.ID, partName)
}
return &event.Event{
ID: eventID,
ID: portal.deterministicEventID(info.Sender, info.ID, partName),
Sender: intent.UserID,
Type: newEventType,
Timestamp: info.Timestamp.UnixMilli(),
@ -877,7 +860,7 @@ func (portal *Portal) finishBatch(txn dbutil.Transaction, eventIDs []id.EventID,
}
eventID := eventIDs[i]
portal.markHandled(txn, nil, info.MessageInfo, eventID, info.SenderMXID, true, false, info.Type, info.Error)
portal.markHandled(txn, nil, info.MessageInfo, eventID, info.SenderMXID, true, false, info.Type, 0, info.Error)
if info.Type == database.MsgReaction {
portal.upsertReaction(txn, nil, info.ReactionTarget, info.Sender, eventID, info.ID)
}
@ -889,33 +872,13 @@ func (portal *Portal) finishBatch(txn dbutil.Transaction, eventIDs []id.EventID,
portal.log.Infofln("Successfully sent %d events", len(eventIDs))
}
func (portal *Portal) sendPostBackfillDummy(lastTimestamp time.Time, insertionEventId id.EventID) {
resp, err := portal.MainIntent().SendMessageEvent(portal.MXID, HistorySyncMarker, map[string]interface{}{
"org.matrix.msc2716.marker.insertion": insertionEventId,
//"m.marker.insertion": insertionEventId,
})
if err != nil {
portal.log.Errorln("Error sending post-backfill dummy event:", err)
return
}
msg := portal.bridge.DB.Message.New()
msg.Chat = portal.Key
msg.MXID = resp.EventID
msg.SenderMXID = portal.MainIntent().UserID
msg.JID = types.MessageID(resp.EventID)
msg.Timestamp = lastTimestamp.Add(1 * time.Second)
msg.Sent = true
msg.Type = database.MsgFake
msg.Insert(nil)
}
func (portal *Portal) updateBackfillStatus(backfillState *database.BackfillState) {
backfillStatus := "backfilling"
if backfillState.BackfillComplete {
backfillStatus = "complete"
}
_, err := portal.MainIntent().SendStateEvent(portal.MXID, BackfillStatusEvent, "", map[string]interface{}{
_, err := portal.bridge.Bot.SendStateEvent(portal.MXID, BackfillStatusEvent, "", map[string]interface{}{
"status": backfillStatus,
"first_timestamp": backfillState.FirstExpectedTimestamp * 1000,
})

39
main.go
View file

@ -19,6 +19,7 @@ package main
import (
_ "embed"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@ -27,19 +28,18 @@ import (
"google.golang.org/protobuf/proto"
"go.mau.fi/util/configupgrade"
"go.mau.fi/whatsmeow"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/configupgrade"
"maunium.net/go/mautrix-whatsapp/config"
"maunium.net/go/mautrix-whatsapp/database"
@ -91,13 +91,18 @@ func (br *WABridge) Init() {
br.EventProcessor.On(TypeMSC3381PollResponse, br.MatrixHandler.HandleMessage)
br.EventProcessor.On(TypeMSC3381V2PollResponse, br.MatrixHandler.HandleMessage)
Segment.log = br.Log.Sub("Segment")
Segment.key = br.Config.SegmentKey
Segment.userID = br.Config.SegmentUserID
if Segment.IsEnabled() {
Segment.log.Infoln("Segment metrics are enabled")
if Segment.userID != "" {
Segment.log.Infoln("Overriding Segment user_id with %v", Segment.userID)
Analytics.log = br.Log.Sub("Analytics")
Analytics.url = (&url.URL{
Scheme: "https",
Host: br.Config.Analytics.Host,
Path: "/v1/track",
}).String()
Analytics.key = br.Config.Analytics.Token
Analytics.userID = br.Config.Analytics.UserID
if Analytics.IsEnabled() {
Analytics.log.Infoln("Analytics metrics are enabled")
if Analytics.userID != "" {
Analytics.log.Infoln("Overriding analytics user_id with %v", Analytics.userID)
}
}
@ -248,20 +253,6 @@ func (br *WABridge) GetConfigPtr() interface{} {
return br.Config
}
const unstableFeatureBatchSending = "org.matrix.msc2716"
func (br *WABridge) CheckFeatures(versions *mautrix.RespVersions) (string, bool) {
if br.Config.Bridge.HistorySync.Backfill {
supported, known := versions.UnstableFeatures[unstableFeatureBatchSending]
if !known {
return "Backfilling is enabled in bridge config, but homeserver does not support MSC2716 batch sending", false
} else if !supported {
return "Backfilling is enabled in bridge config, but MSC2716 batch sending is not enabled on homeserver", false
}
}
return "", true
}
func main() {
br := &WABridge{
usersByMXID: make(map[id.UserID]*User),
@ -277,7 +268,7 @@ func main() {
Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "0.8.5",
Version: "0.10.5",
ProtocolName: "WhatsApp",
BeeperServiceName: "whatsapp",
BeeperNetworkName: "whatsapp",

View file

@ -37,6 +37,7 @@ var (
errUserNotConnected = errors.New("you are not connected to WhatsApp")
errDifferentUser = errors.New("user is not the recipient of this private chat portal")
errUserNotLoggedIn = errors.New("user is not logged in and chat has no relay bot")
errRelaybotNotLoggedIn = errors.New("neither user nor relay bot of chat are logged in")
errMNoticeDisabled = errors.New("bridging m.notice messages is disabled")
errUnexpectedParsedContentType = errors.New("unexpected parsed content type")
errInvalidGeoURI = errors.New("invalid `geo:` URI in message")
@ -55,6 +56,9 @@ var (
errPollMissingQuestion = errors.New("poll message is missing question")
errPollDuplicateOption = errors.New("poll options must be unique")
errGalleryRelay = errors.New("can't send gallery through relay user")
errGalleryCaption = errors.New("can't send gallery with caption")
errEditUnknownTarget = errors.New("unknown edit target message")
errEditUnknownTargetType = errors.New("unsupported edited message type")
errEditDifferentSender = errors.New("can't edit message sent by another user")
@ -108,7 +112,8 @@ func errorToStatusReason(err error) (reason event.MessageStatusReason, status ev
errors.Is(err, errUserNotConnected):
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, true, ""
case errors.Is(err, errUserNotLoggedIn),
errors.Is(err, errDifferentUser):
errors.Is(err, errDifferentUser),
errors.Is(err, errRelaybotNotLoggedIn):
return event.MessageStatusGenericError, event.MessageStatusRetriable, true, false, ""
case errors.Is(err, errMessageDisconnected),
errors.Is(err, errMessageRetryDisconnected):
@ -147,7 +152,7 @@ func (portal *Portal) sendErrorMessage(evt *event.Event, err error, msgType stri
return resp.EventID
}
func (portal *Portal) sendStatusEvent(evtID, lastRetry id.EventID, err error) {
func (portal *Portal) sendStatusEvent(evtID, lastRetry id.EventID, err error, deliveredTo *[]id.UserID) {
if !portal.bridge.Config.Bridge.MessageStatusEvents {
return
}
@ -165,7 +170,8 @@ func (portal *Portal) sendStatusEvent(evtID, lastRetry id.EventID, err error) {
Type: event.RelReference,
EventID: evtID,
},
LastRetry: lastRetry,
DeliveredToUsers: deliveredTo,
LastRetry: lastRetry,
}
if err == nil {
content.Status = event.MessageStatusSuccess
@ -224,12 +230,16 @@ func (portal *Portal) sendMessageMetrics(evt *event.Event, err error, part strin
if sendNotice {
ms.setNoticeID(portal.sendErrorMessage(evt, err, msgType, isCertain, ms.getNoticeID()))
}
portal.sendStatusEvent(origEvtID, evt.ID, err)
portal.sendStatusEvent(origEvtID, evt.ID, err, nil)
} else {
portal.log.Debugfln("Handled Matrix %s %s", msgType, evtDescription)
portal.sendDeliveryReceipt(evt.ID)
portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, ms.getRetryNum())
portal.sendStatusEvent(origEvtID, evt.ID, nil)
var deliveredTo *[]id.UserID
if portal.IsPrivateChat() {
deliveredTo = &[]id.UserID{}
}
portal.sendStatusEvent(origEvtID, evt.ID, nil, deliveredTo)
if prevNotice := ms.popNoticeID(); prevNotice != "" {
_, _ = portal.MainIntent().RedactEvent(portal.MXID, prevNotice, mautrix.ReqRedact{
Reason: "error resolved",

View file

@ -52,6 +52,7 @@ type MetricsHandler struct {
countCollection prometheus.Histogram
disconnections *prometheus.CounterVec
incomingRetryReceipts *prometheus.CounterVec
connectionFailures *prometheus.CounterVec
puppetCount prometheus.Gauge
userCount prometheus.Gauge
messageCount prometheus.Gauge
@ -101,6 +102,10 @@ func NewMetricsHandler(address string, log log.Logger, db *database.Database) *M
Name: "whatsapp_disconnections",
Help: "Number of times a Matrix user has been disconnected from WhatsApp",
}, []string{"user_id"}),
connectionFailures: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "whatsapp_connection_failures",
Help: "Number of times a connection has failed to whatsapp",
}, []string{"reason"}),
incomingRetryReceipts: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "whatsapp_incoming_retry_receipts",
Help: "Number of times a remote WhatsApp user has requested a retry from the bridge. retry_count = 5 is usually the last attempt (and very likely means a failed message)",
@ -173,6 +178,13 @@ func (mh *MetricsHandler) TrackDisconnection(userID id.UserID) {
mh.disconnections.With(prometheus.Labels{"user_id": string(userID)}).Inc()
}
func (mh *MetricsHandler) TrackConnectionFailure(reason string) {
if !mh.running {
return
}
mh.connectionFailures.With(prometheus.Labels{"reason": reason}).Inc()
}
func (mh *MetricsHandler) TrackRetryReceipt(count int, found bool) {
if !mh.running {
return

720
portal.go

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,7 @@ import (
"fmt"
"net"
"net/http"
"strconv"
_ "net/http/pprof"
"strings"
"time"
@ -32,7 +32,6 @@ import (
"github.com/gorilla/websocket"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow"
@ -61,7 +60,6 @@ func (prov *ProvisioningAPI) Init() {
r.HandleFunc("/v1/disconnect", prov.Disconnect).Methods(http.MethodPost)
r.HandleFunc("/v1/reconnect", prov.Reconnect).Methods(http.MethodPost)
r.HandleFunc("/v1/debug/appstate/{name}", prov.SyncAppState).Methods(http.MethodPost)
r.HandleFunc("/v1/debug/retry", prov.SendRetryReceipt).Methods(http.MethodPost)
r.HandleFunc("/v1/contacts", prov.ListContacts).Methods(http.MethodGet)
r.HandleFunc("/v1/groups", prov.ListGroups).Methods(http.MethodGet, http.MethodPost)
r.HandleFunc("/v1/resolve_identifier/{number}", prov.ResolveIdentifier).Methods(http.MethodGet)
@ -74,6 +72,13 @@ func (prov *ProvisioningAPI) Init() {
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost)
prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost)
if prov.bridge.Config.Bridge.Provisioning.DebugEndpoints {
prov.log.Debugln("Enabling debug API at /debug")
r := prov.bridge.AS.Router.PathPrefix("/debug").Subrouter()
r.Use(prov.AuthMiddleware)
r.PathPrefix("/pprof").Handler(http.DefaultServeMux)
}
// Deprecated, just use /disconnect
r.HandleFunc("/v1/delete_connection", prov.Disconnect).Methods(http.MethodPost)
}
@ -191,55 +196,6 @@ func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
}
}
type debugRetryReceiptContent struct {
ID types.MessageID `json:"id"`
From types.JID `json:"from"`
Recipient types.JID `json:"recipient"`
Participant types.JID `json:"participant"`
Timestamp int64 `json:"timestamp"`
Count int `json:"count"`
ForceIncludeIdentity bool `json:"force_include_identity"`
}
func (prov *ProvisioningAPI) SendRetryReceipt(w http.ResponseWriter, r *http.Request) {
var req debugRetryReceiptContent
user := r.Context().Value("user").(*User)
if user == nil || user.Client == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "User is not connected to WhatsApp",
ErrCode: "no session",
})
return
} else if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, Error{
Error: "Failed to parse request JSON",
ErrCode: "bad json",
})
} else {
node := &waBinary.Node{
Attrs: waBinary.Attrs{
"id": string(req.ID),
"from": req.From,
"t": strconv.FormatInt(req.Timestamp, 10),
},
}
if !req.Recipient.IsEmpty() {
node.Attrs["recipient"] = req.Recipient
}
if !req.Participant.IsEmpty() {
node.Attrs["participant"] = req.Participant
}
if req.Count > 0 {
node.Content = []waBinary.Node{{
Tag: "enc",
Attrs: waBinary.Attrs{"count": strconv.Itoa(req.Count)},
}}
}
user.Client.DangerousInternals().SendRetryReceipt(node, req.ForceIncludeIdentity)
}
}
func (prov *ProvisioningAPI) SyncAppState(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user == nil || user.Client == nil {
@ -503,7 +459,7 @@ func (prov *ProvisioningAPI) OpenGroup(w http.ResponseWriter, r *http.Request) {
portal := user.GetPortalByJID(info.JID)
status := http.StatusOK
if len(portal.MXID) == 0 {
err = portal.CreateMatrixRoom(user, info, true, true)
err = portal.CreateMatrixRoom(user, info, nil, true, true)
if err != nil {
jsonResponse(w, http.StatusInternalServerError, Error{
Error: fmt.Sprintf("Failed to create portal: %v", err),
@ -584,7 +540,7 @@ func (prov *ProvisioningAPI) JoinGroup(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
if len(portal.MXID) == 0 {
time.Sleep(500 * time.Millisecond) // Wait for incoming group info to create the portal automatically
err = portal.CreateMatrixRoom(user, info, true, true)
err = portal.CreateMatrixRoom(user, info, nil, true, true)
if err != nil {
jsonResponse(w, http.StatusInternalServerError, Error{
Error: fmt.Sprintf("Failed to create portal: %v", err),
@ -744,7 +700,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
}
}
user.log.Debugln("Started login via provisioning API")
Segment.Track(user.MXID, "$login_start")
Analytics.Track(user.MXID, "$login_start")
for {
select {
@ -753,7 +709,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelSuccess.Event:
jid := user.Client.Store.ID
user.log.Debugln("Successful login as", jid, "via provisioning API")
Segment.Track(user.MXID, "$login_success")
Analytics.Track(user.MXID, "$login_success")
_ = c.WriteJSON(map[string]interface{}{
"success": true,
"jid": jid,
@ -763,7 +719,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelTimeout.Event:
user.log.Debugln("Login via provisioning API timed out")
errCode := "login timed out"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "QR code scan timed out. Please try again.",
ErrCode: errCode,
@ -771,7 +727,7 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelErrUnexpectedEvent.Event:
user.log.Debugln("Login via provisioning API failed due to unexpected event")
errCode := "unexpected event"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Got unexpected event while waiting for QRs, perhaps you're already logged in?",
ErrCode: errCode,
@ -779,14 +735,14 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
case whatsmeow.QRChannelClientOutdated.Event:
user.log.Debugln("Login via provisioning API failed due to outdated client")
errCode := "bridge outdated"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Got client outdated error while waiting for QRs. The bridge must be updated to continue.",
ErrCode: errCode,
})
case whatsmeow.QRChannelScannedWithoutMultidevice.Event:
errCode := "multidevice not enabled"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Please enable the WhatsApp multidevice beta and scan the QR code again.",
ErrCode: errCode,
@ -794,13 +750,13 @@ func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
continue
case "error":
errCode := "fatal error"
Segment.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
Analytics.Track(user.MXID, "$login_failure", map[string]interface{}{"error": errCode})
_ = c.WriteJSON(Error{
Error: "Fatal error while logging in",
ErrCode: errCode,
})
case "code":
Segment.Track(user.MXID, "$qrcode_retrieved")
Analytics.Track(user.MXID, "$qrcode_retrieved")
_ = c.WriteJSON(map[string]interface{}{
"code": evt.Code,
"timeout": int(evt.Timeout.Seconds()),

View file

@ -26,9 +26,9 @@ import (
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/config"
@ -264,7 +264,7 @@ func (puppet *Puppet) UpdateName(contact types.ContactInfo, forcePortalSync bool
}
func (puppet *Puppet) UpdateContactInfo() bool {
if puppet.bridge.Config.Homeserver.Software != bridgeconfig.SoftwareHungry {
if !puppet.bridge.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) {
return false
}
@ -330,6 +330,9 @@ func (puppet *Puppet) updatePortalName() {
}
func (puppet *Puppet) SyncContact(source *User, onlyIfNoName, shouldHavePushName bool, reason string) {
if puppet == nil {
return
}
if onlyIfNoName && len(puppet.Displayname) > 0 && (!shouldHavePushName || puppet.NameQuality > config.NameQualityPhone) {
source.EnqueuePuppetResync(puppet)
return

139
user.go
View file

@ -327,7 +327,7 @@ func (user *User) doPuppetResync() {
user.log.Warnfln("Failed to get group info for %s to do background sync: %v", portal.Key.JID, err)
} else {
user.log.Debugfln("Doing background sync for %s", portal.Key.JID)
portal.UpdateMatrixRoom(user, groupInfo)
portal.UpdateMatrixRoom(user, groupInfo, nil)
}
}
if len(puppetJIDs) == 0 {
@ -496,8 +496,9 @@ func (user *User) createClient(sess *store.Device) {
user.Client = whatsmeow.NewClient(sess, &waLogger{user.log.Sub("Client")})
user.Client.AddEventHandler(user.HandleEvent)
user.Client.SetForceActiveDeliveryReceipts(user.bridge.Config.Bridge.ForceActiveDeliveryReceipts)
user.Client.AutomaticMessageRerequestFromPhone = true
user.Client.GetMessageForRetry = func(requester, to types.JID, id types.MessageID) *waProto.Message {
Segment.Track(user.MXID, "WhatsApp incoming retry (message not found)", map[string]interface{}{
Analytics.Track(user.MXID, "WhatsApp incoming retry (message not found)", map[string]interface{}{
"requester": user.obfuscateJID(requester),
"messageID": id,
})
@ -505,7 +506,7 @@ func (user *User) createClient(sess *store.Device) {
return nil
}
user.Client.PreRetryCallback = func(receipt *events.Receipt, messageID types.MessageID, retryCount int, msg *waProto.Message) bool {
Segment.Track(user.MXID, "WhatsApp incoming retry (accepted)", map[string]interface{}{
Analytics.Track(user.MXID, "WhatsApp incoming retry (accepted)", map[string]interface{}{
"requester": user.obfuscateJID(receipt.Sender),
"messageID": messageID,
"retryCount": retryCount,
@ -611,30 +612,6 @@ func (user *User) IsLoggedIn() bool {
return user.IsConnected() && user.Client.IsLoggedIn()
}
func (user *User) tryAutomaticDoublePuppeting() {
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
return
}
user.log.Debugln("Checking if double puppeting needs to be enabled")
puppet := user.bridge.GetPuppetByJID(user.JID)
if len(puppet.CustomMXID) > 0 {
user.log.Debugln("User already has double-puppeting enabled")
// Custom puppet already enabled
return
}
accessToken, err := puppet.loginWithSharedSecret(user.MXID)
if err != nil {
user.log.Warnln("Failed to login with shared secret:", err)
return
}
err = puppet.SwitchCustomMXID(accessToken, user.MXID)
if err != nil {
puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err)
return
}
user.log.Infoln("Successfully automatically enabled custom puppet")
}
func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface{}) {
if user.bridge.Config.Bridge.DisableBridgeAlerts {
return
@ -654,19 +631,21 @@ func (user *User) handleCallStart(sender types.JID, id, callType string, ts time
return
}
portal := user.GetPortalByJID(sender)
text := "Incoming call"
text := "Incoming call. Use the WhatsApp app to answer."
if callType != "" {
text = fmt.Sprintf("Incoming %s call", callType)
text = fmt.Sprintf("Incoming %s call. Use the WhatsApp app to answer.", callType)
}
portal.messages <- PortalMessage{
fake: &fakeMessage{
Sender: sender,
Text: text,
ID: id,
Time: ts,
Important: true,
portal.events <- &PortalEvent{
Message: &PortalMessage{
fake: &fakeMessage{
Sender: sender,
Text: text,
ID: id,
Time: ts,
Important: true,
},
source: user,
},
source: user,
}
}
@ -676,7 +655,7 @@ const PhoneMinPingInterval = 24 * time.Hour
func (user *User) sendHackyPhonePing() {
user.PhoneLastPinged = time.Now()
msgID := whatsmeow.GenerateMessageID()
msgID := user.Client.GenerateMessageID()
keyIDs := make([]*waProto.AppStateSyncKeyId, 0, 1)
lastKeyID, err := user.GetLastAppStateKeyID()
if lastKeyID != nil {
@ -842,15 +821,18 @@ func (user *User) HandleEvent(event interface{}) {
user.sendMarkdownBridgeAlert("The bridge was started in another location. Use `reconnect` to reconnect this one.")
}
case *events.ConnectFailure:
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: fmt.Sprintf("Unknown connection failure: %s", v.Reason)})
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: fmt.Sprintf("Unknown connection failure: %s (%s)", v.Reason, v.Message)})
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.bridge.Metrics.TrackConnectionFailure(fmt.Sprintf("status-%d", v.Reason))
case *events.ClientOutdated:
user.log.Errorfln("Got a client outdated connect failure. The bridge is likely out of date, please update immediately.")
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateUnknownError, Message: "Connect failure: 405 client outdated"})
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.bridge.Metrics.TrackConnectionFailure("client-outdated")
case *events.TemporaryBan:
user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: v.String()})
user.bridge.Metrics.TrackConnectionState(user.JID, false)
user.bridge.Metrics.TrackConnectionFailure("temporary-ban")
case *events.Disconnected:
// 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
@ -870,6 +852,10 @@ func (user *User) HandleEvent(event interface{}) {
case *events.JoinedGroup:
user.groupListCache = nil
go user.handleGroupCreate(v)
case *events.NewsletterJoin:
go user.handleNewsletterJoin(v)
case *events.NewsletterLeave:
go user.handleNewsletterLeave(v)
case *events.Picture:
go user.handlePictureUpdate(v)
case *events.Receipt:
@ -881,39 +867,50 @@ func (user *User) HandleEvent(event interface{}) {
go user.handleChatPresence(v)
case *events.Message:
portal := user.GetPortalByMessageSource(v.Info.MessageSource)
portal.messages <- PortalMessage{evt: v, source: user}
portal.events <- &PortalEvent{
Message: &PortalMessage{evt: v, source: user},
}
case *events.MediaRetry:
user.phoneSeen(v.Timestamp)
portal := user.GetPortalByJID(v.ChatID)
portal.mediaRetries <- PortalMediaRetry{evt: v, source: user}
portal.events <- &PortalEvent{
MediaRetry: &PortalMediaRetry{evt: v, source: user},
}
case *events.CallOffer:
user.handleCallStart(v.CallCreator, v.CallID, "", v.Timestamp)
case *events.CallOfferNotice:
user.handleCallStart(v.CallCreator, v.CallID, v.Type, v.Timestamp)
case *events.IdentityChange:
puppet := user.bridge.GetPuppetByJID(v.JID)
if puppet == nil {
return
}
portal := user.GetPortalByJID(v.JID)
if len(portal.MXID) > 0 && user.bridge.Config.Bridge.IdentityChangeNotices {
text := fmt.Sprintf("Your security code with %s changed.", puppet.Displayname)
if v.Implicit {
text = fmt.Sprintf("Your security code with %s (device #%d) changed.", puppet.Displayname, v.JID.Device)
}
portal.messages <- PortalMessage{
fake: &fakeMessage{
Sender: v.JID,
Text: text,
ID: strconv.FormatInt(v.Timestamp.Unix(), 10),
Time: v.Timestamp,
Important: false,
portal.events <- &PortalEvent{
Message: &PortalMessage{
fake: &fakeMessage{
Sender: v.JID,
Text: text,
ID: strconv.FormatInt(v.Timestamp.Unix(), 10),
Time: v.Timestamp,
Important: false,
},
source: user,
},
source: user,
}
}
case *events.CallTerminate, *events.CallRelayLatency, *events.CallAccept, *events.UnknownCallEvent:
// ignore
case *events.UndecryptableMessage:
portal := user.GetPortalByMessageSource(v.Info.MessageSource)
portal.messages <- PortalMessage{undecryptable: v, source: user}
portal.events <- &PortalEvent{
Message: &PortalMessage{undecryptable: v, source: user},
}
case *events.HistorySync:
if user.bridge.Config.Bridge.HistorySync.Backfill {
user.historySyncs <- v
@ -1201,13 +1198,13 @@ func (user *User) ResyncGroups(createPortals bool) error {
portal := user.GetPortalByJID(group.JID)
if len(portal.MXID) == 0 {
if createPortals {
err = portal.CreateMatrixRoom(user, group, true, true)
err = portal.CreateMatrixRoom(user, group, nil, true, true)
if err != nil {
return fmt.Errorf("failed to create room for %s: %w", group.JID, err)
}
}
} else {
portal.UpdateMatrixRoom(user, group)
portal.UpdateMatrixRoom(user, group, nil)
}
}
return nil
@ -1217,6 +1214,9 @@ const WATypingTimeout = 15 * time.Second
func (user *User) handleChatPresence(presence *events.ChatPresence) {
puppet := user.bridge.GetPuppetByJID(presence.Sender)
if puppet == nil {
return
}
portal := user.GetPortalByJID(presence.Chat)
if puppet == nil || portal == nil || len(portal.MXID) == 0 {
return
@ -1238,14 +1238,16 @@ func (user *User) handleChatPresence(presence *events.ChatPresence) {
}
func (user *User) handleReceipt(receipt *events.Receipt) {
if receipt.Type != events.ReceiptTypeRead && receipt.Type != events.ReceiptTypeReadSelf {
if receipt.Type != types.ReceiptTypeRead && receipt.Type != types.ReceiptTypeReadSelf && receipt.Type != types.ReceiptTypeDelivered {
return
}
portal := user.GetPortalByMessageSource(receipt.MessageSource)
if portal == nil || len(portal.MXID) == 0 {
return
}
portal.messages <- PortalMessage{receipt: receipt, source: user}
portal.events <- &PortalEvent{
Message: &PortalMessage{receipt: receipt, source: user},
}
}
func (user *User) makeReadMarkerContent(eventID id.EventID, doublePuppet bool) CustomReadMarkers {
@ -1315,12 +1317,12 @@ func (user *User) handleGroupCreate(evt *events.JoinedGroup) {
user.log.Debugfln("Ignoring group create event with key %s", evt.CreateKey)
return
}
err := portal.CreateMatrixRoom(user, &evt.GroupInfo, true, true)
err := portal.CreateMatrixRoom(user, &evt.GroupInfo, nil, true, true)
if err != nil {
user.log.Errorln("Failed to create Matrix room after join notification: %v", err)
}
} else {
portal.UpdateMatrixRoom(user, &evt.GroupInfo)
portal.UpdateMatrixRoom(user, &evt.GroupInfo, nil)
}
}
@ -1337,6 +1339,10 @@ func (user *User) handleGroupUpdate(evt *events.GroupInfo) {
log.Debug().Msg("Ignoring group info update in chat with no portal")
return
}
if evt.Sender != nil && evt.Sender.Server == types.HiddenUserServer {
log.Debug().Str("sender", evt.Sender.String()).Msg("Ignoring group info update from @lid user")
return
}
switch {
case evt.Announce != nil:
log.Debug().Msg("Group announcement mode (message send permission) changed")
@ -1386,6 +1392,25 @@ func (user *User) handleGroupUpdate(evt *events.GroupInfo) {
}
}
func (user *User) handleNewsletterJoin(evt *events.NewsletterJoin) {
portal := user.GetPortalByJID(evt.ID)
if portal.MXID == "" {
err := portal.CreateMatrixRoom(user, nil, &evt.NewsletterMetadata, true, false)
if err != nil {
user.zlog.Err(err).Msg("Failed to create room on newsletter join event")
}
} else {
portal.UpdateMatrixRoom(user, nil, &evt.NewsletterMetadata)
}
}
func (user *User) handleNewsletterLeave(evt *events.NewsletterLeave) {
portal := user.GetPortalByJID(evt.ID)
if portal.MXID != "" {
portal.HandleWhatsAppKick(user, user.JID, []types.JID{user.JID})
}
}
func (user *User) handlePictureUpdate(evt *events.Picture) {
if evt.JID.Server == types.DefaultUserServer {
puppet := user.bridge.GetPuppetByJID(evt.JID)
@ -1415,7 +1440,7 @@ func (user *User) StartPM(jid types.JID, reason string) (*Portal, *Puppet, bool,
return portal, puppet, false, nil
}
}
err := portal.CreateMatrixRoom(user, nil, false, true)
err := portal.CreateMatrixRoom(user, nil, nil, false, true)
return portal, puppet, true, err
}