custompuppet.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
  2. // Copyright (C) 2020 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. "time"
  22. "github.com/pkg/errors"
  23. "github.com/Rhymen/go-whatsapp"
  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()
  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.bridge.AS.StateStore.MarkRegistered(puppet.CustomMXID)
  51. puppet.Update()
  52. // TODO leave rooms with default puppet
  53. return nil
  54. }
  55. func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
  56. mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret))
  57. mac.Write([]byte(mxid))
  58. resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{
  59. Type: "m.login.password",
  60. Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)},
  61. Password: hex.EncodeToString(mac.Sum(nil)),
  62. DeviceID: "WhatsApp Bridge",
  63. InitialDeviceDisplayName: "WhatsApp Bridge",
  64. })
  65. if err != nil {
  66. return "", err
  67. }
  68. return resp.AccessToken, nil
  69. }
  70. func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
  71. if len(puppet.CustomMXID) == 0 {
  72. return nil, ErrNoCustomMXID
  73. }
  74. client, err := mautrix.NewClient(puppet.bridge.AS.HomeserverURL, puppet.CustomMXID, puppet.AccessToken)
  75. if err != nil {
  76. return nil, err
  77. }
  78. client.Logger = puppet.bridge.AS.Log.Sub(string(puppet.CustomMXID))
  79. client.Syncer = puppet
  80. client.Store = puppet
  81. ia := puppet.bridge.AS.NewIntentAPI("custom")
  82. ia.Client = client
  83. ia.Localpart, _, _ = puppet.CustomMXID.Parse()
  84. ia.UserID = puppet.CustomMXID
  85. ia.IsCustomPuppet = true
  86. return ia, nil
  87. }
  88. func (puppet *Puppet) clearCustomMXID() {
  89. puppet.CustomMXID = ""
  90. puppet.AccessToken = ""
  91. puppet.customIntent = nil
  92. puppet.customTypingIn = nil
  93. puppet.customUser = nil
  94. }
  95. func (puppet *Puppet) StartCustomMXID() error {
  96. if len(puppet.CustomMXID) == 0 {
  97. puppet.clearCustomMXID()
  98. return nil
  99. }
  100. intent, err := puppet.newCustomIntent()
  101. if err != nil {
  102. puppet.clearCustomMXID()
  103. return err
  104. }
  105. resp, err := intent.Whoami()
  106. if err != nil {
  107. puppet.clearCustomMXID()
  108. return err
  109. }
  110. if resp.UserID != puppet.CustomMXID {
  111. puppet.clearCustomMXID()
  112. return ErrMismatchingMXID
  113. }
  114. puppet.customIntent = intent
  115. puppet.customTypingIn = make(map[id.RoomID]bool)
  116. puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
  117. puppet.startSyncing()
  118. return nil
  119. }
  120. func (puppet *Puppet) startSyncing() {
  121. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  122. return
  123. }
  124. go func() {
  125. puppet.log.Debugln("Starting syncing...")
  126. puppet.customIntent.SyncPresence = "offline"
  127. err := puppet.customIntent.Sync()
  128. if err != nil {
  129. puppet.log.Errorln("Fatal error syncing:", err)
  130. }
  131. }()
  132. }
  133. func (puppet *Puppet) stopSyncing() {
  134. if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets {
  135. return
  136. }
  137. puppet.customIntent.StopSync()
  138. }
  139. func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error {
  140. if !puppet.customUser.IsConnected() {
  141. puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
  142. return nil
  143. }
  144. for roomID, events := range resp.Rooms.Join {
  145. portal := puppet.bridge.GetPortalByMXID(roomID)
  146. if portal == nil {
  147. continue
  148. }
  149. for _, evt := range events.Ephemeral.Events {
  150. err := evt.Content.ParseRaw(evt.Type)
  151. if err != nil {
  152. continue
  153. }
  154. switch evt.Type {
  155. case event.EphemeralEventReceipt:
  156. go puppet.handleReceiptEvent(portal, evt)
  157. case event.EphemeralEventTyping:
  158. go puppet.handleTypingEvent(portal, evt)
  159. }
  160. }
  161. }
  162. for _, evt := range resp.Presence.Events {
  163. if evt.Sender != puppet.CustomMXID {
  164. continue
  165. }
  166. err := evt.Content.ParseRaw(evt.Type)
  167. if err != nil {
  168. continue
  169. }
  170. go puppet.handlePresenceEvent(evt)
  171. }
  172. return nil
  173. }
  174. func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
  175. presence := whatsapp.PresenceAvailable
  176. if event.Content.Raw["presence"].(string) != "online" {
  177. presence = whatsapp.PresenceUnavailable
  178. puppet.customUser.log.Debugln("Marking offline")
  179. } else {
  180. puppet.customUser.log.Debugln("Marking online")
  181. }
  182. _, err := puppet.customUser.Conn.Presence("", presence)
  183. if err != nil {
  184. puppet.customUser.log.Warnln("Failed to set presence:", err)
  185. }
  186. }
  187. func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
  188. for eventID, receipts := range *event.Content.AsReceipt() {
  189. if _, ok := receipts.Read[puppet.CustomMXID]; !ok {
  190. continue
  191. }
  192. message := puppet.bridge.DB.Message.GetByMXID(eventID)
  193. if message == nil {
  194. continue
  195. }
  196. puppet.customUser.log.Infofln("Marking %s/%s in %s/%s as read", message.JID, message.MXID, portal.Key.JID, portal.MXID)
  197. _, err := puppet.customUser.Conn.Read(portal.Key.JID, message.JID)
  198. if err != nil {
  199. puppet.customUser.log.Warnln("Error marking read:", err)
  200. }
  201. }
  202. }
  203. func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
  204. isTyping := false
  205. for _, userID := range evt.Content.AsTyping().UserIDs {
  206. if userID == puppet.CustomMXID {
  207. isTyping = true
  208. break
  209. }
  210. }
  211. if puppet.customTypingIn[evt.RoomID] != isTyping {
  212. puppet.customTypingIn[evt.RoomID] = isTyping
  213. presence := whatsapp.PresenceComposing
  214. if !isTyping {
  215. puppet.customUser.log.Infofln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
  216. presence = whatsapp.PresencePaused
  217. } else {
  218. puppet.customUser.log.Infofln("Marking typing in %s/%s", portal.Key.JID, portal.MXID)
  219. }
  220. _, err := puppet.customUser.Conn.Presence(portal.Key.JID, presence)
  221. if err != nil {
  222. puppet.customUser.log.Warnln("Error setting typing:", err)
  223. }
  224. }
  225. }
  226. func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) {
  227. puppet.log.Warnln("Sync error:", err)
  228. return 10 * time.Second, nil
  229. }
  230. func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
  231. everything := []event.Type{{Type: "*"}}
  232. return &mautrix.Filter{
  233. Presence: mautrix.FilterPart{
  234. Senders: []id.UserID{puppet.CustomMXID},
  235. Types: []event.Type{event.EphemeralEventPresence},
  236. },
  237. AccountData: mautrix.FilterPart{NotTypes: everything},
  238. Room: mautrix.RoomFilter{
  239. Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
  240. IncludeLeave: false,
  241. AccountData: mautrix.FilterPart{NotTypes: everything},
  242. State: mautrix.FilterPart{NotTypes: everything},
  243. Timeline: mautrix.FilterPart{NotTypes: everything},
  244. },
  245. }
  246. }
  247. func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {}
  248. func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() }
  249. func (puppet *Puppet) SaveRoom(room *mautrix.Room) {}
  250. func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" }
  251. func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch }
  252. func (puppet *Puppet) LoadRoom(roomID id.RoomID) *mautrix.Room { return nil }