portal.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
  2. // Copyright (C) 2018 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. "maunium.net/go/mautrix-whatsapp/database"
  19. log "maunium.net/go/maulogger"
  20. "fmt"
  21. "maunium.net/go/mautrix-whatsapp/types"
  22. "maunium.net/go/gomatrix"
  23. "strings"
  24. "maunium.net/go/mautrix-appservice"
  25. "github.com/Rhymen/go-whatsapp"
  26. "sync"
  27. "net/http"
  28. "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
  29. )
  30. func (user *User) GetPortalByMXID(mxid types.MatrixRoomID) *Portal {
  31. user.portalsLock.Lock()
  32. defer user.portalsLock.Unlock()
  33. portal, ok := user.portalsByMXID[mxid]
  34. if !ok {
  35. dbPortal := user.bridge.DB.Portal.GetByMXID(mxid)
  36. if dbPortal == nil || dbPortal.Owner != user.ID {
  37. return nil
  38. }
  39. portal = user.NewPortal(dbPortal)
  40. user.portalsByJID[portal.JID] = portal
  41. if len(portal.MXID) > 0 {
  42. user.portalsByMXID[portal.MXID] = portal
  43. }
  44. }
  45. return portal
  46. }
  47. func (user *User) GetPortalByJID(jid types.WhatsAppID) *Portal {
  48. user.portalsLock.Lock()
  49. defer user.portalsLock.Unlock()
  50. portal, ok := user.portalsByJID[jid]
  51. if !ok {
  52. dbPortal := user.bridge.DB.Portal.GetByJID(user.ID, jid)
  53. if dbPortal == nil {
  54. dbPortal = user.bridge.DB.Portal.New()
  55. dbPortal.JID = jid
  56. dbPortal.Owner = user.ID
  57. dbPortal.Insert()
  58. }
  59. portal = user.NewPortal(dbPortal)
  60. user.portalsByJID[portal.JID] = portal
  61. if len(portal.MXID) > 0 {
  62. user.portalsByMXID[portal.MXID] = portal
  63. }
  64. }
  65. return portal
  66. }
  67. func (user *User) GetAllPortals() []*Portal {
  68. user.portalsLock.Lock()
  69. defer user.portalsLock.Unlock()
  70. dbPortals := user.bridge.DB.Portal.GetAll(user.ID)
  71. output := make([]*Portal, len(dbPortals))
  72. for index, dbPortal := range dbPortals {
  73. portal, ok := user.portalsByJID[dbPortal.JID]
  74. if !ok {
  75. portal = user.NewPortal(dbPortal)
  76. user.portalsByJID[dbPortal.JID] = portal
  77. if len(dbPortal.MXID) > 0 {
  78. user.portalsByMXID[dbPortal.MXID] = portal
  79. }
  80. }
  81. output[index] = portal
  82. }
  83. return output
  84. }
  85. func (user *User) NewPortal(dbPortal *database.Portal) *Portal {
  86. return &Portal{
  87. Portal: dbPortal,
  88. user: user,
  89. bridge: user.bridge,
  90. log: user.log.Sub(fmt.Sprintf("Portal/%s", dbPortal.JID)),
  91. }
  92. }
  93. type Portal struct {
  94. *database.Portal
  95. user *User
  96. bridge *Bridge
  97. log log.Logger
  98. roomCreateLock sync.Mutex
  99. }
  100. func (portal *Portal) SyncParticipants(metadata *whatsapp_ext.GroupInfo) {
  101. for _, participant := range metadata.Participants {
  102. intent := portal.user.GetPuppetByJID(participant.JID).Intent()
  103. intent.EnsureJoined(portal.MXID)
  104. }
  105. }
  106. func (portal *Portal) UpdateAvatar() bool {
  107. avatar, err := portal.user.Conn.GetProfilePicThumb(portal.JID)
  108. if err != nil {
  109. portal.log.Errorln(err)
  110. return false
  111. }
  112. if portal.Avatar == avatar.Tag {
  113. return false
  114. }
  115. data, err := avatar.DownloadBytes()
  116. if err != nil {
  117. portal.log.Errorln("Failed to download avatar:", err)
  118. return false
  119. }
  120. mime := http.DetectContentType(data)
  121. resp, err := portal.MainIntent().UploadBytes(data, mime)
  122. if err != nil {
  123. portal.log.Errorln("Failed to upload avatar:", err)
  124. return false
  125. }
  126. _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, resp.ContentURI)
  127. if err != nil {
  128. portal.log.Warnln("Failed to set room topic:", err)
  129. return false
  130. }
  131. portal.Avatar = avatar.Tag
  132. return true
  133. }
  134. func (portal *Portal) UpdateName(metadata *whatsapp_ext.GroupInfo) bool {
  135. if portal.Name != metadata.Name {
  136. _, err := portal.MainIntent().SetRoomName(portal.MXID, metadata.Name)
  137. if err == nil {
  138. portal.Name = metadata.Name
  139. return true
  140. }
  141. portal.log.Warnln("Failed to set room name:", err)
  142. }
  143. return false
  144. }
  145. func (portal *Portal) UpdateTopic(metadata *whatsapp_ext.GroupInfo) bool {
  146. if portal.Topic != metadata.Topic {
  147. _, err := portal.MainIntent().SetRoomTopic(portal.MXID, metadata.Topic)
  148. if err == nil {
  149. portal.Topic = metadata.Topic
  150. return true
  151. }
  152. portal.log.Warnln("Failed to set room topic:", err)
  153. }
  154. return false
  155. }
  156. func (portal *Portal) UpdateMetadata() bool {
  157. metadata, err := portal.user.Conn.GetGroupMetaData(portal.JID)
  158. if err != nil {
  159. portal.log.Errorln(err)
  160. return false
  161. }
  162. portal.SyncParticipants(metadata)
  163. update := false
  164. update = portal.UpdateName(metadata) || update
  165. update = portal.UpdateTopic(metadata) || update
  166. return update
  167. }
  168. func (portal *Portal) Sync(contact whatsapp.Contact) {
  169. if len(portal.MXID) == 0 {
  170. if !portal.IsPrivateChat() {
  171. portal.Name = contact.Name
  172. }
  173. err := portal.CreateMatrixRoom()
  174. if err != nil {
  175. portal.log.Errorln("Failed to create portal room:", err)
  176. return
  177. }
  178. }
  179. if portal.IsPrivateChat() {
  180. return
  181. }
  182. update := false
  183. update = portal.UpdateMetadata() || update
  184. update = portal.UpdateAvatar() || update
  185. if update {
  186. portal.Update()
  187. }
  188. }
  189. func (portal *Portal) CreateMatrixRoom() error {
  190. portal.roomCreateLock.Lock()
  191. defer portal.roomCreateLock.Unlock()
  192. if len(portal.MXID) > 0 {
  193. return nil
  194. }
  195. name := portal.Name
  196. topic := portal.Topic
  197. isPrivateChat := false
  198. if strings.HasSuffix(portal.JID, "s.whatsapp.net") {
  199. puppet := portal.user.GetPuppetByJID(portal.JID)
  200. name = puppet.Displayname
  201. topic = "WhatsApp private chat"
  202. isPrivateChat = true
  203. }
  204. resp, err := portal.MainIntent().CreateRoom(&gomatrix.ReqCreateRoom{
  205. Visibility: "private",
  206. Name: name,
  207. Topic: topic,
  208. Invite: []string{portal.user.ID},
  209. Preset: "private_chat",
  210. IsDirect: isPrivateChat,
  211. })
  212. if err != nil {
  213. return err
  214. }
  215. portal.MXID = resp.RoomID
  216. portal.Update()
  217. return nil
  218. }
  219. func (portal *Portal) IsPrivateChat() bool {
  220. return strings.HasSuffix(portal.JID, puppetJIDStrippedSuffix)
  221. }
  222. func (portal *Portal) MainIntent() *appservice.IntentAPI {
  223. if portal.IsPrivateChat() {
  224. return portal.user.GetPuppetByJID(portal.JID).Intent()
  225. }
  226. return portal.bridge.AppService.BotIntent()
  227. }
  228. func (portal *Portal) IsDuplicate(id types.WhatsAppMessageID) bool {
  229. msg := portal.bridge.DB.Message.GetByJID(portal.Owner, id)
  230. if msg != nil {
  231. portal.log.Debugln("Ignoring duplicate message", id)
  232. return true
  233. }
  234. return false
  235. }
  236. func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) {
  237. msg := portal.bridge.DB.Message.New()
  238. msg.Owner = portal.Owner
  239. msg.JID = jid
  240. msg.MXID = mxid
  241. msg.Insert()
  242. }
  243. func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI {
  244. if info.FromMe {
  245. portal.log.Debugln("Unhandled message from me:", info.Id)
  246. return nil
  247. } else if portal.IsPrivateChat() {
  248. return portal.MainIntent()
  249. }
  250. puppet := portal.user.GetPuppetByJID(info.SenderJid)
  251. return puppet.Intent()
  252. }
  253. func (portal *Portal) HandleTextMessage(message whatsapp.TextMessage) {
  254. if portal.IsDuplicate(message.Info.Id) {
  255. return
  256. }
  257. portal.CreateMatrixRoom()
  258. intent := portal.GetMessageIntent(message.Info)
  259. if intent == nil {
  260. return
  261. }
  262. resp, err := intent.SendText(portal.MXID, message.Text)
  263. if err != nil {
  264. portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
  265. return
  266. }
  267. portal.MarkHandled(message.Info.Id, resp.EventID)
  268. portal.log.Debugln("Handled message", message.Info.Id, "->", resp.EventID)
  269. }
  270. func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), info whatsapp.MessageInfo, mime, caption string) {
  271. if portal.IsDuplicate(info.Id) {
  272. return
  273. }
  274. portal.CreateMatrixRoom()
  275. intent := portal.GetMessageIntent(info)
  276. if intent == nil {
  277. return
  278. }
  279. img, err := download()
  280. if err != nil {
  281. portal.log.Errorln("Failed to download media:", err)
  282. return
  283. }
  284. uploaded, err := intent.UploadBytes(img, mime)
  285. if err != nil {
  286. portal.log.Errorln("Failed to upload media:", err)
  287. return
  288. }
  289. resp, err := intent.SendImage(portal.MXID, caption, uploaded.ContentURI)
  290. if err != nil {
  291. portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err)
  292. return
  293. }
  294. portal.MarkHandled(info.Id, resp.EventID)
  295. portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
  296. }
  297. func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
  298. var err error
  299. switch evt.Content.MsgType {
  300. case gomatrix.MsgText:
  301. err = portal.user.Conn.Send(whatsapp.TextMessage{
  302. Text: evt.Content.Body,
  303. Info: whatsapp.MessageInfo{
  304. RemoteJid: portal.JID,
  305. },
  306. })
  307. default:
  308. portal.log.Debugln("Unhandled Matrix event:", evt)
  309. return
  310. }
  311. if err != nil {
  312. portal.log.Errorln("Error handling Matrix event %s: %v", evt.ID, err)
  313. } else {
  314. portal.log.Debugln("Handled Matrix event:", evt)
  315. }
  316. }