mautrix-whatsapp/main.go

285 lines
8.9 KiB
Go
Raw Permalink Normal View History

// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2022 Tulir Asokan
2018-08-12 21:26:05 +02:00
//
// 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 (
2021-10-31 19:42:53 +01:00
_ "embed"
"net/http"
"os"
2021-10-22 19:14:34 +02:00
"strconv"
"strings"
"sync"
2019-11-10 20:22:11 +01:00
"time"
2018-08-26 21:53:13 +02:00
2022-05-23 23:08:28 +02:00
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow"
2021-10-22 19:14:34 +02:00
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
2021-02-17 00:21:30 +01:00
"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"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix/util/configupgrade"
2019-01-11 20:17:31 +01:00
2018-08-26 21:53:13 +02:00
"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 (
2020-06-03 19:59:44 +02:00
Tag = "unknown"
Commit = "unknown"
BuildTime = "unknown"
)
2021-10-31 12:04:44 +01:00
//go:embed example-config.yaml
var ExampleConfig string
type WABridge struct {
bridge.Bridge
Config *config.Config
DB *database.Database
Provisioning *ProvisioningAPI
Formatter *Formatter
Metrics *MetricsHandler
WAContainer *sqlstore.Container
WAVersion string
2020-05-08 21:32:22 +02:00
usersByMXID map[id.UserID]*User
2021-10-22 19:14:34 +02:00
usersByUsername map[string]*User
usersLock sync.Mutex
spaceRooms map[id.RoomID]*User
spaceRoomsLock sync.Mutex
managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex
2020-05-08 21:32:22 +02:00
portalsByMXID map[id.RoomID]*Portal
portalsByJID map[database.PortalKey]*Portal
portalsLock sync.Mutex
2021-10-22 19:14:34 +02:00
puppets map[types.JID]*Puppet
2020-05-08 21:32:22 +02:00
puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex
}
func (br *WABridge) Init() {
br.CommandProcessor = commands.NewProcessor(&br.Bridge)
br.RegisterCommands()
// TODO this is a weird place for this
br.EventProcessor.On(event.EphemeralEventPresence, br.HandlePresence)
2022-11-17 23:20:14 +01:00
br.EventProcessor.On(TypeMSC3881PollResponse, br.MatrixHandler.HandleMessage)
br.EventProcessor.On(TypeMSC3881V2PollResponse, br.MatrixHandler.HandleMessage)
Segment.log = br.Log.Sub("Segment")
Segment.key = br.Config.SegmentKey
2022-05-16 12:46:18 +02:00
if Segment.IsEnabled() {
Segment.log.Infoln("Segment metrics are enabled")
}
2022-08-14 18:26:42 +02:00
br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
br.WAContainer = sqlstore.NewWithDB(br.DB.RawDB, br.DB.Dialect.String(), &waLogger{br.Log.Sub("Database").Sub("WhatsApp")})
br.WAContainer.DatabaseErrorHandler = br.DB.HandleSignalStoreError
2021-10-22 19:14:34 +02:00
ss := br.Config.Bridge.Provisioning.SharedSecret
2020-02-09 19:32:14 +01:00
if len(ss) > 0 && ss != "disable" {
br.Provisioning = &ProvisioningAPI{bridge: br}
2020-02-09 19:32:14 +01:00
}
br.Formatter = NewFormatter(br)
br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)
2022-05-22 16:32:22 +02:00
br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent
2021-10-22 19:14:34 +02:00
store.BaseClientPayload.UserAgent.OsVersion = proto.String(br.WAVersion)
store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(br.WAVersion)
2022-05-23 23:08:28 +02:00
store.DeviceProps.Os = proto.String(br.Config.WhatsApp.OSName)
store.DeviceProps.RequireFullSync = proto.Bool(br.Config.Bridge.HistorySync.RequestFullSync)
versionParts := strings.Split(br.WAVersion, ".")
2021-10-22 19:14:34 +02:00
if len(versionParts) > 2 {
primary, _ := strconv.Atoi(versionParts[0])
secondary, _ := strconv.Atoi(versionParts[1])
tertiary, _ := strconv.Atoi(versionParts[2])
2022-05-23 23:08:28 +02:00
store.DeviceProps.Version.Primary = proto.Uint32(uint32(primary))
store.DeviceProps.Version.Secondary = proto.Uint32(uint32(secondary))
store.DeviceProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
2021-10-22 19:14:34 +02:00
}
2022-07-30 10:30:44 +02:00
platformID, ok := waProto.DeviceProps_PlatformType_value[strings.ToUpper(br.Config.WhatsApp.BrowserName)]
2021-10-22 19:14:34 +02:00
if ok {
2022-07-30 10:30:44 +02:00
store.DeviceProps.PlatformType = waProto.DeviceProps_PlatformType(platformID).Enum()
2021-10-22 19:14:34 +02:00
}
}
func (br *WABridge) Start() {
err := br.WAContainer.Upgrade()
if err != nil {
br.Log.Fatalln("Failed to upgrade whatsmeow database: %v", err)
2018-08-29 22:48:15 +02:00
os.Exit(15)
}
if br.Provisioning != nil {
br.Log.Debugln("Initializing provisioning API")
br.Provisioning.Init()
2020-02-09 19:32:14 +01:00
}
go br.CheckWhatsAppUpdate()
go br.StartUsers()
if br.Config.Metrics.Enabled {
go br.Metrics.Start()
}
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) {
2022-03-23 13:46:30 +01:00
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)
2022-03-23 13:46:30 +01:00
} 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) StartUsers() {
br.Log.Debugln("Starting users")
foundAnySessions := false
for _, user := range br.GetAllUsers() {
2021-10-22 19:14:34 +02:00
if !user.JID.IsEmpty() {
foundAnySessions = true
}
go user.Connect()
}
if !foundAnySessions {
br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
}
br.Log.Debugln("Starting custom puppets")
for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() {
2019-05-31 22:07:33 +02:00
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)
}
2019-05-31 22:07:33 +02:00
}(loopuppet)
}
}
func (br *WABridge) Stop() {
br.Metrics.Stop()
for _, user := range br.usersByUsername {
2021-10-22 19:14:34 +02:00
if user.Client == nil {
continue
}
br.Log.Debugln("Disconnecting", user.MXID)
2021-10-22 19:14:34 +02:00
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
}
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 {
2022-06-30 12:52:07 +02:00
return "Backfilling is enabled in bridge config, but homeserver does not support MSC2716 batch sending", false
} else if !supported {
2022-06-30 12:52:07 +02:00
return "Backfilling is enabled in bridge config, but MSC2716 batch sending is not enabled on homeserver", false
}
}
return "", true
}
2018-08-12 21:26:05 +02:00
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.",
2022-12-16 16:20:51 +01:00
Version: "0.8.0",
ProtocolName: "WhatsApp",
CryptoPickleKey: "maunium.net/go/mautrix-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()
}