custompuppet.go 8.2 KB

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