123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- // 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"
- "google.golang.org/protobuf/proto"
- "go.mau.fi/whatsmeow"
- waProto "go.mau.fi/whatsmeow/binary/proto"
- "go.mau.fi/whatsmeow/store"
- "go.mau.fi/whatsmeow/store/sqlstore"
- "go.mau.fi/whatsmeow/types"
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/bridge"
- "maunium.net/go/mautrix/bridge/commands"
- "maunium.net/go/mautrix/bridge/status"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
- "maunium.net/go/mautrix/util/configupgrade"
- "maunium.net/go/mautrix-whatsapp/config"
- "maunium.net/go/mautrix-whatsapp/database"
- )
- // 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
- 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() {
- br.CommandProcessor = commands.NewProcessor(&br.Bridge)
- br.RegisterCommands()
- // TODO this is a weird place for this
- br.EventProcessor.On(event.EphemeralEventPresence, br.HandlePresence)
- br.EventProcessor.On(TypeMSC3381PollStart, br.MatrixHandler.HandleMessage)
- br.EventProcessor.On(TypeMSC3381PollResponse, br.MatrixHandler.HandleMessage)
- br.EventProcessor.On(TypeMSC3381V2PollResponse, br.MatrixHandler.HandleMessage)
- Segment.log = br.Log.Sub("Segment")
- Segment.key = br.Config.SegmentKey
- Segment.userID = br.Config.SegmentUserId
- if Segment.IsEnabled() {
- Segment.log.Infoln("Segment metrics are enabled")
- if Segment.userID != "" {
- Segment.log.Infoln("Overriding Segment user_id with %v", Segment.userID)
- }
- }
- 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
- ss := br.Config.Bridge.Provisioning.SharedSecret
- if len(ss) > 0 && ss != "disable" {
- 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
- store.BaseClientPayload.UserAgent.OsVersion = proto.String(br.WAVersion)
- store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(br.WAVersion)
- store.DeviceProps.Os = proto.String(br.Config.WhatsApp.OSName)
- store.DeviceProps.RequireFullSync = proto.Bool(br.Config.Bridge.HistorySync.RequestFullSync)
- if fsc := br.Config.Bridge.HistorySync.FullSyncConfig; fsc.DaysLimit > 0 && fsc.SizeLimit > 0 && fsc.StorageQuota > 0 {
- store.DeviceProps.HistorySyncConfig = &waProto.DeviceProps_HistorySyncConfig{
- FullSyncDaysLimit: proto.Uint32(fsc.DaysLimit),
- FullSyncSizeMbLimit: proto.Uint32(fsc.SizeLimit),
- StorageQuotaMb: proto.Uint32(fsc.StorageQuota),
- }
- }
- 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.DeviceProps.Version.Primary = proto.Uint32(uint32(primary))
- store.DeviceProps.Version.Secondary = proto.Uint32(uint32(secondary))
- store.DeviceProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
- }
- platformID, ok := waProto.DeviceProps_PlatformType_value[strings.ToUpper(br.Config.WhatsApp.BrowserName)]
- if ok {
- store.DeviceProps.PlatformType = waProto.DeviceProps_PlatformType(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()
- }
- 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) 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(status.BridgeState{StateEvent: status.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() {
- br.Metrics.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
- }
- const unstableFeatureBatchSending = "org.matrix.msc2716"
- func (br *WABridge) CheckFeatures(versions *mautrix.RespVersions) (string, bool) {
- if br.Config.Bridge.HistorySync.Backfill {
- supported, known := versions.UnstableFeatures[unstableFeatureBatchSending]
- if !known {
- return "Backfilling is enabled in bridge config, but homeserver does not support MSC2716 batch sending", false
- } else if !supported {
- return "Backfilling is enabled in bridge config, but MSC2716 batch sending is not enabled on homeserver", false
- }
- }
- return "", true
- }
- func main() {
- br := &WABridge{
- usersByMXID: make(map[id.UserID]*User),
- 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.8.1",
- 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()
- }
|