main.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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. "go.mau.fi/whatsmeow"
  26. waProto "go.mau.fi/whatsmeow/binary/proto"
  27. "go.mau.fi/whatsmeow/store"
  28. "go.mau.fi/whatsmeow/store/sqlstore"
  29. "go.mau.fi/whatsmeow/types"
  30. "google.golang.org/protobuf/proto"
  31. "maunium.net/go/mautrix/bridge"
  32. "maunium.net/go/mautrix/id"
  33. "maunium.net/go/mautrix/util/configupgrade"
  34. "maunium.net/go/mautrix-whatsapp/config"
  35. "maunium.net/go/mautrix-whatsapp/database"
  36. )
  37. // Information to find out exactly which commit the bridge was built from.
  38. // These are filled at build time with the -X linker flag.
  39. var (
  40. Tag = "unknown"
  41. Commit = "unknown"
  42. BuildTime = "unknown"
  43. )
  44. //go:embed example-config.yaml
  45. var ExampleConfig string
  46. type WABridge struct {
  47. bridge.Bridge
  48. MatrixHandler *MatrixHandler
  49. Config *config.Config
  50. DB *database.Database
  51. Provisioning *ProvisioningAPI
  52. Formatter *Formatter
  53. Metrics *MetricsHandler
  54. WAContainer *sqlstore.Container
  55. WAVersion string
  56. usersByMXID map[id.UserID]*User
  57. usersByUsername map[string]*User
  58. usersLock sync.Mutex
  59. spaceRooms map[id.RoomID]*User
  60. spaceRoomsLock sync.Mutex
  61. managementRooms map[id.RoomID]*User
  62. managementRoomsLock sync.Mutex
  63. portalsByMXID map[id.RoomID]*Portal
  64. portalsByJID map[database.PortalKey]*Portal
  65. portalsLock sync.Mutex
  66. puppets map[types.JID]*Puppet
  67. puppetsByCustomMXID map[id.UserID]*Puppet
  68. puppetsLock sync.Mutex
  69. }
  70. func (br *WABridge) Init() {
  71. Segment.log = br.Log.Sub("Segment")
  72. Segment.key = br.Config.SegmentKey
  73. if Segment.IsEnabled() {
  74. Segment.log.Infoln("Segment metrics are enabled")
  75. }
  76. br.DB = database.New(br.Bridge.DB)
  77. br.WAContainer = sqlstore.NewWithDB(br.DB.DB, br.DB.Dialect.String(), nil)
  78. br.WAContainer.DatabaseErrorHandler = br.DB.HandleSignalStoreError
  79. ss := br.Config.Bridge.Provisioning.SharedSecret
  80. if len(ss) > 0 && ss != "disable" {
  81. br.Provisioning = &ProvisioningAPI{bridge: br}
  82. }
  83. br.Log.Debugln("Initializing Matrix event handler")
  84. br.MatrixHandler = NewMatrixHandler(br)
  85. br.Formatter = NewFormatter(br)
  86. br.Metrics = NewMetricsHandler(br.Config.Metrics.Listen, br.Log.Sub("Metrics"), br.DB)
  87. store.BaseClientPayload.UserAgent.OsVersion = proto.String(br.WAVersion)
  88. store.BaseClientPayload.UserAgent.OsBuildNumber = proto.String(br.WAVersion)
  89. store.CompanionProps.Os = proto.String(br.Config.WhatsApp.OSName)
  90. store.CompanionProps.RequireFullSync = proto.Bool(br.Config.Bridge.HistorySync.RequestFullSync)
  91. versionParts := strings.Split(br.WAVersion, ".")
  92. if len(versionParts) > 2 {
  93. primary, _ := strconv.Atoi(versionParts[0])
  94. secondary, _ := strconv.Atoi(versionParts[1])
  95. tertiary, _ := strconv.Atoi(versionParts[2])
  96. store.CompanionProps.Version.Primary = proto.Uint32(uint32(primary))
  97. store.CompanionProps.Version.Secondary = proto.Uint32(uint32(secondary))
  98. store.CompanionProps.Version.Tertiary = proto.Uint32(uint32(tertiary))
  99. }
  100. platformID, ok := waProto.CompanionProps_CompanionPropsPlatformType_value[strings.ToUpper(br.Config.WhatsApp.BrowserName)]
  101. if ok {
  102. store.CompanionProps.PlatformType = waProto.CompanionProps_CompanionPropsPlatformType(platformID).Enum()
  103. }
  104. }
  105. func (br *WABridge) Start() {
  106. err := br.WAContainer.Upgrade()
  107. if err != nil {
  108. br.Log.Fatalln("Failed to upgrade whatsmeow database: %v", err)
  109. os.Exit(15)
  110. }
  111. if br.Provisioning != nil {
  112. br.Log.Debugln("Initializing provisioning API")
  113. br.Provisioning.Init()
  114. }
  115. go br.CheckWhatsAppUpdate()
  116. go br.StartUsers()
  117. if br.Config.Metrics.Enabled {
  118. go br.Metrics.Start()
  119. }
  120. if br.Config.Bridge.ResendBridgeInfo {
  121. go br.ResendBridgeInfo()
  122. }
  123. go br.Loop()
  124. }
  125. func (br *WABridge) CheckWhatsAppUpdate() {
  126. br.Log.Debugfln("Checking for WhatsApp web update")
  127. resp, err := whatsmeow.CheckUpdate(http.DefaultClient)
  128. if err != nil {
  129. br.Log.Warnfln("Failed to check for WhatsApp web update: %v", err)
  130. return
  131. }
  132. if store.GetWAVersion() == resp.ParsedVersion {
  133. br.Log.Debugfln("Bridge is using latest WhatsApp web protocol")
  134. } else if store.GetWAVersion().LessThan(resp.ParsedVersion) {
  135. if resp.IsBelowHard || resp.IsBroken {
  136. br.Log.Warnfln("Bridge is using outdated WhatsApp web protocol and probably doesn't work anymore (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  137. } else if resp.IsBelowSoft {
  138. br.Log.Infofln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  139. } else {
  140. br.Log.Debugfln("Bridge is using outdated WhatsApp web protocol (%s, latest is %s)", store.GetWAVersion(), resp.ParsedVersion)
  141. }
  142. } else {
  143. br.Log.Debugfln("Bridge is using newer than latest WhatsApp web protocol")
  144. }
  145. }
  146. func (br *WABridge) Loop() {
  147. for {
  148. br.SleepAndDeleteUpcoming()
  149. time.Sleep(1 * time.Hour)
  150. br.WarnUsersAboutDisconnection()
  151. }
  152. }
  153. func (br *WABridge) WarnUsersAboutDisconnection() {
  154. br.usersLock.Lock()
  155. for _, user := range br.usersByUsername {
  156. if user.IsConnected() && !user.PhoneRecentlySeen(true) {
  157. go user.sendPhoneOfflineWarning()
  158. }
  159. }
  160. br.usersLock.Unlock()
  161. }
  162. func (br *WABridge) ResendBridgeInfo() {
  163. // FIXME
  164. //if *dontSaveConfig {
  165. // br.Log.Warnln("Not setting resend_bridge_info to false in config due to --no-update flag")
  166. //} else {
  167. // err := config.Mutate(*configPath, func(helper *configupgrade.Helper) {
  168. // helper.Set(configupgrade.Bool, "false", "bridge", "resend_bridge_info")
  169. // })
  170. // if err != nil {
  171. // br.Log.Errorln("Failed to save config after setting resend_bridge_info to false:", err)
  172. // }
  173. //}
  174. //br.Log.Infoln("Re-sending bridge info state event to all portals")
  175. //for _, portal := range br.GetAllPortals() {
  176. // portal.UpdateBridgeInfo()
  177. //}
  178. //br.Log.Infoln("Finished re-sending bridge info state events")
  179. }
  180. func (br *WABridge) StartUsers() {
  181. br.Log.Debugln("Starting users")
  182. foundAnySessions := false
  183. for _, user := range br.GetAllUsers() {
  184. if !user.JID.IsEmpty() {
  185. foundAnySessions = true
  186. }
  187. go user.Connect()
  188. }
  189. if !foundAnySessions {
  190. br.sendGlobalBridgeState(BridgeState{StateEvent: StateUnconfigured}.fill(nil))
  191. }
  192. br.Log.Debugln("Starting custom puppets")
  193. for _, loopuppet := range br.GetAllPuppetsWithCustomMXID() {
  194. go func(puppet *Puppet) {
  195. puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID)
  196. err := puppet.StartCustomMXID(true)
  197. if err != nil {
  198. puppet.log.Errorln("Failed to start custom puppet:", err)
  199. }
  200. }(loopuppet)
  201. }
  202. }
  203. func (br *WABridge) Stop() {
  204. if br.Crypto != nil {
  205. br.Crypto.Stop()
  206. }
  207. br.AS.Stop()
  208. br.Metrics.Stop()
  209. br.EventProcessor.Stop()
  210. for _, user := range br.usersByUsername {
  211. if user.Client == nil {
  212. continue
  213. }
  214. br.Log.Debugln("Disconnecting", user.MXID)
  215. user.Client.Disconnect()
  216. close(user.historySyncs)
  217. }
  218. }
  219. func (br *WABridge) GetExampleConfig() string {
  220. return ExampleConfig
  221. }
  222. func (br *WABridge) GetConfigPtr() interface{} {
  223. br.Config = &config.Config{
  224. BaseConfig: &br.Bridge.Config,
  225. }
  226. br.Config.BaseConfig.Bridge = &br.Config.Bridge
  227. return br.Config
  228. }
  229. func main() {
  230. br := &WABridge{
  231. usersByMXID: make(map[id.UserID]*User),
  232. usersByUsername: make(map[string]*User),
  233. spaceRooms: make(map[id.RoomID]*User),
  234. managementRooms: make(map[id.RoomID]*User),
  235. portalsByMXID: make(map[id.RoomID]*Portal),
  236. portalsByJID: make(map[database.PortalKey]*Portal),
  237. puppets: make(map[types.JID]*Puppet),
  238. puppetsByCustomMXID: make(map[id.UserID]*Puppet),
  239. }
  240. br.Bridge = bridge.Bridge{
  241. Name: "mautrix-whatsapp",
  242. URL: "https://github.com/mautrix/whatsapp",
  243. Description: "A Matrix-WhatsApp puppeting bridge.",
  244. Version: "0.4.0",
  245. ProtocolName: "WhatsApp",
  246. ConfigUpgrader: &configupgrade.StructUpgrader{
  247. SimpleUpgrader: configupgrade.SimpleUpgrader(config.DoUpgrade),
  248. Blocks: config.SpacedBlocks,
  249. Base: ExampleConfig,
  250. },
  251. Child: br,
  252. }
  253. br.InitVersion(Tag, Commit, BuildTime)
  254. br.WAVersion = strings.FieldsFunc(br.Version, func(r rune) bool { return r == '-' || r == '+' })[0]
  255. br.Main()
  256. }