forked from MirrorHub/mautrix-whatsapp
a948ea0146
See d578d1a610
Database upgrades from before v0.4.0 were squashed, users must update
to at least v0.4.0 before updating beyond this commit.
284 lines
8.7 KiB
Go
284 lines
8.7 KiB
Go
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
|
// Copyright (C) 2022 Tulir Asokan
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"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"
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"maunium.net/go/mautrix/bridge"
|
|
"maunium.net/go/mautrix/id"
|
|
"maunium.net/go/mautrix/util/configupgrade"
|
|
|
|
"maunium.net/go/mautrix-whatsapp/config"
|
|
"maunium.net/go/mautrix-whatsapp/database"
|
|
)
|
|
|
|
// Information to find out exactly which commit the bridge was built from.
|
|
// These are filled at build time with the -X linker flag.
|
|
var (
|
|
Tag = "unknown"
|
|
Commit = "unknown"
|
|
BuildTime = "unknown"
|
|
)
|
|
|
|
//go:embed example-config.yaml
|
|
var ExampleConfig string
|
|
|
|
type WABridge struct {
|
|
bridge.Bridge
|
|
MatrixHandler *MatrixHandler
|
|
Config *config.Config
|
|
DB *database.Database
|
|
Provisioning *ProvisioningAPI
|
|
Formatter *Formatter
|
|
Metrics *MetricsHandler
|
|
WAContainer *sqlstore.Container
|
|
WAVersion string
|
|
|
|
usersByMXID map[id.UserID]*User
|
|
usersByUsername map[string]*User
|
|
usersLock sync.Mutex
|
|
spaceRooms map[id.RoomID]*User
|
|
spaceRoomsLock sync.Mutex
|
|
managementRooms map[id.RoomID]*User
|
|
managementRoomsLock sync.Mutex
|
|
portalsByMXID map[id.RoomID]*Portal
|
|
portalsByJID map[database.PortalKey]*Portal
|
|
portalsLock sync.Mutex
|
|
puppets map[types.JID]*Puppet
|
|
puppetsByCustomMXID map[id.UserID]*Puppet
|
|
puppetsLock sync.Mutex
|
|
}
|
|
|
|
func (br *WABridge) Init() {
|
|
Segment.log = br.Log.Sub("Segment")
|
|
Segment.key = br.Config.SegmentKey
|
|
if Segment.IsEnabled() {
|
|
Segment.log.Infoln("Segment metrics are enabled")
|
|
}
|
|
|
|
br.DB = database.New(br.Bridge.DB)
|
|
br.WAContainer = sqlstore.NewWithDB(br.DB.DB, br.DB.Dialect.String(), nil)
|
|
br.WAContainer.DatabaseErrorHandler = br.DB.HandleSignalStoreError
|
|
|
|
ss := br.Config.Bridge.Provisioning.SharedSecret
|
|
if len(ss) > 0 && ss != "disable" {
|
|
br.Provisioning = &ProvisioningAPI{bridge: br}
|
|
}
|
|
|
|
br.Log.Debugln("Initializing Matrix event handler")
|
|
br.MatrixHandler = NewMatrixHandler(br)
|
|
br.Formatter = NewFormatter(br)
|
|
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)
|
|
|
|
store.BaseClientPayload.UserAgent.OsVersion = proto.String(br.WAVersion)
|
|
store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(br.WAVersion)
|
|
store.CompanionProps.Os = proto.String(br.Config.WhatsApp.OSName)
|
|
store.CompanionProps.RequireFullSync = proto.Bool(br.Config.Bridge.HistorySync.RequestFullSync)
|
|
versionParts := strings.Split(br.WAVersion, ".")
|
|
if len(versionParts) > 2 {
|
|
primary, _ := strconv.Atoi(versionParts[0])
|
|
secondary, _ := strconv.Atoi(versionParts[1])
|
|
tertiary, _ := strconv.Atoi(versionParts[2])
|
|
store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
|
|
store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
|
|
store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
|
|
}
|
|
platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(br.Config.WhatsApp.BrowserName)]
|
|
if ok {
|
|
store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
|
|
}
|
|
}
|
|
|
|
func (br *WABridge) Start() {
|
|
err := br.WAContainer.Upgrade()
|
|
if err != nil {
|
|
br.Log.Fatalln("Failed to upgrade whatsmeow database: %v", err)
|
|
os.Exit(15)
|
|
}
|
|
if br.Provisioning != nil {
|
|
br.Log.Debugln("Initializing provisioning API")
|
|
br.Provisioning.Init()
|
|
}
|
|
go br.CheckWhatsAppUpdate()
|
|
go br.StartUsers()
|
|
if br.Config.Metrics.Enabled {
|
|
go br.Metrics.Start()
|
|
}
|
|
|
|
if br.Config.Bridge.ResendBridgeInfo {
|
|
go br.ResendBridgeInfo()
|
|
}
|
|
go br.Loop()
|
|
}
|
|
|
|
func (br *WABridge) CheckWhatsAppUpdate() {
|
|
br.Log.Debugfln("Checking for WhatsApp web update")
|
|
resp, err := whatsmeow.CheckUpdate(http.DefaultClient)
|
|
if err != nil {
|
|
br.Log.Warnfln("Failed to check for WhatsApp web update: %v", err)
|
|
return
|
|
}
|
|
if store.GetWAVersion() == resp.ParsedVersion {
|
|
br.Log.Debugfln("Bridge is using latest WhatsApp web protocol")
|
|
} else if store.GetWAVersion().LessThan(resp.ParsedVersion) {
|
|
if resp.IsBelowHard || resp.IsBroken {
|
|
br.Log.Warnfln("Bridge is using outdated WhatsApp web protocol and probably doesn't work anymore (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
|
|
} else if resp.IsBelowSoft {
|
|
br.Log.Infofln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
|
|
} else {
|
|
br.Log.Debugfln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
|
|
}
|
|
} else {
|
|
br.Log.Debugfln("Bridge is using newer than latest WhatsApp web protocol")
|
|
}
|
|
}
|
|
|
|
func (br *WABridge) Loop() {
|
|
for {
|
|
br.SleepAndDeleteUpcoming()
|
|
time.Sleep(1 * time.Hour)
|
|
br.WarnUsersAboutDisconnection()
|
|
}
|
|
}
|
|
|
|
func (br *WABridge) WarnUsersAboutDisconnection() {
|
|
br.usersLock.Lock()
|
|
for _, user := range br.usersByUsername {
|
|
if user.IsConnected() && !user.PhoneRecentlySeen(true) {
|
|
go user.sendPhoneOfflineWarning()
|
|
}
|
|
}
|
|
br.usersLock.Unlock()
|
|
}
|
|
|
|
func (br *WABridge) ResendBridgeInfo() {
|
|
// FIXME
|
|
//if *dontSaveConfig {
|
|
// br.Log.Warnln("Not setting resend_bridge_info to false in config due to --no-update flag")
|
|
//} else {
|
|
// err := config.Mutate(*configPath, func(helper *configupgrade.Helper) {
|
|
// helper.Set(configupgrade.Bool, "false", "bridge", "resend_bridge_info")
|
|
// })
|
|
// if err != nil {
|
|
// br.Log.Errorln("Failed to save config after setting resend_bridge_info to false:", err)
|
|
// }
|
|
//}
|
|
//br.Log.Infoln("Re-sending bridge info state event to all portals")
|
|
//for _, portal := range br.GetAllPortals() {
|
|
// portal.UpdateBridgeInfo()
|
|
//}
|
|
//br.Log.Infoln("Finished re-sending bridge info state events")
|
|
}
|
|
|
|
func (br *WABridge) StartUsers() {
|
|
br.Log.Debugln("Starting users")
|
|
foundAnySessions := false
|
|
for _, user := range br.GetAllUsers() {
|
|
if !user.JID.IsEmpty() {
|
|
foundAnySessions = true
|
|
}
|
|
go user.Connect()
|
|
}
|
|
if !foundAnySessions {
|
|
br.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))
|
|
}
|
|
br.Log.Debugln("Starting custom puppets")
|
|
for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() {
|
|
go func(puppet *Puppet) {
|
|
puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID)
|
|
err := puppet.StartCustomMXID(true)
|
|
if err != nil {
|
|
puppet.log.Errorln("Failed to start custom puppet:", err)
|
|
}
|
|
}(loopuppet)
|
|
}
|
|
}
|
|
|
|
func (br *WABridge) Stop() {
|
|
if br.Crypto != nil {
|
|
br.Crypto.Stop()
|
|
}
|
|
br.AS.Stop()
|
|
br.Metrics.Stop()
|
|
br.EventProcessor.Stop()
|
|
for _, user := range br.usersByUsername {
|
|
if user.Client == nil {
|
|
continue
|
|
}
|
|
br.Log.Debugln("Disconnecting", user.MXID)
|
|
user.Client.Disconnect()
|
|
close(user.historySyncs)
|
|
}
|
|
}
|
|
|
|
func (br *WABridge) GetExampleConfig() string {
|
|
return ExampleConfig
|
|
}
|
|
|
|
func (br *WABridge) GetConfigPtr() interface{} {
|
|
br.Config = &config.Config{
|
|
BaseConfig: &br.Bridge.Config,
|
|
}
|
|
br.Config.BaseConfig.Bridge = &br.Config.Bridge
|
|
return br.Config
|
|
}
|
|
|
|
func main() {
|
|
br := &WABridge{
|
|
usersByMXID: make(map[id.UserID]*User),
|
|
usersByUsername: make(map[string]*User),
|
|
spaceRooms: make(map[id.RoomID]*User),
|
|
managementRooms: make(map[id.RoomID]*User),
|
|
portalsByMXID: make(map[id.RoomID]*Portal),
|
|
portalsByJID: make(map[database.PortalKey]*Portal),
|
|
puppets: make(map[types.JID]*Puppet),
|
|
puppetsByCustomMXID: make(map[id.UserID]*Puppet),
|
|
}
|
|
br.Bridge = bridge.Bridge{
|
|
Name: "mautrix-whatsapp",
|
|
URL: "https://github.com/mautrix/whatsapp",
|
|
Description: "A Matrix-WhatsApp puppeting bridge.",
|
|
Version: "0.4.0",
|
|
ProtocolName: "WhatsApp",
|
|
|
|
ConfigUpgrader: &configupgrade.StructUpgrader{
|
|
SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
|
|
Blocks: config.SpacedBlocks,
|
|
Base: ExampleConfig,
|
|
},
|
|
|
|
Child: br,
|
|
}
|
|
br.InitVersion(Tag, Commit, BuildTime)
|
|
br.WAVersion = strings.FieldsFunc(br.Version, func(r rune) bool { return r == '-' || r == '+' })[0]
|
|
|
|
br.Main()
|
|
}
|