forked from MirrorHub/mautrix-whatsapp
Merge pull request 'Merge upstream' (#1) from MirrorHub/mautrix-whatsapp:master into master
Reviewed-on: #1
This commit is contained in:
commit
805d84776a
42 changed files with 1386 additions and 1044 deletions
12
.github/workflows/go.yml
vendored
12
.github/workflows/go.yml
vendored
|
@ -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
1
.gitignore
vendored
|
@ -10,3 +10,4 @@
|
|||
*.log
|
||||
|
||||
/mautrix-whatsapp
|
||||
/start
|
||||
|
|
1
.idea/icon.svg
Normal file
1
.idea/icon.svg
Normal 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 |
|
@ -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
|
||||
|
|
68
CHANGELOG.md
68
CHANGELOG.md
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM alpine:3.17
|
||||
FROM alpine:3.19
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
|
@ -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()
|
||||
|
|
156
commands.go
156
commands.go
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"},
|
||||
|
|
256
custompuppet.go
256
custompuppet.go
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
2
database/upgrades/57-message-timestamp-index.sql
Normal file
2
database/upgrades/57-message-timestamp-index.sql
Normal 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);
|
|
@ -20,7 +20,7 @@ import (
|
|||
"embed"
|
||||
"errors"
|
||||
|
||||
"maunium.net/go/mautrix/util/dbutil"
|
||||
"go.mau.fi/util/dbutil"
|
||||
)
|
||||
|
||||
var Table dbutil.UpgradeTable
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
51
go.mod
|
@ -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
124
go.sum
|
@ -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=
|
||||
|
|
389
historysync.go
389
historysync.go
|
@ -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
39
main.go
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
12
metrics.go
12
metrics.go
|
@ -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
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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
139
user.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue