main.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
  2. // Copyright (C) 2022 Tulir Asokan
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package main
  17. import (
  18. _ "embed"
  19. "net/http"
  20. "os"
  21. "strconv"
  22. "strings"
  23. "sync"
  24. "time"
  25. "google.golang.org/protobuf/proto"
  26. "go.mau.fi/whatsmeow"
  27. waProto "go.mau.fi/whatsmeow/binary/proto"
  28. "go.mau.fi/whatsmeow/store"
  29. "go.mau.fi/whatsmeow/store/sqlstore"
  30. "go.mau.fi/whatsmeow/types"
  31. "maunium.net/go/mautrix"
  32. "maunium.net/go/mautrix/bridge"
  33. "maunium.net/go/mautrix/bridge/commands"
  34. "maunium.net/go/mautrix/bridge/status"
  35. "maunium.net/go/mautrix/event"
  36. "maunium.net/go/mautrix/id"
  37. "maunium.net/go/mautrix/util/configupgrade"
  38. "maunium.net/go/mautrix-whatsapp/config"
  39. "maunium.net/go/mautrix-whatsapp/database"
  40. )
  41. // Information to find out exactly which commit the bridge was built from.
  42. // These are filled at build time with the -X linker flag.
  43. var (
  44. Tag = "unknown"
  45. Commit = "unknown"
  46. BuildTime = "unknown"
  47. )
  48. //go:embed example-config.yaml
  49. var ExampleConfig string
  50. type WABridge struct {
  51. bridge.Bridge
  52. Config *config.Config
  53. DB *database.Database
  54. Provisioning *ProvisioningAPI
  55. Formatter *Formatter
  56. Metrics *MetricsHandler
  57. WAContainer *sqlstore.Container
  58. WAVersion string
  59. usersByMXID map[id.UserID]*User
  60. usersByUsername map[string]*User
  61. usersLock sync.Mutex
  62. spaceRooms map[id.RoomID]*User
  63. spaceRoomsLock sync.Mutex
  64. managementRooms map[id.RoomID]*User
  65. managementRoomsLock sync.Mutex
  66. portalsByMXID map[id.RoomID]*Portal
  67. portalsByJID map[database.PortalKey]*Portal
  68. portalsLock sync.Mutex
  69. puppets map[types.JID]*Puppet
  70. puppetsByCustomMXID map[id.UserID]*Puppet
  71. puppetsLock sync.Mutex
  72. }
  73. func (br *WABridge) Init() {
  74. br.CommandProcessor = commands.NewProcessor(&br.Bridge)
  75. br.RegisterCommands()
  76. // TODO this is a weird place for this
  77. br.EventProcessor.On(event.EphemeralEventPresence, br.HandlePresence)
  78. br.EventProcessor.On(TypeMSC3381PollStart, br.MatrixHandler.HandleMessage)
  79. br.EventProcessor.On(TypeMSC3381PollResponse, br.MatrixHandler.HandleMessage)
  80. br.EventProcessor.On(TypeMSC3381V2PollResponse, br.MatrixHandler.HandleMessage)
  81. Segment.log = br.Log.Sub("Segment")
  82. Segment.key = br.Config.SegmentKey
  83. Segment.userID = br.Config.SegmentUserId
  84. if Segment.IsEnabled() {
  85. Segment.log.Infoln("Segment metrics are enabled")
  86. if Segment.userID != "" {
  87. Segment.log.Infoln("Overriding Segment user_id with %v", Segment.userID)
  88. }
  89. }
  90. br.DB = database.New(br.Bridge.DB, br.Log.Sub("Database"))
  91. br.WAContainer = sqlstore.NewWithDB(br.DB.RawDB, br.DB.Dialect.String(), &waLogger{br.Log.Sub("Database").Sub("WhatsApp")})
  92. br.WAContainer.DatabaseErrorHandler = br.DB.HandleSignalStoreError
  93. ss := br.Config.Bridge.Provisioning.SharedSecret
  94. if len(ss) > 0 && ss != "disable" {
  95. br.Provisioning = &ProvisioningAPI{bridge: br}
  96. }
  97. br.Formatter = NewFormatter(br)
  98. br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)
  99. br.MatrixHandler.TrackEventDuration = br.Metrics.TrackMatrixEvent
  100. store.BaseClientPayload.UserAgent.OsVersion = proto.String(br.WAVersion)
  101. store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(br.WAVersion)
  102. store.DeviceProps.Os = proto.String(br.Config.WhatsApp.OSName)
  103. store.DeviceProps.RequireFullSync = proto.Bool(br.Config.Bridge.HistorySync.RequestFullSync)
  104. if fsc := br.Config.Bridge.HistorySync.FullSyncConfig; fsc.DaysLimit > 0 && fsc.SizeLimit > 0 && fsc.StorageQuota > 0 {
  105. store.DeviceProps.HistorySyncConfig = &waProto.DeviceProps_HistorySyncConfig{
  106. FullSyncDaysLimit: proto.Uint32(fsc.DaysLimit),
  107. FullSyncSizeMbLimit: proto.Uint32(fsc.SizeLimit),
  108. StorageQuotaMb: proto.Uint32(fsc.StorageQuota),
  109. }
  110. }
  111. versionParts := strings.Split(br.WAVersion, ".")
  112. if len(versionParts) > 2 {
  113. primary, _ := strconv.Atoi(versionParts[0])
  114. secondary, _ := strconv.Atoi(versionParts[1])
  115. tertiary, _ := strconv.Atoi(versionParts[2])
  116. store.DeviceProps.Version.Primary = proto.Uint32(uint32(primary))
  117. store.DeviceProps.Version.Secondary = proto.Uint32(uint32(secondary))
  118. store.DeviceProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
  119. }
  120. platformID, ok := waProto.DeviceProps_PlatformType_value[strings.ToUpper(br.Config.WhatsApp.BrowserName)]
  121. if ok {
  122. store.DeviceProps.PlatformType = waProto.DeviceProps_PlatformType(platformID).Enum()
  123. }
  124. }
  125. func (br *WABridge) Start() {
  126. err := br.WAContainer.Upgrade()
  127. if err != nil {
  128. br.Log.Fatalln("Failed to upgrade whatsmeow database: %v", err)
  129. os.Exit(15)
  130. }
  131. if br.Provisioning != nil {
  132. br.Log.Debugln("Initializing provisioning API")
  133. br.Provisioning.Init()
  134. }
  135. go br.CheckWhatsAppUpdate()
  136. go br.StartUsers()
  137. if br.Config.Metrics.Enabled {
  138. go br.Metrics.Start()
  139. }
  140. go br.Loop()
  141. }
  142. func (br *WABridge) CheckWhatsAppUpdate() {
  143. br.Log.Debugfln("Checking for WhatsApp web update")
  144. resp, err := whatsmeow.CheckUpdate(http.DefaultClient)
  145. if err != nil {
  146. br.Log.Warnfln("Failed to check for WhatsApp web update: %v", err)
  147. return
  148. }
  149. if store.GetWAVersion() == resp.ParsedVersion {
  150. br.Log.Debugfln("Bridge is using latest WhatsApp web protocol")
  151. } else if store.GetWAVersion().LessThan(resp.ParsedVersion) {
  152. if resp.IsBelowHard || resp.IsBroken {
  153. br.Log.Warnfln("Bridge is using outdated WhatsApp web protocol and probably doesn't work anymore (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  154. } else if resp.IsBelowSoft {
  155. br.Log.Infofln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  156. } else {
  157. br.Log.Debugfln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  158. }
  159. } else {
  160. br.Log.Debugfln("Bridge is using newer than latest WhatsApp web protocol")
  161. }
  162. }
  163. func (br *WABridge) Loop() {
  164. for {
  165. br.SleepAndDeleteUpcoming()
  166. time.Sleep(1 * time.Hour)
  167. br.WarnUsersAboutDisconnection()
  168. }
  169. }
  170. func (br *WABridge) WarnUsersAboutDisconnection() {
  171. br.usersLock.Lock()
  172. for _, user := range br.usersByUsername {
  173. if user.IsConnected() && !user.PhoneRecentlySeen(true) {
  174. go user.sendPhoneOfflineWarning()
  175. }
  176. }
  177. br.usersLock.Unlock()
  178. }
  179. func (br *WABridge) StartUsers() {
  180. br.Log.Debugln("Starting users")
  181. foundAnySessions := false
  182. for _, user := range br.GetAllUsers() {
  183. if !user.JID.IsEmpty() {
  184. foundAnySessions = true
  185. }
  186. go user.Connect()
  187. }
  188. if !foundAnySessions {
  189. br.SendGlobalBridgeState(status.BridgeState{StateEvent: status.StateUnconfigured}.Fill(nil))
  190. }
  191. br.Log.Debugln("Starting custom puppets")
  192. for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() {
  193. go func(puppet *Puppet) {
  194. puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID)
  195. err := puppet.StartCustomMXID(true)
  196. if err != nil {
  197. puppet.log.Errorln("Failed to start custom puppet:", err)
  198. }
  199. }(loopuppet)
  200. }
  201. }
  202. func (br *WABridge) Stop() {
  203. br.Metrics.Stop()
  204. for _, user := range br.usersByUsername {
  205. if user.Client == nil {
  206. continue
  207. }
  208. br.Log.Debugln("Disconnecting", user.MXID)
  209. user.Client.Disconnect()
  210. close(user.historySyncs)
  211. }
  212. }
  213. func (br *WABridge) GetExampleConfig() string {
  214. return ExampleConfig
  215. }
  216. func (br *WABridge) GetConfigPtr() interface{} {
  217. br.Config = &config.Config{
  218. BaseConfig: &br.Bridge.Config,
  219. }
  220. br.Config.BaseConfig.Bridge = &br.Config.Bridge
  221. return br.Config
  222. }
  223. const unstableFeatureBatchSending = "org.matrix.msc2716"
  224. func (br *WABridge) CheckFeatures(versions *mautrix.RespVersions) (string, bool) {
  225. if br.Config.Bridge.HistorySync.Backfill {
  226. supported, known := versions.UnstableFeatures[unstableFeatureBatchSending]
  227. if !known {
  228. return "Backfilling is enabled in bridge config, but homeserver does not support MSC2716 batch sending", false
  229. } else if !supported {
  230. return "Backfilling is enabled in bridge config, but MSC2716 batch sending is not enabled on homeserver", false
  231. }
  232. }
  233. return "", true
  234. }
  235. func main() {
  236. br := &WABridge{
  237. usersByMXID: make(map[id.UserID]*User),
  238. usersByUsername: make(map[string]*User),
  239. spaceRooms: make(map[id.RoomID]*User),
  240. managementRooms: make(map[id.RoomID]*User),
  241. portalsByMXID: make(map[id.RoomID]*Portal),
  242. portalsByJID: make(map[database.PortalKey]*Portal),
  243. puppets: make(map[types.JID]*Puppet),
  244. puppetsByCustomMXID: make(map[id.UserID]*Puppet),
  245. }
  246. br.Bridge = bridge.Bridge{
  247. Name: "mautrix-whatsapp",
  248. URL: "https://github.com/mautrix/whatsapp",
  249. Description: "A Matrix-WhatsApp puppeting bridge.",
  250. Version: "0.8.1",
  251. ProtocolName: "WhatsApp",
  252. CryptoPickleKey: "maunium.net/go/mautrix-whatsapp",
  253. ConfigUpgrader: &configupgrade.StructUpgrader{
  254. SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
  255. Blocks: config.SpacedBlocks,
  256. Base: ExampleConfig,
  257. },
  258. Child: br,
  259. }
  260. br.InitVersion(Tag, Commit, BuildTime)
  261. br.WAVersion = strings.FieldsFunc(br.Version, func(r rune) bool { return r == '-' || r == '+' })[0]
  262. br.Main()
  263. }