custompuppet.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
  2. // Copyright (C) 2021 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. "crypto/hmac"
  19. "crypto/sha512"
  20. "encoding/hex"
  21. "errors"
  22. "fmt"
  23. "time"
  24. "maunium.net/go/mautrix"
  25. "maunium.net/go/mautrix/appservice"
  26. "maunium.net/go/mautrix/event"
  27. "maunium.net/go/mautrix/id"
  28. )
  29. var (
  30. ErrNoCustomMXID = errors.New("no custom mxid set")
  31. ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
  32. )
  33. func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
  34. prevCustomMXID := puppet.CustomMXID
  35. if puppet.customIntent != nil {
  36. puppet.stopSyncing()
  37. }
  38. puppet.CustomMXID = mxid
  39. puppet.AccessToken = accessToken
  40. err := puppet.StartCustomMXID(false)
  41. if err != nil {
  42. return err
  43. }
  44. if len(prevCustomMXID) > 0 {
  45. delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID)
  46. }
  47. if len(puppet.CustomMXID) > 0 {
  48. puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
  49. }
  50. puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
  51. puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts
  52. puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID)
  53. puppet.Update()
  54. // TODO leave rooms with default puppet
  55. return nil
  56. }
  57. func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
  58. _, homeserver, _ := mxid.Parse()
  59. puppet.log.Debugfln("Logging into %s with shared secret", mxid)
  60. loginSecret := puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
  61. client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
  62. if err != nil {
  63. return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
  64. }
  65. req := mautrix.ReqLogin{
  66. Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
  67. DeviceID: "WhatsApp Bridge",
  68. InitialDeviceDisplayName: "WhatsApp Bridge",
  69. }
  70. if loginSecret == "appservice" {
  71. client.AccessToken = puppet.bridge.AS.Registration.AppToken
  72. req.Type = mautrix.AuthTypeAppservice
  73. } else {
  74. mac := hmac.New(sha512.New, []byte(loginSecret))
  75. mac.Write([]byte(mxid))
  76. req.Password = hex.EncodeToString(mac.Sum(nil))
  77. req.Type = mautrix.AuthTypePassword
  78. }
  79. resp, err := client.Login(&req)
  80. if err != nil {
  81. return "", err
  82. }
  83. return resp.AccessToken, nil
  84. }
  85. func (br *WABridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
  86. _, homeserver, err := mxid.Parse()
  87. if err != nil {
  88. return nil, err
  89. }
  90. homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
  91. if !found {
  92. if homeserver == br.AS.HomeserverDomain {
  93. homeserverURL = br.AS.HomeserverURL
  94. } else if br.Config.Bridge.DoublePuppetAllowDiscovery {
  95. resp, err := mautrix.DiscoverClientAPI(homeserver)
  96. if err != nil {
  97. return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
  98. }
  99. homeserverURL = resp.Homeserver.BaseURL
  100. br.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid)
  101. } else {
  102. return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
  103. }
  104. }
  105. client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
  106. if err != nil {
  107. return nil, err
  108. }
  109. client.Log = br.AS.Log.With().Str("as_user_id", mxid.String()).Logger()
  110. client.StateStore = br.AS.StateStore
  111. client.Client = br.AS.HTTPClient
  112. client.DefaultHTTPRetries = br.AS.DefaultHTTPRetries
  113. return client, nil
  114. }
  115. func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
  116. if len(puppet.CustomMXID) == 0 {
  117. return nil, ErrNoCustomMXID
  118. }
  119. client, err := puppet.bridge.newDoublePuppetClient(puppet.CustomMXID, puppet.AccessToken)
  120. if err != nil {
  121. return nil, err
  122. }
  123. client.Syncer = puppet
  124. client.Store = puppet
  125. ia := puppet.bridge.AS.NewIntentAPI("custom")
  126. ia.Client = client
  127. ia.Localpart, _, _ = puppet.CustomMXID.Parse()
  128. ia.UserID = puppet.CustomMXID
  129. ia.IsCustomPuppet = true
  130. return ia, nil
  131. }
  132. func (puppet *Puppet) clearCustomMXID() {
  133. puppet.CustomMXID = ""
  134. puppet.AccessToken = ""
  135. puppet.customIntent = nil
  136. puppet.customUser = nil
  137. }
  138. func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
  139. if len(puppet.CustomMXID) == 0 {
  140. puppet.clearCustomMXID()
  141. return nil
  142. }
  143. intent, err := puppet.newCustomIntent()
  144. if err != nil {
  145. puppet.clearCustomMXID()
  146. return err
  147. }
  148. resp, err := intent.Whoami()
  149. if err != nil {
  150. if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) {
  151. puppet.clearCustomMXID()
  152. return err
  153. }
  154. intent.AccessToken = puppet.AccessToken
  155. } else if resp.UserID != puppet.CustomMXID {
  156. puppet.clearCustomMXID()
  157. return ErrMismatchingMXID
  158. }
  159. puppet.customIntent = intent
  160. puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
  161. puppet.startSyncing()
  162. return nil
  163. }
  164. func (puppet *Puppet) startSyncing() {
  165. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  166. return
  167. }
  168. go func() {
  169. puppet.log.Debugln("Starting syncing...")
  170. puppet.customIntent.SyncPresence = "offline"
  171. err := puppet.customIntent.Sync()
  172. if err != nil {
  173. puppet.log.Errorln("Fatal error syncing:", err)
  174. }
  175. }()
  176. }
  177. func (puppet *Puppet) stopSyncing() {
  178. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  179. return
  180. }
  181. puppet.customIntent.StopSync()
  182. }
  183. func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
  184. if !puppet.customUser.IsLoggedIn() {
  185. puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
  186. return nil
  187. }
  188. for roomID, events := range resp.Rooms.Join {
  189. for _, evt := range events.Ephemeral.Events {
  190. evt.RoomID = roomID
  191. err := evt.Content.ParseRaw(evt.Type)
  192. if err != nil {
  193. continue
  194. }
  195. switch evt.Type {
  196. case event.EphemeralEventReceipt:
  197. if puppet.EnableReceipts {
  198. go puppet.bridge.MatrixHandler.HandleReceipt(evt)
  199. }
  200. case event.EphemeralEventTyping:
  201. go puppet.bridge.MatrixHandler.HandleTyping(evt)
  202. }
  203. }
  204. }
  205. if puppet.EnablePresence {
  206. for _, evt := range resp.Presence.Events {
  207. if evt.Sender != puppet.CustomMXID {
  208. continue
  209. }
  210. err := evt.Content.ParseRaw(evt.Type)
  211. if err != nil {
  212. continue
  213. }
  214. go puppet.bridge.HandlePresence(evt)
  215. }
  216. }
  217. return nil
  218. }
  219. func (puppet *Puppet) tryRelogin(cause error, action string) bool {
  220. if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
  221. return false
  222. }
  223. puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
  224. accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
  225. if err != nil {
  226. puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
  227. return false
  228. }
  229. puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
  230. puppet.AccessToken = accessToken
  231. return true
  232. }
  233. func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
  234. puppet.log.Warnln("Sync error:", err)
  235. if errors.Is(err, mautrix.MUnknownToken) {
  236. if !puppet.tryRelogin(err, "syncing") {
  237. return 0, err
  238. }
  239. puppet.customIntent.AccessToken = puppet.AccessToken
  240. return 0, nil
  241. }
  242. return 10 * time.Second, nil
  243. }
  244. func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
  245. everything := []event.Type{{Type: "*"}}
  246. return &mautrix.Filter{
  247. Presence: mautrix.FilterPart{
  248. Senders: []id.UserID{puppet.CustomMXID},
  249. Types: []event.Type{event.EphemeralEventPresence},
  250. },
  251. AccountData: mautrix.FilterPart{NotTypes: everything},
  252. Room: mautrix.RoomFilter{
  253. Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
  254. IncludeLeave: false,
  255. AccountData: mautrix.FilterPart{NotTypes: everything},
  256. State: mautrix.FilterPart{NotTypes: everything},
  257. Timeline: mautrix.FilterPart{NotTypes: everything},
  258. },
  259. }
  260. }
  261. func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {}
  262. func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() }
  263. func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {}
  264. func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" }
  265. func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch }
  266. func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil }