custompuppet.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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. "go.mau.fi/whatsmeow/types"
  25. "maunium.net/go/mautrix"
  26. "maunium.net/go/mautrix/appservice"
  27. "maunium.net/go/mautrix/event"
  28. "maunium.net/go/mautrix/id"
  29. )
  30. var (
  31. ErrNoCustomMXID = errors.New("no custom mxid set")
  32. ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
  33. )
  34. func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
  35. prevCustomMXID := puppet.CustomMXID
  36. if puppet.customIntent != nil {
  37. puppet.stopSyncing()
  38. }
  39. puppet.CustomMXID = mxid
  40. puppet.AccessToken = accessToken
  41. err := puppet.StartCustomMXID(false)
  42. if err != nil {
  43. return err
  44. }
  45. if len(prevCustomMXID) > 0 {
  46. delete(puppet.bridge.puppetsByCustomMXID, prevCustomMXID)
  47. }
  48. if len(puppet.CustomMXID) > 0 {
  49. puppet.bridge.puppetsByCustomMXID[puppet.CustomMXID] = puppet
  50. }
  51. puppet.EnablePresence = puppet.bridge.Config.Bridge.DefaultBridgePresence
  52. puppet.EnableReceipts = puppet.bridge.Config.Bridge.DefaultBridgeReceipts
  53. puppet.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID)
  54. puppet.Update()
  55. // TODO leave rooms with default puppet
  56. return nil
  57. }
  58. func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
  59. _, homeserver, _ := mxid.Parse()
  60. puppet.log.Debugfln("Logging into %s with shared secret", mxid)
  61. mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]))
  62. mac.Write([]byte(mxid))
  63. client, err := puppet.bridge.newDoublePuppetClient(mxid, "")
  64. if err != nil {
  65. return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
  66. }
  67. resp, err := client.Login(&mautrix.ReqLogin{
  68. Type: mautrix.AuthTypePassword,
  69. Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
  70. Password: hex.EncodeToString(mac.Sum(nil)),
  71. DeviceID: "WhatsApp Bridge",
  72. InitialDeviceDisplayName: "WhatsApp Bridge",
  73. })
  74. if err != nil {
  75. return "", err
  76. }
  77. return resp.AccessToken, nil
  78. }
  79. func (bridge *Bridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
  80. _, homeserver, err := mxid.Parse()
  81. if err != nil {
  82. return nil, err
  83. }
  84. homeserverURL, found := bridge.Config.Bridge.DoublePuppetServerMap[homeserver]
  85. if !found {
  86. if homeserver == bridge.AS.HomeserverDomain {
  87. homeserverURL = bridge.AS.HomeserverURL
  88. } else if bridge.Config.Bridge.DoublePuppetAllowDiscovery {
  89. resp, err := mautrix.DiscoverClientAPI(homeserver)
  90. if err != nil {
  91. return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err)
  92. }
  93. homeserverURL = resp.Homeserver.BaseURL
  94. bridge.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid)
  95. } else {
  96. return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
  97. }
  98. }
  99. client, err := mautrix.NewClient(homeserverURL, mxid, accessToken)
  100. if err != nil {
  101. return nil, err
  102. }
  103. client.Logger = bridge.AS.Log.Sub(mxid.String())
  104. client.Client = bridge.AS.HTTPClient
  105. client.DefaultHTTPRetries = bridge.AS.DefaultHTTPRetries
  106. return client, nil
  107. }
  108. func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
  109. if len(puppet.CustomMXID) == 0 {
  110. return nil, ErrNoCustomMXID
  111. }
  112. client, err := puppet.bridge.newDoublePuppetClient(puppet.CustomMXID, puppet.AccessToken)
  113. if err != nil {
  114. return nil, err
  115. }
  116. client.Syncer = puppet
  117. client.Store = puppet
  118. ia := puppet.bridge.AS.NewIntentAPI("custom")
  119. ia.Client = client
  120. ia.Localpart, _, _ = puppet.CustomMXID.Parse()
  121. ia.UserID = puppet.CustomMXID
  122. ia.IsCustomPuppet = true
  123. return ia, nil
  124. }
  125. func (puppet *Puppet) clearCustomMXID() {
  126. puppet.CustomMXID = ""
  127. puppet.AccessToken = ""
  128. puppet.customIntent = nil
  129. puppet.customTypingIn = nil
  130. puppet.customUser = nil
  131. }
  132. func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error {
  133. if len(puppet.CustomMXID) == 0 {
  134. puppet.clearCustomMXID()
  135. return nil
  136. }
  137. intent, err := puppet.newCustomIntent()
  138. if err != nil {
  139. puppet.clearCustomMXID()
  140. return err
  141. }
  142. resp, err := intent.Whoami()
  143. if err != nil {
  144. if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) {
  145. puppet.clearCustomMXID()
  146. return err
  147. }
  148. intent.AccessToken = puppet.AccessToken
  149. } else if resp.UserID != puppet.CustomMXID {
  150. puppet.clearCustomMXID()
  151. return ErrMismatchingMXID
  152. }
  153. puppet.customIntent = intent
  154. puppet.customTypingIn = make(map[id.RoomID]bool)
  155. puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
  156. puppet.startSyncing()
  157. return nil
  158. }
  159. func (puppet *Puppet) startSyncing() {
  160. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  161. return
  162. }
  163. go func() {
  164. puppet.log.Debugln("Starting syncing...")
  165. puppet.customIntent.SyncPresence = "offline"
  166. err := puppet.customIntent.Sync()
  167. if err != nil {
  168. puppet.log.Errorln("Fatal error syncing:", err)
  169. }
  170. }()
  171. }
  172. func (puppet *Puppet) stopSyncing() {
  173. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  174. return
  175. }
  176. puppet.customIntent.StopSync()
  177. }
  178. func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, _ string) error {
  179. if !puppet.customUser.IsLoggedIn() {
  180. puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
  181. return nil
  182. }
  183. for roomID, events := range resp.Rooms.Join {
  184. portal := puppet.bridge.GetPortalByMXID(roomID)
  185. if portal == nil || portal.IsBroadcastList() {
  186. continue
  187. }
  188. for _, evt := range events.Ephemeral.Events {
  189. err := evt.Content.ParseRaw(evt.Type)
  190. if err != nil {
  191. continue
  192. }
  193. switch evt.Type {
  194. case event.EphemeralEventReceipt:
  195. if puppet.EnableReceipts {
  196. go puppet.handleReceiptEvent(portal, evt)
  197. }
  198. case event.EphemeralEventTyping:
  199. go puppet.handleTypingEvent(portal, evt)
  200. }
  201. }
  202. }
  203. if puppet.EnablePresence {
  204. for _, evt := range resp.Presence.Events {
  205. if evt.Sender != puppet.CustomMXID {
  206. continue
  207. }
  208. err := evt.Content.ParseRaw(evt.Type)
  209. if err != nil {
  210. continue
  211. }
  212. go puppet.handlePresenceEvent(evt)
  213. }
  214. }
  215. return nil
  216. }
  217. func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
  218. presence := types.PresenceAvailable
  219. if event.Content.Raw["presence"].(string) != "online" {
  220. presence = types.PresenceUnavailable
  221. puppet.customUser.log.Debugln("Marking offline")
  222. } else {
  223. puppet.customUser.log.Debugln("Marking online")
  224. }
  225. err := puppet.customUser.Client.SendPresence(presence)
  226. if err != nil {
  227. puppet.customUser.log.Warnln("Failed to set presence:", err)
  228. }
  229. }
  230. func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
  231. for eventID, receipts := range *event.Content.AsReceipt() {
  232. if receipt, ok := receipts.Read[puppet.CustomMXID]; !ok {
  233. // Ignore receipt events where this user isn't present.
  234. } else if isDoublePuppeted, _ := receipt.Extra[doublePuppetField].(bool); isDoublePuppeted {
  235. puppet.customUser.log.Debugfln("Ignoring double puppeted read receipt %+v", event.Content.Raw)
  236. // Ignore double puppeted read receipts.
  237. } else if message := puppet.bridge.DB.Message.GetByMXID(eventID); message != nil {
  238. puppet.customUser.log.Debugfln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
  239. err := puppet.customUser.Client.MarkRead([]types.MessageID{message.JID}, time.UnixMilli(receipt.Timestamp), portal.Key.JID, message.Sender)
  240. if err != nil {
  241. puppet.customUser.log.Warnln("Error marking read:", err)
  242. }
  243. }
  244. }
  245. }
  246. func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
  247. isTyping := false
  248. for _, userID := range evt.Content.AsTyping().UserIDs {
  249. if userID == puppet.CustomMXID {
  250. isTyping = true
  251. break
  252. }
  253. }
  254. if puppet.customTypingIn[evt.RoomID] != isTyping {
  255. puppet.customTypingIn[evt.RoomID] = isTyping
  256. presence := types.ChatPresenceComposing
  257. if !isTyping {
  258. puppet.customUser.log.Debugfln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
  259. presence = types.ChatPresencePaused
  260. } else {
  261. puppet.customUser.log.Debugfln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
  262. }
  263. err := puppet.customUser.Client.SendChatPresence(presence, portal.Key.JID)
  264. if err != nil {
  265. puppet.customUser.log.Warnln("Error setting typing:", err)
  266. }
  267. }
  268. }
  269. func (puppet *Puppet) tryRelogin(cause error, action string) bool {
  270. if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) {
  271. return false
  272. }
  273. puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action)
  274. accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID)
  275. if err != nil {
  276. puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err)
  277. return false
  278. }
  279. puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action)
  280. puppet.AccessToken = accessToken
  281. return true
  282. }
  283. func (puppet *Puppet) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) {
  284. puppet.log.Warnln("Sync error:", err)
  285. if errors.Is(err, mautrix.MUnknownToken) {
  286. if !puppet.tryRelogin(err, "syncing") {
  287. return 0, err
  288. }
  289. puppet.customIntent.AccessToken = puppet.AccessToken
  290. return 0, nil
  291. }
  292. return 10 * time.Second, nil
  293. }
  294. func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
  295. everything := []event.Type{{Type: "*"}}
  296. return &mautrix.Filter{
  297. Presence: mautrix.FilterPart{
  298. Senders: []id.UserID{puppet.CustomMXID},
  299. Types: []event.Type{event.EphemeralEventPresence},
  300. },
  301. AccountData: mautrix.FilterPart{NotTypes: everything},
  302. Room: mautrix.RoomFilter{
  303. Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
  304. IncludeLeave: false,
  305. AccountData: mautrix.FilterPart{NotTypes: everything},
  306. State: mautrix.FilterPart{NotTypes: everything},
  307. Timeline: mautrix.FilterPart{NotTypes: everything},
  308. },
  309. }
  310. }
  311. func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {}
  312. func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() }
  313. func (puppet *Puppet) SaveRoom(_ *mautrix.Room) {}
  314. func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" }
  315. func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch }
  316. func (puppet *Puppet) LoadRoom(_ id.RoomID) *mautrix.Room { return nil }