much more cleanup

Signed-off-by: Sumner Evans <sumner@beeper.com>
This commit is contained in:
Sumner Evans 2023-03-19 18:31:16 -06:00
parent e702662919
commit 47656ca0bb
No known key found for this signature in database
GPG Key ID: 8904527AB50022FD
16 changed files with 148 additions and 1282 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea
logs/*
*.session
*.json

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.4.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
@ -9,7 +9,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-beta.5
rev: v1.0.0-rc.1
hooks:
- id: go-imports-repo
args:

View File

@ -69,8 +69,8 @@
* [x] At startup
* [x] When receiving invite
* [x] When receiving message
* [ ] Private chat creation by inviting Matrix puppet of WhatsApp user to new room
* [ ] Option to use own Matrix account for messages sent from WhatsApp mobile/other web clients
* [ ] Private chat creation by inviting Matrix puppet of GroupMe user to new room
* [ ] Option to use own Matrix account for messages sent from GroupMe mobile/other web clients
* [ ] Shared group chat portals
<sup>[1]</sup> Basic feature works. Improvements are TODO.

View File

@ -25,12 +25,14 @@ type Config struct {
*bridgeconfig.BaseConfig `yaml:",inline"`
SegmentKey string `yaml:"segment_key"`
SegmentUserID string `yaml:"segment_user_id"`
Metrics struct {
Enabled bool `yaml:"enabled"`
Listen string `yaml:"listen"`
} `yaml:"metrics"`
// TODO need these?
GroupMe struct {
OSName string `yaml:"os_name"`
BrowserName string `yaml:"browser_name"`

View File

@ -98,7 +98,7 @@ func (br *GMBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
if !found {
if homeserver == br.AS.HomeserverDomain {
homeserverURL = br.AS.HomeserverURL
homeserverURL = ""
} else if br.Config.Bridge.DoublePuppetAllowDiscovery {
resp, err := mautrix.DiscoverClientAPI(homeserver)
if err != nil {
@ -110,14 +110,7 @@ func (br *GMBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
}
}
client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
if err != nil {
return nil, err
}
client.Logger = br.AS.Log.Sub(mxid.String())
client.Client = br.AS.HTTPClient
client.DefaultHTTPRetries = br.AS.DefaultHTTPRetries
return client, nil
return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
}
func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {

View File

@ -1,4 +1,4 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// mautrix-groupme - A Matrix-GroupMe puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify

View File

@ -9,7 +9,7 @@ homeserver:
# Standard Matrix homeservers like Synapse, Dendrite and Conduit should just use "standard" here.
software: standard
# The URL to push real-time bridge status to.
# If set, the bridge will make POST requests to this URL whenever a user's whatsapp connection state changes.
# If set, the bridge will make POST requests to this URL whenever a user's GroupMe connection state changes.
# The bridge will use the appservice as_token to authorize requests.
status_endpoint: null
# Endpoint for reporting per-message status.
@ -29,10 +29,11 @@ appservice:
# Database config.
database:
# The database type. "sqlite3" and "postgres" are supported.
# The database type. "sqlite3-fk-wal" and "postgres" are supported.
type: postgres
# The database URI.
# SQLite: File name is enough. https://github.com/mattn/go-sqlite3#connection-string
# SQLite: A raw file path is supported, but `file:<path>?_txlock=immediate` is recommended.
# https://github.com/mattn/go-sqlite3#connection-string
# Postgres: Connection string. For example, postgres://user:password@host/database?sslmode=disable
# To connect via Unix socket, use something like postgres:///dbname?host=/var/run/postgresql
uri: postgres://user:password@host/database?sslmode=disable
@ -53,7 +54,11 @@ appservice:
# Display name and avatar for bot. Set to "remove" to remove display name/avatar, leave empty
# to leave display name/avatar as-is.
displayname: GroupMe bridge bot
avatar: mxc://malhotra.cc/YTWNAdhgJhYOPsKIxyfFZsrA
avatar: mxc://nevarro.space/eoAJPcSuTEvffoNycrXjvsmj
# Whether or not to receive ephemeral events via appservice transactions.
# Requires MSC2409 support (i.e. Synapse 1.22+).
ephemeral_events: true
# Authentication tokens for AS <-> HS communication. Autogenerated; do not modify.
as_token: "This value is generated when generating the registration"
@ -80,8 +85,8 @@ groupme:
# Bridge config
bridge:
# Localpart template of MXIDs for WhatsApp users.
# {{.}} is replaced with the phone number of the WhatsApp user.
# Localpart template of MXIDs for GroupMe users.
# {{.}} is replaced with the phone number of the GroupMe user.
username_template: groupme_{{.}}
# Displayname template for GroupMe users.
# {{call .UserID.String}} - the number GroupMe assigns to the user
@ -91,7 +96,7 @@ bridge:
# Should the bridge create a space for each logged-in user and add bridged rooms to it?
# Users who logged in before turning this on should run `!wa sync space` to create and fill the space for the first time.
personal_filtering_spaces: false
# Should the bridge send a read receipt from the bridge bot when a message has been sent to WhatsApp?
# Should the bridge send a read receipt from the bridge bot when a message has been sent to GroupMe?
delivery_receipts: false
# Whether the bridge should send the message status as a custom com.beeper.message_send_status event.
message_status_events: false
@ -158,7 +163,7 @@ bridge:
# manually.
login_shared_secret: null
# Whether or not to invite own WhatsApp user's Matrix puppet into private
# Whether or not to invite own GroupMe user's Matrix puppet into private
# chat portals when backfilling if needed.
# This always uses the default puppet instead of custom puppets due to
# rate limits and timestamp massaging.
@ -172,11 +177,11 @@ bridge:
# except if the config file is not writable.
resend_bridge_info: false
# Whether or not thumbnails from WhatsApp should be sent.
# Whether or not thumbnails from GroupMe should be sent.
# They're disabled by default due to very low resolution.
whatsapp_thumbnail: false
groupme_thumbnail: false
# Allow invite permission for user. User can invite any bots to room with whatsapp
# Allow invite permission for user. User can invite any bots to room with GroupMe
# users (private chat and groups)
allow_user_invite: false
@ -222,7 +227,7 @@ bridge:
# verified - Require manual per-device verification
# (currently only possible by modifying the `trust` column in the `crypto_device` database table).
verification_levels:
# Minimum level for which the bridge should send keys to when bridging messages from WhatsApp to Matrix.
# Minimum level for which the bridge should send keys to when bridging messages from GroupMe to Matrix.
receive: unverified
# Minimum level that the bridge should accept for incoming Matrix messages.
send: unverified
@ -256,7 +261,7 @@ bridge:
# Permissions for using the bridge.
# Permitted values:
# relaybot - Talk through the relaybot (if enabled), no access otherwise
# user - Access to use the bridge to chat with a WhatsApp account.
# user - Access to use the bridge to chat with a GroupMe account.
# admin - User level and some additional administration tools
# Permitted keys:
# * - All Matrix users
@ -267,18 +272,15 @@ bridge:
"example.com": user
"@admin:example.com": admin
# Logging config.
# Logging config. See https://github.com/tulir/zeroconfig for details.
logging:
# The directory for log files. Will be created if not found.
directory: ./logs
# Available variables: .Date for the file date and .Index for different log files on the same day.
file_name_format: "{{.Date}}-{{.Index}}.log"
# Date format for file names in the Go time format: https://golang.org/pkg/time/#pkg-constants
file_date_format: 2006-01-02
# Log file permissions.
file_mode: 0600
# Timestamp format for log entries in the Go time format.
timestamp_format: Jan _2, 2006 15:04:05
# Minimum severity for log messages.
# Options: debug, info, warn, error, fatal
print_level: debug
min_level: debug
writers:
- type: stdout
format: pretty-colored
- type: file
format: json
filename: ./logs/mautrix-groupme.log
max_size: 100
max_backups: 10
compress: true

View File

@ -18,127 +18,33 @@ package main
import (
"fmt"
"regexp"
"strings"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"github.com/beeper/groupme-lib"
"maunium.net/go/mautrix/util/variationselector"
)
var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)")
var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)")
var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```")
const formatterContextAllowedMentionsKey = "com.beeper.groupme.allowed_mentions"
const mentionedGMIDsContextKey = "net.maunium.groupme.mentioned_gmids"
type Formatter struct {
bridge *GMBridge
matrixHTMLParser *format.HTMLParser
waReplString map[*regexp.Regexp]string
waReplFunc map[*regexp.Regexp]func(string) string
waReplFuncText map[*regexp.Regexp]func(string) string
func (br *GMBridge) pillConverter(displayname, mxid, eventID string, ctx format.Context) string {
// GroupMe only supports user mentions.
if len(mxid) == 0 || mxid[0] != '@' {
return displayname
}
func NewFormatter(bridge *GMBridge) *Formatter {
formatter := &Formatter{
bridge: bridge,
matrixHTMLParser: &format.HTMLParser{
return fmt.Sprintf("@%s", displayname)
}
var matrixHTMLParser = &format.HTMLParser{
TabsToSpaces: 4,
Newline: "\n",
HorizontalLine: "\n---\n",
}
PillConverter: func(displayname, mxid, eventID string, ctx format.Context) string {
if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
if puppet != nil {
gmids, ok := ctx[mentionedGMIDsContextKey].([]groupme.ID)
if !ok {
ctx[mentionedGMIDsContextKey] = []groupme.ID{puppet.GMID}
func (portal *Portal) parseMatrixHTML(content *event.MessageEventContent) string {
if content.Format == event.FormatHTML && len(content.FormattedBody) > 0 {
return variationselector.FullyQualify(matrixHTMLParser.Parse(content.FormattedBody, format.NewContext()))
} else {
ctx[mentionedGMIDsContextKey] = append(gmids, puppet.GMID)
}
return "@" + puppet.PhoneNumber()
return variationselector.FullyQualify(content.Body)
}
}
return mxid
},
BoldConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("*%s*", text)
},
ItalicConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("_%s_", text)
},
StrikethroughConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("~%s~", text)
},
MonospaceConverter: func(text string, _ format.Context) string {
return fmt.Sprintf("```%s```", text)
},
MonospaceBlockConverter: func(text, language string, _ format.Context) string {
return fmt.Sprintf("```%s```", text)
},
},
waReplString: map[*regexp.Regexp]string{
italicRegex: "$1<em>$2</em>$3",
boldRegex: "$1<strong>$2</strong>$3",
strikethroughRegex: "$1<del>$2</del>$3",
},
}
formatter.waReplFunc = map[*regexp.Regexp]func(string) string{
codeBlockRegex: func(str string) string {
str = str[3 : len(str)-3]
if strings.ContainsRune(str, '\n') {
return fmt.Sprintf("<pre><code>%s</code></pre>", str)
}
return fmt.Sprintf("<code>%s</code>", str)
},
}
formatter.waReplFuncText = map[*regexp.Regexp]func(string) string{}
return formatter
}
//func (formatter *Formatter) getMatrixInfoByJID(jid groupme.ID) (mxid id.UserID, displayname string) {
// if user := formatter.bridge.GetUserByJID(jid); user != nil {
// mxid = user.MXID
// displayname = string(user.MXID)
// } else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil {
// mxid = puppet.MXID
// displayname = puppet.Displayname
// }
// return
//}
//func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent, mentionedJIDs []groupme.ID) {
// output := html.EscapeString(content.Body)
// for regex, replacement := range formatter.waReplString {
// output = regex.ReplaceAllString(output, replacement)
// }
// for regex, replacer := range formatter.waReplFunc {
// output = regex.ReplaceAllStringFunc(output, replacer)
// }
// for _, jid := range mentionedJIDs {
// mxid, displayname := formatter.getMatrixInfoByJID(jid)
// number := "@" + strings.Replace(jid, whatsappExt.NewUserSuffix, "", 1)
// output = strings.Replace(output, number, fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, mxid, displayname), -1)
// content.Body = strings.Replace(content.Body, number, displayname, -1)
// }
// if output != content.Body {
// output = strings.Replace(output, "\n", "<br/>", -1)
// content.FormattedBody = output
// content.Format = event.FormatHTML
// for regex, replacer := range formatter.waReplFuncText {
// content.Body = regex.ReplaceAllStringFunc(content.Body, replacer)
// }
// }
//}
func (formatter *Formatter) ParseMatrix(html string) (string, []groupme.ID) {
ctx := make(format.Context)
result := formatter.matrixHTMLParser.Parse(html, ctx)
mentionedJIDs, _ := ctx[mentionedGMIDsContextKey].([]groupme.ID)
return result, mentionedJIDs
}

25
go.mod
View File

@ -3,42 +3,43 @@ module github.com/beeper/groupme
go 1.19
require (
github.com/Rhymen/go-whatsapp v0.1.1
github.com/beeper/groupme-lib v0.2.1-0.20221021205945-8f23e04eea71
github.com/gabriel-vasile/mimetype v1.1.2
github.com/gorilla/websocket v1.5.0
github.com/karmanyaahm/wray v0.0.0-20210303233435-756d58657c14
github.com/lib/pq v1.10.7
github.com/mattn/go-sqlite3 v1.14.15
github.com/mattn/go-sqlite3 v1.14.16
github.com/prometheus/client_golang v1.9.0
maunium.net/go/maulogger/v2 v2.3.2
maunium.net/go/mautrix v0.12.3-0.20221020190005-d0c13d2f04a1
maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.15.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.15.0 // indirect
github.com/prometheus/procfs v0.6.0 // indirect
github.com/rs/zerolog v1.28.0 // indirect
github.com/tidwall/gjson v1.14.3 // indirect
github.com/rs/zerolog v1.29.0 // indirect
github.com/tidwall/gjson v1.14.4 // 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.2 // indirect
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a // indirect
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
go.mau.fi/zeroconfig v0.1.2 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/net v0.6.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.25.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
)

61
go.sum
View File

@ -1,16 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp v0.1.1 h1:OK+bCugQcr2YjyYKeDzULqCtM50TPUFM6LvQtszKfcw=
github.com/Rhymen/go-whatsapp v0.1.1/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
@ -48,6 +40,7 @@ github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:z
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 h1:rtAn27wIbmOGUs7RIbVgPEjb31ehTVniDwPGXyMxm5U=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@ -89,7 +82,6 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@ -124,8 +116,6 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@ -184,17 +174,15 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
@ -241,7 +229,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -279,8 +266,8 @@ github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqn
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
@ -289,7 +276,6 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@ -307,10 +293,10 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/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/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=
@ -321,10 +307,12 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
github.com/yuin/goldmark v1.5.2/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=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@ -337,14 +325,13 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a h1:NmSIgad6KjE6VvHciPZuNRTKxGhlPfD6OA87W/PLkqg=
golang.org/x/crypto v0.0.0-20221012134737-56aed061732a/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -370,8 +357,8 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -390,7 +377,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -405,8 +391,8 @@ golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -434,7 +420,6 @@ google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
@ -469,6 +454,8 @@ gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qS
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
@ -487,9 +474,9 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
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.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.12.3-0.20221020190005-d0c13d2f04a1 h1:daraaP+GcSrFLgVckFpp+ciVrtQeG5s2w3Fi8AInaj8=
maunium.net/go/mautrix v0.12.3-0.20221020190005-d0c13d2f04a1/go.mod h1:bCw45Qx/m9qsz7eazmbe7Rzq5ZbTPzwRE1UgX2S9DXs=
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.0 h1:gkK9HXc1SSPwY7qOAqchzj2xxYqiOYeee8lr28A2g/o=
maunium.net/go/mautrix v0.15.0/go.mod h1:1v8QVDd7q/eJ+eg4sgeOSEafBAFhkt4ab2i97M3IkNQ=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

67
main.go
View File

@ -20,6 +20,7 @@ import (
_ "embed"
"sync"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/bridge/status"
@ -48,7 +49,6 @@ type GMBridge struct {
Config *config.Config
DB *database.Database
Provisioning *ProvisioningAPI
Formatter *Formatter
Metrics *MetricsHandler
usersByMXID map[id.UserID]*User
@ -70,10 +70,16 @@ func (br *GMBridge) Init() {
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
br.RegisterCommands()
matrixHTMLParser.PillConverter = br.pillConverter
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)
}
}
br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
@ -83,47 +89,18 @@ func (br *GMBridge) Init() {
br.Provisioning = &ProvisioningAPI{bridge: br}
}
br.Formatter = NewFormatter(br)
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)
br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent
}
func (bridge *GMBridge) Start() {
if bridge.Provisioning != nil {
bridge.Log.Debugln("Initializing provisioning API")
bridge.Provisioning.Init()
func (br *GMBridge) Start() {
if br.Provisioning != nil {
br.Log.Debugln("Initializing provisioning API")
br.Provisioning.Init()
}
go bridge.StartUsers()
if bridge.Config.Metrics.Enabled {
go bridge.Metrics.Start()
}
}
func (bridge *GMBridge) UpdateBotProfile() {
bridge.Log.Debugln("Updating bot profile")
botConfig := bridge.Config.AppService.Bot
var err error
var mxc id.ContentURI
if botConfig.Avatar == "remove" {
err = bridge.Bot.SetAvatarURL(mxc)
} else if len(botConfig.Avatar) > 0 {
mxc, err = id.ParseContentURI(botConfig.Avatar)
if err == nil {
err = bridge.Bot.SetAvatarURL(mxc)
}
}
if err != nil {
bridge.Log.Warnln("Failed to update bot avatar:", err)
}
if botConfig.Displayname == "remove" {
err = bridge.Bot.SetDisplayName("")
} else if len(botConfig.Avatar) > 0 {
err = bridge.Bot.SetDisplayName(botConfig.Displayname)
}
if err != nil {
bridge.Log.Warnln("Failed to update bot displayname:", err)
go br.StartUsers()
if br.Config.Metrics.Enabled {
go br.Metrics.Start()
}
}
@ -131,7 +108,7 @@ func (br *GMBridge) StartUsers() {
br.Log.Debugln("Starting users")
foundAnySessions := false
for _, user := range br.GetAllUsers() {
if user.GMID.String() != "" {
if user.GMID.Valid() {
foundAnySessions = true
}
go user.Connect()
@ -174,6 +151,20 @@ func (br *GMBridge) GetConfigPtr() interface{} {
return br.Config
}
const unstableFeatureBatchSending = "org.matrix.msc2716"
func (br *GMBridge) 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 := &GMBridge{
usersByMXID: make(map[id.UserID]*User),

View File

@ -97,7 +97,6 @@ func (portal *Portal) sendStatusEvent(evtID, lastRetry id.EventID, err error) {
content.Reason, content.Status, _, _, content.Message = errorToStatusReason(err)
content.Error = err.Error()
}
content.FillLegacyBooleans()
_, err = intent.SendMessageEvent(portal.MXID, event.BeeperMessageStatus, &content)
if err != nil {
portal.log.Warnln("Failed to send message status event:", err)

302
portal.go
View File

@ -37,7 +37,6 @@ import (
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/crypto/attachment"
"github.com/Rhymen/go-whatsapp"
"github.com/gabriel-vasile/mimetype"
"github.com/beeper/groupme-lib"
@ -385,7 +384,7 @@ func (portal *Portal) SyncParticipants(metadata *groupme.Group) {
if !shouldBePresent {
_, err := portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{
UserID: member,
Reason: "User had left this WhatsApp chat",
Reason: "User had left this GroupMe chat",
})
if err != nil {
portal.log.Warnfln("Failed to kick user %s who had left: %v", member, err)
@ -1158,7 +1157,6 @@ func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message)
sendText = sendText && text
}
// portal.bridge.Formatter.ParseWhatsApp(content, message.ContextInfo.MentionedJID)
// portal.SetReply(content, message.ContextInfo)
//TODO: mentions
content := &event.MessageEventContent{
@ -1262,108 +1260,6 @@ func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message)
// return
// }
func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) {
// intent := portal.startHandling(source, message.Info)
// if intent == nil {
// return
// }
//
// url := message.Url
// if len(url) == 0 {
// url = fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude)
// }
// name := message.Name
// if len(name) == 0 {
// latChar := 'N'
// if message.DegreesLatitude < 0 {
// latChar = 'S'
// }
// longChar := 'E'
// if message.DegreesLongitude < 0 {
// longChar = 'W'
// }
// name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(message.DegreesLatitude), latChar, math.Abs(message.DegreesLongitude), longChar)
// }
//
// content := &event.MessageEventContent{
// MsgType: event.MsgLocation,
// Body: fmt.Sprintf("Location: %s\n%s\n%s", name, message.Address, url),
// Format: event.FormatHTML,
// FormattedBody: fmt.Sprintf("Location: <a href='%s'>%s</a><br>%s", url, name, message.Address),
// GeoURI: fmt.Sprintf("geo:%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude),
// }
//
// if len(message.JpegThumbnail) > 0 {
// thumbnailMime := http.DetectContentType(message.JpegThumbnail)
// uploadedThumbnail, _ := intent.UploadBytes(message.JpegThumbnail, thumbnailMime)
// if uploadedThumbnail != nil {
// cfg, _, _ := image.DecodeConfig(bytes.NewReader(message.JpegThumbnail))
// content.Info = &event.FileInfo{
// ThumbnailInfo: &event.FileInfo{
// Size: len(message.JpegThumbnail),
// Width: cfg.Width,
// Height: cfg.Height,
// MimeType: thumbnailMime,
// },
// ThumbnailURL: uploadedThumbnail.ContentURI.CUString(),
// }
// }
// }
//
// portal.SetReply(content, message.ContextInfo)
//
// _, _ = intent.UserTyping(portal.MXID, false, 0)
// resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
// if err != nil {
// portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
// return
// }
// portal.finishHandling(source, message.Info.Source, resp.EventID)
//}
//func (portal *Portal) HandleContactMessage(source *User, message whatsapp.ContactMessage) {
// intent := portal.startHandling(source, message.Info)
// if intent == nil {
// return
// }
//
// fileName := fmt.Sprintf("%s.vcf", message.DisplayName)
// data := []byte(message.Vcard)
// mimeType := "text/vcard"
// data, uploadMimeType, file := portal.encryptFile(data, mimeType)
//
// uploadResp, err := intent.UploadBytesWithName(data, uploadMimeType, fileName)
// if err != nil {
// portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err)
// return
// }
//
// content := &event.MessageEventContent{
// Body: fileName,
// MsgType: event.MsgFile,
// File: file,
// Info: &event.FileInfo{
// MimeType: mimeType,
// Size: len(message.Vcard),
// },
// }
// if content.File != nil {
// content.File.URL = uploadResp.ContentURI.CUString()
// } else {
// content.URL = uploadResp.ContentURI.CUString()
// }
//
// portal.SetReply(content, message.ContextInfo)
//
// _, _ = intent.UserTyping(portal.MXID, false, 0)
// resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
// if err != nil {
// portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
// return
// }
// portal.finishHandling(source, message.Info.Source, resp.EventID)
}
func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, message groupme.Message, bridgeErr error) {
portal.log.Errorfln("Failed to bridge media for %s: %v", message.UserID.String(), bridgeErr)
resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{
@ -1467,93 +1363,13 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) ([]*g
case event.MsgText, event.MsgEmote, event.MsgNotice:
text := content.Body
if content.Format == event.FormatHTML {
text, _ = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
//TODO mentions
text = portal.parseMatrixHTML(content)
}
if content.MsgType == event.MsgEmote && !relaybotFormatted {
text = "/me " + text
}
info.Text = text
// if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil {
// info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{
// Text: &text,
// ContextInfo: ctxInfo,
// }
// }
//else {
// info.Message.Conversation = &text
// }
// case event.MsgImage:
// media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage)
// if media == nil {
// return nil, sender
// }
// ctxInfo.MentionedJid = media.MentionedJIDs
// info.Message.ImageMessage = &waProto.ImageMessage{
// ContextInfo: ctxInfo,
// Caption: &media.Caption,
// JpegThumbnail: media.Thumbnail,
// Url: &media.URL,
// MediaKey: media.MediaKey,
// Mimetype: &content.GetInfo().MimeType,
// FileEncSha256: media.FileEncSHA256,
// FileSha256: media.FileSHA256,
// FileLength: &media.FileLength,
// }
// case event.MsgVideo:
// gifPlayback := content.GetInfo().MimeType == "image/gif"
// media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo)
// if media == nil {
// return nil, sender
// }
// duration := uint32(content.GetInfo().Duration)
// ctxInfo.MentionedJid = media.MentionedJIDs
// info.Message.VideoMessage = &waProto.VideoMessage{
// ContextInfo: ctxInfo,
// Caption: &media.Caption,
// JpegThumbnail: media.Thumbnail,
// Url: &media.URL,
// MediaKey: media.MediaKey,
// Mimetype: &content.GetInfo().MimeType,
// GifPlayback: &gifPlayback,
// Seconds: &duration,
// FileEncSha256: media.FileEncSHA256,
// FileSha256: media.FileSHA256,
// FileLength: &media.FileLength,
// }
// case event.MsgAudio:
// media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio)
// if media == nil {
// return nil, sender
// }
// duration := uint32(content.GetInfo().Duration)
// info.Message.AudioMessage = &waProto.AudioMessage{
// ContextInfo: ctxInfo,
// Url: &media.URL,
// MediaKey: media.MediaKey,
// Mimetype: &content.GetInfo().MimeType,
// Seconds: &duration,
// FileEncSha256: media.FileEncSHA256,
// FileSha256: media.FileSHA256,
// FileLength: &media.FileLength,
// }
// case event.MsgFile:
// media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument)
// if media == nil {
// return nil, sender
// }
// info.Message.DocumentMessage = &waProto.DocumentMessage{
// ContextInfo: ctxInfo,
// Url: &media.URL,
// Title: &content.Body,
// FileName: &content.Body,
// MediaKey: media.MediaKey,
// Mimetype: &content.GetInfo().MimeType,
// FileEncSha256: media.FileEncSHA256,
// FileSha256: media.FileSHA256,
// FileLength: &media.FileLength,
// }
default:
portal.log.Debugln("Unhandled Matrix event %s: unknown msgtype %s", evt.ID, content.MsgType)
return nil, sender
@ -1562,13 +1378,6 @@ func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) ([]*g
}
func (portal *Portal) wasMessageSent(sender *User, id string) bool {
// _, err := sender.Conn.LoadMessagesAfter(portal.Key.JID, id, true, 0)
// if err != nil {
// if err != whatsapp.ErrServerRespondedWith404 {
// portal.log.Warnfln("Failed to check if message was bridged without response: %v", err)
// }
// return false
// }
return true
}
@ -1620,98 +1429,9 @@ func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *groupme.Mess
}
}
return m, nil
// errChan := make(chan error, 1)
// go sender.Conn.SendRaw(info, errChan)
// var err error
// var errorEventID id.EventID
// select {
// case err = <-errChan:
// var statusResp whatsapp.StatusResponse
// if !isRetry && errors.As(err, &statusResp) && statusResp.Status == 599 {
// portal.log.Debugfln("599 status response sending %s to WhatsApp (%+v), retrying...", evt.ID, statusResp)
// errorEventID = portal.sendErrorMessage(fmt.Sprintf("%v. The bridge will retry in 5 seconds.", err))
// time.Sleep(5 * time.Second)
// portal.sendRaw(sender, evt, info, true)
// }
// case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second):
// if portal.bridge.Config.Bridge.FetchMessageOnTimeout && portal.wasMessageSent(sender, info.Key.GetId()) {
// portal.log.Debugln("Matrix event %s was bridged, but response didn't arrive within timeout")
// portal.sendDeliveryReceipt(evt.ID)
// } else {
// portal.log.Warnfln("Response when bridging Matrix event %s is taking long to arrive", evt.ID)
// errorEventID = portal.sendErrorMessage(timeout.Error())
// }
// err = <-errChan
// }
// if err != nil {
// portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
// var statusResp whatsapp.StatusResponse
// if errors.As(err, &statusResp) && statusResp.Status == 599 {
// portal.log.Debugfln("599 status response data: %+v", statusResp)
// }
// portal.sendErrorMessage(err.Error())
// } else {
// portal.log.Debugfln("Handled Matrix event %s", evt.ID)
// portal.sendDeliveryReceipt(evt.ID)
// }
// if errorEventID != "" {
// _, err = portal.MainIntent().RedactEvent(portal.MXID, errorEventID)
// if err != nil {
// portal.log.Warnfln("Failed to redact timeout warning message %s: %v", errorEventID, err)
// }
// }
}
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
// if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
// return
// }
// msg := portal.bridge.DB.Message.GetByMXID(evt.Redacts)
// if msg == nil || msg.Sender != sender.JID {
// return
// }
// ts := uint64(evt.Timestamp / 1000)
// status := waProto.WebMessageInfo_PENDING
// protoMsgType := waProto.ProtocolMessage_REVOKE
// fromMe := true
// info := &waProto.WebMessageInfo{
// Key: &waProto.MessageKey{
// FromMe: &fromMe,
// Id: makeMessageID(),
// RemoteJid: &portal.Key.JID,
// },
// MessageTimestamp: &ts,
// Message: &waProto.Message{
// ProtocolMessage: &waProto.ProtocolMessage{
// Type: &protoMsgType,
// Key: &waProto.MessageKey{
// FromMe: &fromMe,
// Id: &msg.JID,
// RemoteJid: &portal.Key.JID,
// },
// },
// },
// Status: &status,
// }
// errChan := make(chan error, 1)
// go sender.Conn.SendRaw(info, errChan)
// var err error
// select {
// case err = <-errChan:
// case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second):
// portal.log.Warnfln("Response when bridging Matrix redaction %s is taking long to arrive", evt.ID)
// err = <-errChan
// }
// if err != nil {
// portal.log.Errorfln("Error handling Matrix redaction %s: %v", evt.ID, err)
// } else {
// portal.log.Debugln("Handled Matrix redaction %s of %s", evt.ID, evt.Redacts)
// portal.sendDeliveryReceipt(evt.ID)
// }
}
func (portal *Portal) Delete() {
@ -1811,25 +1531,7 @@ func (portal *Portal) HandleMatrixLeave(sender *User) {
}
func (portal *Portal) HandleMatrixKick(sender *User, evt *event.Event) {
// puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
// if puppet != nil {
// resp, err := sender.Conn.RemoveMember(portal.Key.JID, []string{puppet.JID})
// if err != nil {
// portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err)
// return
// }
// portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
// }
}
func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) {
// puppet := portal.bridge.GetPuppetByMXID(id.UserID(evt.GetStateKey()))
// if puppet != nil {
// resp, err := sender.Conn.AddMember(portal.Key.JID, []string{puppet.JID})
// if err != nil {
// portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err)
// return
// }
// portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp)
// }
}

View File

@ -21,7 +21,6 @@ import (
"net/http"
"strings"
"github.com/gorilla/websocket"
log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/id"
@ -37,13 +36,6 @@ func (prov *ProvisioningAPI) Init() {
prov.log.Debugln("Enabling provisioning API at", prov.bridge.Config.Bridge.Provisioning.Prefix)
r := prov.bridge.AS.Router.PathPrefix(prov.bridge.Config.Bridge.Provisioning.Prefix).Subrouter()
r.Use(prov.AuthMiddleware)
r.HandleFunc("/ping", prov.Ping).Methods(http.MethodGet)
r.HandleFunc("/login", prov.Login)
r.HandleFunc("/logout", prov.Logout).Methods(http.MethodPost)
r.HandleFunc("/delete_session", prov.DeleteSession).Methods(http.MethodPost)
r.HandleFunc("/delete_connection", prov.DeleteConnection).Methods(http.MethodPost)
r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost)
r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost)
}
func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler {
@ -53,8 +45,8 @@ func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler {
authParts := strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",")
for _, part := range authParts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "net.maunium.whatsapp.auth-") {
auth = part[len("net.maunium.whatsapp.auth-"):]
if strings.HasPrefix(part, "com.beeper.groupme.auth-") {
auth = part[len("com.beeper.groupme.auth-"):]
break
}
}
@ -85,326 +77,8 @@ type Response struct {
Status string `json:"status"`
}
func (prov *ProvisioningAPI) DeleteSession(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value("user").(*User)
// if user.Session == nil && user.Conn == nil {
// jsonResponse(w, http.StatusNotFound, Error{
// Error: "Nothing to purge: no session information stored and no active connection.",
// ErrCode: "no session",
// })
// return
// }
// user.SetSession(nil)
// if user.Conn != nil {
// _, _ = user.Conn.Disconnect()
// user.Conn.RemoveHandlers()
// user.Conn = nil
// user.bridge.Metrics.TrackConnectionState(user.JID, false)
// }
// jsonResponse(w, http.StatusOK, Response{true, "Session information purged"})
}
func (prov *ProvisioningAPI) DeleteConnection(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value("user").(*User)
// if user.Conn == nil {
// jsonResponse(w, http.StatusNotFound, Error{
// Error: "You don't have a WhatsApp connection.",
// ErrCode: "not connected",
// })
// return
// }
// sess, err := user.Conn.Disconnect()
// if err == nil && len(sess.Wid) > 0 {
// user.SetSession(&sess)
// }
// user.Conn.RemoveHandlers()
// user.Conn = nil
// user.bridge.Metrics.TrackConnectionState(user.JID, false)
// jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp and connection deleted"})
}
func (prov *ProvisioningAPI) Disconnect(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
if user.Conn == nil {
jsonResponse(w, http.StatusNotFound, Error{
Error: "You don't have a WhatsApp connection.",
ErrCode: "no connection",
})
return
}
//sess, err :=
//user.Conn.Stop(context.TODO())
// if err == whatsapp.ErrNotConnected {
// jsonResponse(w, http.StatusNotFound, Error{
// Error: "You were not connected",
// ErrCode: "not connected",
// })
// return
// } else if err != nil {
// user.log.Warnln("Error while disconnecting:", err)
// jsonResponse(w, http.StatusInternalServerError, Error{
// Error: fmt.Sprintf("Unknown error while disconnecting: %v", err),
// ErrCode: err.Error(),
// })
// return
// } else if len(sess.Wid) > 0 {
// user.SetSession(&sess)
// }
user.bridge.Metrics.TrackConnectionState(user.GMID, false)
jsonResponse(w, http.StatusOK, Response{true, "Disconnected from WhatsApp"})
}
func (prov *ProvisioningAPI) Reconnect(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value("user").(*User)
// if user.Conn == nil {
// if user.Session == nil {
// jsonResponse(w, http.StatusForbidden, Error{
// Error: "No existing connection and no session. Please log in first.",
// ErrCode: "no session",
// })
// } else {
// user.Connect(false)
// jsonResponse(w, http.StatusOK, Response{true, "Created connection to WhatsApp."})
// }
// return
// }
// wasConnected := true
// sess, err := user.Conn.Disconnect()
// if err == whatsapp.ErrNotConnected {
// wasConnected = false
// } else if err != nil {
// user.log.Warnln("Error while disconnecting:", err)
// } else if len(sess.Wid) > 0 {
// user.SetSession(&sess)
// }
// err = user.Conn.Restore()
// if err == whatsapp.ErrInvalidSession {
// if user.Session != nil {
// user.log.Debugln("Got invalid session error when reconnecting, but user has session. Retrying using RestoreWithSession()...")
// var sess whatsapp.Session
// sess, err = user.Conn.RestoreWithSession(*user.Session)
// if err == nil {
// user.SetSession(&sess)
// }
// } else {
// jsonResponse(w, http.StatusForbidden, Error{
// Error: "You're not logged in",
// ErrCode: "not logged in",
// })
// return
// }
// } else if err == whatsapp.ErrLoginInProgress {
// jsonResponse(w, http.StatusConflict, Error{
// Error: "A login or reconnection is already in progress.",
// ErrCode: "login in progress",
// })
// return
// } else if err == whatsapp.ErrAlreadyLoggedIn {
// jsonResponse(w, http.StatusConflict, Error{
// Error: "You were already connected.",
// ErrCode: err.Error(),
// })
// return
// }
// if err != nil {
// user.log.Warnln("Error while reconnecting:", err)
// if err.Error() == "restore session connection timed out" {
// jsonResponse(w, http.StatusForbidden, Error{
// Error: "Reconnection timed out. Is WhatsApp on your phone reachable?",
// ErrCode: err.Error(),
// })
// } else {
// jsonResponse(w, http.StatusForbidden, Error{
// Error: fmt.Sprintf("Unknown error while reconnecting: %v", err),
// ErrCode: err.Error(),
// })
// }
// user.log.Debugln("Disconnecting due to failed session restore in reconnect command...")
// sess, err := user.Conn.Disconnect()
// if err != nil {
// user.log.Errorln("Failed to disconnect after failed session restore in reconnect command:", err)
// } else if len(sess.Wid) > 0 {
// user.SetSession(&sess)
// }
// return
// }
// user.ConnectionErrors = 0
// user.PostLogin()
// var msg string
// if wasConnected {
// msg = "Reconnected successfully."
// } else {
// msg = "Connected successfully."
// }
// jsonResponse(w, http.StatusOK, Response{true, msg})
}
func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value("user").(*User)
// wa := map[string]interface{}{
// "has_session": user.Client != nil,
// "management_room": user.ManagementRoom,
// "jid": user.JID,
// "conn": nil,
// "ping": nil,
// }
// if user.Conn != nil {
// wa["conn"] = map[string]interface{}{
// "is_connected": user.IsConnected(),
// "is_logged_in": user.IsLoggedIn(),
// "is_login_in_progress": user.IsLoginInProgress(),
// }
// err := user.Conn.AdminTest()
// wa["ping"] = map[string]interface{}{
// "ok": err == nil,
// "err": err,
// }
// }
// resp := map[string]interface{}{
// "mxid": user.MXID,
// "admin": user.Admin,
// "whitelisted": user.Whitelisted,
// "relaybot_whitelisted": user.RelaybotWhitelisted,
// "whatsapp": wa,
// }
// jsonResponse(w, http.StatusOK, resp)
}
func jsonResponse(w http.ResponseWriter, status int, response interface{}) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(response)
}
func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
// user := r.Context().Value("user").(*User)
// if user.Session == nil {
// jsonResponse(w, http.StatusNotFound, Error{
// Error: "You're not logged in",
// ErrCode: "not logged in",
// })
// return
// }
// force := strings.ToLower(r.URL.Query().Get("force")) != "false"
// if user.Conn == nil {
// if !force {
// jsonResponse(w, http.StatusNotFound, Error{
// Error: "You're not connected",
// ErrCode: "not connected",
// })
// }
// } else {
// err := user.Conn.Logout()
// if err != nil {
// user.log.Warnln("Error while logging out:", err)
// if !force {
// jsonResponse(w, http.StatusInternalServerError, Error{
// Error: fmt.Sprintf("Unknown error while logging out: %v", err),
// ErrCode: err.Error(),
// })
// return
// }
// }
// _, err = user.Conn.Disconnect()
// if err != nil {
// user.log.Warnln("Error while disconnecting after logout:", err)
// }
// user.Conn.RemoveHandlers()
// user.Conn = nil
// }
// user.bridge.Metrics.TrackConnectionState(user.JID, false)
// user.removeFromJIDMap()
// // TODO this causes a foreign key violation, which should be fixed
// //ce.User.JID = ""
// user.SetSession(nil)
// jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"net.maunium.whatsapp.login"},
}
func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
// userID := r.URL.Query().Get("user_id")
// user := prov.bridge.GetUserByMXID(id.UserID(userID))
// if len(ce.Args) < 1 {
// // Return error that the token needs to be longer than 0 length
// // ce.Reply(`Get your access token from https://dev.groupme.com/ which should be the first argument to login`)
// return
// }
// user.Token = ce.Args[0]
// user.addToJIDMap()
// // ce.Reply("Successfully logged in, synchronizing chats...")
// user.PostLogin()
// user.Connect()
// c, err := upgrader.Upgrade(w, r, nil)
// if err != nil {
// prov.log.Errorfln("Failed to upgrade connection to websocket:", err)
// return
// }
// defer c.Close()
// if !user.Connect(true) {
// user.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.")
// _ = c.WriteJSON(Error{
// Error: "Failed to connect to WhatsApp",
// ErrCode: "connection error",
// })
// return
// }
// qrChan := make(chan string, 3)
// go func() {
// for code := range qrChan {
// if code == "stop" {
// return
// }
// _ = c.WriteJSON(map[string]interface{}{
// "code": code,
// })
// }
// }()
// session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount)
// qrChan <- "stop"
// if err != nil {
// var msg string
// if err == whatsapp.ErrAlreadyLoggedIn {
// msg = "You're already logged in"
// } else if err == whatsapp.ErrLoginInProgress {
// msg = "You have a login in progress already."
// } else if err == whatsapp.ErrLoginTimedOut {
// msg = "QR code scan timed out. Please try again."
// } else {
// user.log.Warnln("Failed to log in:", err)
// msg = fmt.Sprintf("Unknown error while logging in: %v", err)
// }
// _ = c.WriteJSON(Error{
// Error: msg,
// ErrCode: err.Error(),
// })
// return
// }
// user.ConnectionErrors = 0
// user.JID = strings.Replace(user.Conn.Info.Wid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, 1)
// user.addToJIDMap()
// user.SetSession(&session)
// _ = c.WriteJSON(map[string]interface{}{
// "success": true,
// "jid": user.JID,
// })
// user.PostLogin()
}

View File

@ -30,6 +30,7 @@ const SegmentURL = "https://api.segment.io/v1/track"
type SegmentClient struct {
key string
userID string
log log.Logger
client http.Client
}
@ -38,8 +39,14 @@ var Segment SegmentClient
func (sc *SegmentClient) trackSync(userID id.UserID, event string, properties map[string]interface{}) error {
var buf bytes.Buffer
var segmentUserID string
if Segment.userID != "" {
segmentUserID = Segment.userID
} else {
segmentUserID = userID.String()
}
err := json.NewEncoder(&buf).Encode(map[string]interface{}{
"userId": userID,
"userId": segmentUserID,
"event": event,
"properties": properties,
})

407
user.go
View File

@ -26,9 +26,6 @@ import (
"time"
log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
@ -206,7 +203,7 @@ func (br *GMBridge) NewUser(dbUser *database.User) *User {
user.PermissionLevel = user.bridge.Config.Bridge.Permissions.Get(user.MXID)
user.Whitelisted = user.PermissionLevel >= bridgeconfig.PermissionLevelUser
user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin
user.BridgeState = br.NewBridgeStateQueue(user, user.log)
user.BridgeState = br.NewBridgeStateQueue(user)
go user.handleMessageLoop()
go user.runMessageRingBuffer()
return user
@ -332,14 +329,6 @@ func (user *User) SetManagementRoom(roomID id.RoomID) {
user.Update()
}
func (user *User) SetSession(session *whatsapp.Session) {
// user.Session = session
// if session == nil {
// user.LastConnection = 0
// }
// user.Update()
}
func (user *User) Connect() bool {
if user.Conn != nil {
return true
@ -347,14 +336,14 @@ func (user *User) Connect() bool {
return false
}
user.log.Debugln("Connecting to GroupMe")
user.log.Debugfln("Connecting to GroupMe")
timeout := time.Duration(user.bridge.Config.GroupMe.ConnectionTimeout)
if timeout == 0 {
timeout = 20
}
conn := groupme.NewPushSubscription(context.Background())
user.Conn = &conn
user.Conn.StartListening(context.TODO(), groupmeext.NewFayeClient(user.log))
user.Conn.StartListening(context.Background(), groupmeext.NewFayeClient(user.log))
user.Conn.AddFullHandler(user)
//TODO: typing notification?
@ -737,112 +726,7 @@ func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) {
}
}
func (user *User) HandleContactList(contacts []whatsapp.Contact) {
contactMap := make(map[string]whatsapp.Contact)
for _, contact := range contacts {
contactMap[contact.Jid] = contact
}
go user.syncPuppets(contactMap)
}
func (user *User) syncPuppets(contacts map[string]whatsapp.Contact) {
// if contacts == nil {
// contacts = user.Conn.Store.Contacts
// }
// user.log.Infoln("Syncing puppet info from contacts")
// for jid, contact := range contacts {
// if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) {
// puppet := user.bridge.GetPuppetByJID(contact.Jid)
// puppet.Sync(user, contact)
// }
// }
// user.log.Infoln("Finished syncing puppet info from contacts")
}
func (user *User) updateLastConnectionIfNecessary() {
// if user.LastConnection+60 < uint64(time.Now().Unix()) {
// user.UpdateLastConnection()
// }
}
func (user *User) HandleError(err error) {
if !errors.Is(err, whatsapp.ErrInvalidWsData) {
user.log.Errorfln("WhatsApp error: %v", err)
}
if closed, ok := err.(*whatsapp.ErrConnectionClosed); ok {
user.bridge.Metrics.TrackDisconnection(user.MXID)
if closed.Code == 1000 && user.cleanDisconnection {
user.bridge.Metrics.TrackConnectionState(user.GMID, false)
user.cleanDisconnection = false
user.log.Infoln("Clean disconnection by server")
return
}
go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection was closed with websocket status code %d", closed.Code))
} else if failed, ok := err.(*whatsapp.ErrConnectionFailed); ok {
user.bridge.Metrics.TrackDisconnection(user.MXID)
user.ConnectionErrors++
go user.tryReconnect(fmt.Sprintf("Your WhatsApp connection failed: %v", failed.Err))
}
// Otherwise unknown error, probably mostly harmless
}
func (user *User) tryReconnect(msg string) {
// user.bridge.Metrics.TrackConnectionState(user.JID, false)
// if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts {
// user.sendMarkdownBridgeAlert("%s. Use the `reconnect` command to reconnect.", msg)
// return
// }
// if user.bridge.Config.Bridge.ReportConnectionRetry {
// user.sendBridgeNotice("%s. Reconnecting...", msg)
// // Don't want the same error to be repeated
// msg = ""
// }
// var tries uint
// var exponentialBackoff bool
// baseDelay := time.Duration(user.bridge.Config.Bridge.ConnectionRetryDelay)
// if baseDelay < 0 {
// exponentialBackoff = true
// baseDelay = -baseDelay + 1
// }
// delay := baseDelay
// for user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts {
// err := user.Conn.Restore()
// if err == nil {
// user.ConnectionErrors = 0
// if user.bridge.Config.Bridge.ReportConnectionRetry {
// user.sendBridgeNotice("Reconnected successfully")
// }
// user.PostLogin()
// return
// } else if err.Error() == "init responded with 400" {
// user.log.Infoln("Got init 400 error when trying to reconnect, resetting connection...")
// sess, err := user.Conn.Disconnect()
// if err != nil {
// user.log.Debugln("Error while disconnecting for connection reset:", err)
// }
// if len(sess.Wid) > 0 {
// user.SetSession(&sess)
// }
// }
// user.log.Errorln("Error while trying to reconnect after disconnection:", err)
// tries++
// user.ConnectionErrors++
// if user.ConnectionErrors <= user.bridge.Config.Bridge.MaxConnectionAttempts {
// if exponentialBackoff {
// delay = (1 << tries) + baseDelay
// }
// if user.bridge.Config.Bridge.ReportConnectionRetry {
// user.sendBridgeNotice("Reconnection attempt failed: %v. Retrying in %d seconds...", err, delay)
// }
// time.Sleep(delay * time.Second)
// }
// }
// if user.bridge.Config.Bridge.ReportConnectionRetry {
// user.sendMarkdownBridgeAlert("%d reconnection attempts failed. Use the `reconnect` command to try to reconnect manually.", tries)
// } else {
// user.sendMarkdownBridgeAlert("\u26a0 %s. Additionally, %d reconnection attempts failed. Use the `reconnect` command to try to reconnect.", msg, tries)
// }
}
func (user *User) ShouldCallSynchronously() bool {
@ -850,7 +734,7 @@ func (user *User) ShouldCallSynchronously() bool {
}
func (user *User) HandleJSONParseError(err error) {
user.log.Errorln("WhatsApp JSON parse error:", err)
user.log.Errorln("GroupMe JSON parse error:", err)
}
func (user *User) PortalKey(gmid groupme.ID) database.PortalKey {
@ -893,34 +777,6 @@ func (user *User) handleMessageLoop() {
}
}
//func (user *User) HandleNewContact(contact whatsapp.Contact) {
// user.log.Debugfln("Contact message: %+v", contact)
// go func() {
// if strings.HasSuffix(contact.Jid, whatsappExt.OldUserSuffix) {
// contact.Jid = strings.Replace(contact.Jid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, -1)
// }
// puppet := user.bridge.GetPuppetByJID(contact.Jid)
// puppet.UpdateName(user, contact)
// }()
//}
//func (user *User) HandleBatteryMessage(battery whatsapp.BatteryMessage) {
// user.log.Debugfln("Battery message: %+v", battery)
// var notice string
// if !battery.Plugged && battery.Percentage < 15 && user.batteryWarningsSent < 1 {
// notice = fmt.Sprintf("Phone battery low (%d %% remaining)", battery.Percentage)
// user.batteryWarningsSent = 1
// } else if !battery.Plugged && battery.Percentage < 5 && user.batteryWarningsSent < 2 {
// notice = fmt.Sprintf("Phone battery very low (%d %% remaining)", battery.Percentage)
// user.batteryWarningsSent = 2
// } else if battery.Percentage > 15 || battery.Plugged {
// user.batteryWarningsSent = 0
// }
// if notice != "" {
// go user.sendBridgeNotice("%s", notice)
// }
//}
func (user *User) HandleTextMessage(message groupme.Message) {
id := database.ParsePortalKey(message.GroupID.String())
@ -983,270 +839,15 @@ func (user *User) HandleNewNickname(groupID, userID groupme.ID, name string) {
func (user *User) HandleNewAvatarInGroup(groupID, userID groupme.ID, url string) {
puppet := user.bridge.GetPuppetByGMID(userID)
if puppet != nil {
puppet.UpdateAvatar(user, false)
}
}
func (user *User) HandleMembers(_ groupme.ID, _ []groupme.Member, _ bool) {
user.HandleChatList()
}
//func (user *User) HandleImageMessage(message whatsapp.ImageMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleContactMessage(message whatsapp.ContactMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) {
// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp}
//}
//
//func (user *User) HandleMessageRevoke(message whatsappExt.MessageRevocation) {
// user.messageInput <- PortalMessage{message.RemoteJid, user, message, 0}
//}
type FakeMessage struct {
Text string
ID string
Alert bool
}
//func (user *User) HandleCallInfo(info whatsappExt.CallInfo) {
// if info.Data != nil {
// return
// }
// data := FakeMessage{
// ID: info.ID,
// }
// switch info.Type {
// case whatsappExt.CallOffer:
// if !user.bridge.Config.Bridge.CallNotices.Start {
// return
// }
// data.Text = "Incoming call"
// data.Alert = true
// case whatsappExt.CallOfferVideo:
// if !user.bridge.Config.Bridge.CallNotices.Start {
// return
// }
// data.Text = "Incoming video call"
// data.Alert = true
// case whatsappExt.CallTerminate:
// if !user.bridge.Config.Bridge.CallNotices.End {
// return
// }
// data.Text = "Call ended"
// data.ID += "E"
// default:
// return
// }
// portal := user.GetPortalByJID(info.From)
// if portal != nil {
// portal.messages <- PortalMessage{info.From, user, data, 0}
// }
//}
//func (user *User) HandlePresence(info whatsappExt.Presence) {
// puppet := user.bridge.GetPuppetByJID(info.SenderJID)
// switch info.Status {
// case whatsapp.PresenceUnavailable:
// _ = puppet.DefaultIntent().SetPresence("offline")
// case whatsapp.PresenceAvailable:
// if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
// portal := user.bridge.GetPortalByMXID(puppet.typingIn)
// _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0)
// puppet.typingIn = ""
// puppet.typingAt = 0
// } else {
// _ = puppet.DefaultIntent().SetPresence("online")
// }
// case whatsapp.PresenceComposing:
// portal := user.GetPortalByJID(info.JID)
// if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
// if puppet.typingIn == portal.MXID {
// return
// }
// _, _ = puppet.IntentFor(portal).UserTyping(puppet.typingIn, false, 0)
// }
// puppet.typingIn = portal.MXID
// puppet.typingAt = time.Now().Unix()
// _, _ = puppet.IntentFor(portal).UserTyping(portal.MXID, true, 15*1000)
// }
//}
//
//func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) {
// if (info.Command == whatsappExt.MsgInfoCommandAck || info.Command == whatsappExt.MsgInfoCommandAcks) && info.Acknowledgement == whatsappExt.AckMessageRead {
// portal := user.GetPortalByJID(info.ToJID)
// if len(portal.MXID) == 0 {
// return
// }
//
// go func() {
// intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal)
// for _, msgID := range info.IDs {
// msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID)
// if msg == nil {
// continue
// }
//
// err := intent.MarkRead(portal.MXID, msg.MXID)
// if err != nil {
// user.log.Warnln("Failed to mark message %s as read by %s: %v", msg.MXID, info.SenderJID, err)
// }
// }
// }()
// }
//}
//func (user *User) HandleReceivedMessage(received whatsapp.ReceivedMessage) {
// if received.Type == "read" {
// user.markSelfRead(received.Jid, received.Index)
// } else {
// user.log.Debugfln("Unknown received message type: %+v", received)
// }
//}
//
//func (user *User) HandleReadMessage(read whatsapp.ReadMessage) {
// user.log.Debugfln("Received chat read message: %+v", read)
// user.markSelfRead(read.Jid, "")
//}
//
//func (user *User) markSelfRead(jid, messageID string) {
// if strings.HasSuffix(jid, whatsappExt.OldUserSuffix) {
// jid = strings.Replace(jid, whatsappExt.OldUserSuffix, whatsappExt.NewUserSuffix, -1)
// }
// puppet := user.bridge.GetPuppetByJID(user.JID)
// if puppet == nil {
// return
// }
// intent := puppet.CustomIntent()
// if intent == nil {
// return
// }
// portal := user.GetPortalByJID(jid)
// if portal == nil {
// return
// }
// var message *database.Message
// if messageID == "" {
// message = user.bridge.DB.Message.GetLastInChat(portal.Key)
// if message == nil {
// return
// }
// user.log.Debugfln("User read chat %s/%s in WhatsApp mobile (last known event: %s/%s)", portal.Key.JID, portal.MXID, message.JID, message.MXID)
// } else {
// message = user.bridge.DB.Message.GetByJID(portal.Key, messageID)
// if message == nil {
// return
// }
// user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID)
// }
// err := intent.MarkRead(portal.MXID, message.MXID)
// if err != nil {
// user.log.Warnfln("Failed to bridge own read receipt in %s: %v", jid, err)
// }
//}
//
//func (user *User) HandleCommand(cmd whatsappExt.Command) {
// switch cmd.Type {
// case whatsappExt.CommandPicture:
// if strings.HasSuffix(cmd.JID, whatsappExt.NewUserSuffix) {
// puppet := user.bridge.GetPuppetByJID(cmd.JID)
// go puppet.UpdateAvatar(user, cmd.ProfilePicInfo)
// } else {
// portal := user.GetPortalByJID(cmd.JID)
// go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true)
// }
// case whatsappExt.CommandDisconnect:
// user.cleanDisconnection = true
// if cmd.Kind == "replaced" {
// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" +
// "Use the `reconnect` command to disconnect the other client and resume bridging.")
// } else {
// user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw))
// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+
// "Use the `reconnect` command to reconnect.", cmd.Kind)
// }
// }
//}
//
//func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) {
// if cmd.Command != whatsappExt.ChatUpdateCommandAction {
// return
// }
//
// portal := user.GetPortalByJID(cmd.JID)
// if len(portal.MXID) == 0 {
// if cmd.Data.Action == whatsappExt.ChatActionIntroduce || cmd.Data.Action == whatsappExt.ChatActionCreate {
// go func() {
// err := portal.CreateMatrixRoom(user)
// if err != nil {
// user.log.Errorln("Failed to create portal room after receiving join event:", err)
// }
// }()
// }
// return
// }
//
// switch cmd.Data.Action {
// case whatsappExt.ChatActionNameChange:
// go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, true)
// case whatsappExt.ChatActionAddTopic:
// go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, true)
// case whatsappExt.ChatActionRemoveTopic:
// go portal.UpdateTopic("", cmd.Data.SenderJID, true)
// case whatsappExt.ChatActionPromote:
// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true)
// case whatsappExt.ChatActionDemote:
// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false)
// case whatsappExt.ChatActionAnnounce:
// go portal.RestrictMessageSending(cmd.Data.Announce)
// case whatsappExt.ChatActionRestrict:
// go portal.RestrictMetadataChanges(cmd.Data.Restrict)
// case whatsappExt.ChatActionRemove:
// go portal.HandleWhatsAppKick(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
// case whatsappExt.ChatActionAdd:
// go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs)
// //case whatsappExt.ChatActionIntroduce:
// // if cmd.Data.SenderJID != "unknown" {
// // go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID})
// // }
// }
//}
//func (user *User) HandleJsonMessage(message string) {
// var msg json.RawMessage
// err := json.Unmarshal([]byte(message), &msg)
// if err != nil {
// return
// }
// user.log.Debugln("JSON message:", message)
// user.updateLastConnectionIfNecessary()
//}
//func (user *User) HandleRawMessage(message *waProto.WebMessageInfo) {
// user.updateLastConnectionIfNecessary()
//}
//
//func (user *User) HandleUnknownBinaryNode(node *waBinary.Node) {
// user.log.Debugfln("Unknown binary message: %+v", node)
//}