user.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. package bridge
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "sync"
  8. "github.com/bwmarrin/discordgo"
  9. "github.com/skip2/go-qrcode"
  10. log "maunium.net/go/maulogger/v2"
  11. "maunium.net/go/mautrix"
  12. "maunium.net/go/mautrix/appservice"
  13. "maunium.net/go/mautrix/event"
  14. "maunium.net/go/mautrix/id"
  15. "gitlab.com/beeper/discord/database"
  16. )
  17. var (
  18. ErrNotConnected = errors.New("not connected")
  19. ErrNotLoggedIn = errors.New("not logged in")
  20. )
  21. type User struct {
  22. *database.User
  23. sync.Mutex
  24. bridge *Bridge
  25. log log.Logger
  26. Session *discordgo.Session
  27. }
  28. func (b *Bridge) loadUser(dbUser *database.User, mxid *id.UserID) *User {
  29. // If we weren't passed in a user we attempt to create one if we were given
  30. // a matrix id.
  31. if dbUser == nil {
  32. if mxid == nil {
  33. return nil
  34. }
  35. dbUser = b.db.User.New()
  36. dbUser.MXID = *mxid
  37. dbUser.Insert()
  38. }
  39. user := b.NewUser(dbUser)
  40. // We assume the usersLock was acquired by our caller.
  41. b.usersByMXID[user.MXID] = user
  42. if user.ID != "" {
  43. b.usersByID[user.ID] = user
  44. }
  45. if user.ManagementRoom != "" {
  46. // Lock the management rooms for our update
  47. b.managementRoomsLock.Lock()
  48. b.managementRooms[user.ManagementRoom] = user
  49. b.managementRoomsLock.Unlock()
  50. }
  51. return user
  52. }
  53. func (b *Bridge) GetUserByMXID(userID id.UserID) *User {
  54. // TODO: check if puppet
  55. b.usersLock.Lock()
  56. defer b.usersLock.Unlock()
  57. user, ok := b.usersByMXID[userID]
  58. if !ok {
  59. return b.loadUser(b.db.User.GetByMXID(userID), &userID)
  60. }
  61. return user
  62. }
  63. func (b *Bridge) GetUserByID(id string) *User {
  64. b.usersLock.Lock()
  65. defer b.usersLock.Unlock()
  66. user, ok := b.usersByID[id]
  67. if !ok {
  68. return b.loadUser(b.db.User.GetByID(id), nil)
  69. }
  70. return user
  71. }
  72. func (b *Bridge) NewUser(dbUser *database.User) *User {
  73. user := &User{
  74. User: dbUser,
  75. bridge: b,
  76. log: b.log.Sub("User").Sub(string(dbUser.MXID)),
  77. }
  78. return user
  79. }
  80. func (b *Bridge) getAllUsers() []*User {
  81. b.usersLock.Lock()
  82. defer b.usersLock.Unlock()
  83. dbUsers := b.db.User.GetAll()
  84. users := make([]*User, len(dbUsers))
  85. for idx, dbUser := range dbUsers {
  86. user, ok := b.usersByMXID[dbUser.MXID]
  87. if !ok {
  88. user = b.loadUser(dbUser, nil)
  89. }
  90. users[idx] = user
  91. }
  92. return users
  93. }
  94. func (b *Bridge) startUsers() {
  95. b.log.Debugln("Starting users")
  96. for _, user := range b.getAllUsers() {
  97. go user.Connect()
  98. }
  99. b.log.Debugln("Starting custom puppets")
  100. for _, customPuppet := range b.GetAllPuppetsWithCustomMXID() {
  101. go func(puppet *Puppet) {
  102. b.log.Debugln("Starting custom puppet", puppet.CustomMXID)
  103. if err := puppet.StartCustomMXID(true); err != nil {
  104. puppet.log.Errorln("Failed to start custom puppet:", err)
  105. }
  106. }(customPuppet)
  107. }
  108. }
  109. func (u *User) SetManagementRoom(roomID id.RoomID) {
  110. u.bridge.managementRoomsLock.Lock()
  111. defer u.bridge.managementRoomsLock.Unlock()
  112. existing, ok := u.bridge.managementRooms[roomID]
  113. if ok {
  114. // If there's a user already assigned to this management room, clear it
  115. // out.
  116. // I think this is due a name change or something? I dunno, leaving it
  117. // for now.
  118. existing.ManagementRoom = ""
  119. existing.Update()
  120. }
  121. u.ManagementRoom = roomID
  122. u.bridge.managementRooms[u.ManagementRoom] = u
  123. u.Update()
  124. }
  125. func (u *User) sendQRCode(bot *appservice.IntentAPI, roomID id.RoomID, code string) (id.EventID, error) {
  126. url, err := u.uploadQRCode(code)
  127. if err != nil {
  128. return "", err
  129. }
  130. content := event.MessageEventContent{
  131. MsgType: event.MsgImage,
  132. Body: code,
  133. URL: url.CUString(),
  134. }
  135. resp, err := bot.SendMessageEvent(roomID, event.EventMessage, &content)
  136. if err != nil {
  137. return "", err
  138. }
  139. return resp.EventID, nil
  140. }
  141. func (u *User) uploadQRCode(code string) (id.ContentURI, error) {
  142. qrCode, err := qrcode.Encode(code, qrcode.Low, 256)
  143. if err != nil {
  144. u.log.Errorln("Failed to encode QR code:", err)
  145. return id.ContentURI{}, err
  146. }
  147. bot := u.bridge.as.BotClient()
  148. resp, err := bot.UploadBytes(qrCode, "image/png")
  149. if err != nil {
  150. u.log.Errorln("Failed to upload QR code:", err)
  151. return id.ContentURI{}, err
  152. }
  153. return resp.ContentURI, nil
  154. }
  155. func (u *User) tryAutomaticDoublePuppeting() {
  156. u.Lock()
  157. defer u.Unlock()
  158. if !u.bridge.Config.CanAutoDoublePuppet(u.MXID) {
  159. return
  160. }
  161. u.log.Debugln("Checking if double puppeting needs to be enabled")
  162. puppet := u.bridge.GetPuppetByID(u.ID)
  163. if puppet.CustomMXID != "" {
  164. u.log.Debugln("User already has double-puppeting enabled")
  165. return
  166. }
  167. accessToken, err := puppet.loginWithSharedSecret(u.MXID)
  168. if err != nil {
  169. u.log.Warnln("Failed to login with shared secret:", err)
  170. return
  171. }
  172. err = puppet.SwitchCustomMXID(accessToken, u.MXID)
  173. if err != nil {
  174. puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err)
  175. return
  176. }
  177. u.log.Infoln("Successfully automatically enabled custom puppet")
  178. }
  179. func (u *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) {
  180. doublePuppet := portal.bridge.GetPuppetByCustomMXID(u.MXID)
  181. if doublePuppet == nil {
  182. return
  183. }
  184. if doublePuppet == nil || doublePuppet.CustomIntent() == nil || portal.MXID == "" {
  185. return
  186. }
  187. // TODO sync mute status
  188. }
  189. func (u *User) Login(token string) error {
  190. if token == "" {
  191. return fmt.Errorf("No token specified")
  192. }
  193. u.Token = token
  194. u.Update()
  195. return u.Connect()
  196. }
  197. func (u *User) LoggedIn() bool {
  198. u.Lock()
  199. defer u.Unlock()
  200. return u.Token != ""
  201. }
  202. func (u *User) Logout() error {
  203. u.Lock()
  204. defer u.Unlock()
  205. if u.Session == nil {
  206. return ErrNotLoggedIn
  207. }
  208. puppet := u.bridge.GetPuppetByID(u.ID)
  209. if puppet.CustomMXID != "" {
  210. err := puppet.SwitchCustomMXID("", "")
  211. if err != nil {
  212. u.log.Warnln("Failed to logout-matrix while logging out of Discord:", err)
  213. }
  214. }
  215. if err := u.Session.Close(); err != nil {
  216. return err
  217. }
  218. u.Session = nil
  219. u.Token = ""
  220. u.Update()
  221. return nil
  222. }
  223. func (u *User) Connected() bool {
  224. u.Lock()
  225. defer u.Unlock()
  226. return u.Session != nil
  227. }
  228. func (u *User) Connect() error {
  229. u.Lock()
  230. defer u.Unlock()
  231. if u.Token == "" {
  232. return ErrNotLoggedIn
  233. }
  234. u.log.Debugln("connecting to discord")
  235. session, err := discordgo.New(u.Token)
  236. if err != nil {
  237. return err
  238. }
  239. u.Session = session
  240. // get our user info
  241. user, err := u.Session.User("@me")
  242. if err != nil {
  243. return err
  244. }
  245. u.User.ID = user.ID
  246. // Add our event handlers
  247. u.Session.AddHandler(u.connectedHandler)
  248. u.Session.AddHandler(u.disconnectedHandler)
  249. u.Session.AddHandler(u.channelCreateHandler)
  250. u.Session.AddHandler(u.channelDeleteHandler)
  251. u.Session.AddHandler(u.channelPinsUpdateHandler)
  252. u.Session.AddHandler(u.channelUpdateHandler)
  253. u.Session.AddHandler(u.messageCreateHandler)
  254. u.Session.AddHandler(u.messageDeleteHandler)
  255. u.Session.AddHandler(u.messageUpdateHandler)
  256. u.Session.AddHandler(u.reactionAddHandler)
  257. u.Session.AddHandler(u.reactionRemoveHandler)
  258. u.Session.Identify.Presence.Status = "online"
  259. return u.Session.Open()
  260. }
  261. func (u *User) Disconnect() error {
  262. u.Lock()
  263. defer u.Unlock()
  264. if u.Session == nil {
  265. return ErrNotConnected
  266. }
  267. if err := u.Session.Close(); err != nil {
  268. return err
  269. }
  270. u.Session = nil
  271. return nil
  272. }
  273. func (u *User) connectedHandler(s *discordgo.Session, c *discordgo.Connect) {
  274. u.log.Debugln("connected to discord")
  275. u.tryAutomaticDoublePuppeting()
  276. }
  277. func (u *User) disconnectedHandler(s *discordgo.Session, d *discordgo.Disconnect) {
  278. u.log.Debugln("disconnected from discord")
  279. }
  280. func (u *User) channelCreateHandler(s *discordgo.Session, c *discordgo.ChannelCreate) {
  281. key := database.NewPortalKey(c.ID, u.User.ID)
  282. portal := u.bridge.GetPortalByID(key)
  283. if portal.MXID != "" {
  284. return
  285. }
  286. portal.Name = c.Name
  287. portal.Topic = c.Topic
  288. portal.Type = c.Type
  289. if portal.Type == discordgo.ChannelTypeDM {
  290. portal.DMUser = c.Recipients[0].ID
  291. }
  292. if c.Icon != "" {
  293. u.log.Debugln("channel icon", c.Icon)
  294. }
  295. portal.Update()
  296. portal.createMatrixRoom(u, c.Channel)
  297. }
  298. func (u *User) channelDeleteHandler(s *discordgo.Session, c *discordgo.ChannelDelete) {
  299. u.log.Debugln("channel delete handler")
  300. }
  301. func (u *User) channelPinsUpdateHandler(s *discordgo.Session, c *discordgo.ChannelPinsUpdate) {
  302. u.log.Debugln("channel pins update")
  303. }
  304. func (u *User) channelUpdateHandler(s *discordgo.Session, c *discordgo.ChannelUpdate) {
  305. key := database.NewPortalKey(c.ID, u.User.ID)
  306. portal := u.bridge.GetPortalByID(key)
  307. portal.Name = c.Name
  308. portal.Topic = c.Topic
  309. u.log.Debugln("channel icon", c.Icon)
  310. portal.Update()
  311. u.log.Debugln("channel update")
  312. }
  313. func (u *User) messageCreateHandler(s *discordgo.Session, m *discordgo.MessageCreate) {
  314. if m.GuildID != "" {
  315. u.log.Debugln("ignoring message for guild")
  316. return
  317. }
  318. key := database.NewPortalKey(m.ChannelID, u.ID)
  319. portal := u.bridge.GetPortalByID(key)
  320. msg := portalDiscordMessage{
  321. msg: m,
  322. user: u,
  323. }
  324. portal.discordMessages <- msg
  325. }
  326. func (u *User) messageDeleteHandler(s *discordgo.Session, m *discordgo.MessageDelete) {
  327. if m.GuildID != "" {
  328. u.log.Debugln("ignoring message delete for guild message")
  329. return
  330. }
  331. key := database.NewPortalKey(m.ChannelID, u.ID)
  332. portal := u.bridge.GetPortalByID(key)
  333. msg := portalDiscordMessage{
  334. msg: m,
  335. user: u,
  336. }
  337. portal.discordMessages <- msg
  338. }
  339. func (u *User) messageUpdateHandler(s *discordgo.Session, m *discordgo.MessageUpdate) {
  340. if m.GuildID != "" {
  341. u.log.Debugln("ignoring message update for guild message")
  342. return
  343. }
  344. key := database.NewPortalKey(m.ChannelID, u.ID)
  345. portal := u.bridge.GetPortalByID(key)
  346. msg := portalDiscordMessage{
  347. msg: m,
  348. user: u,
  349. }
  350. portal.discordMessages <- msg
  351. }
  352. func (u *User) reactionAddHandler(s *discordgo.Session, m *discordgo.MessageReactionAdd) {
  353. if m.GuildID != "" {
  354. u.log.Debugln("ignoring reaction for guild message")
  355. return
  356. }
  357. key := database.NewPortalKey(m.ChannelID, u.User.ID)
  358. portal := u.bridge.GetPortalByID(key)
  359. msg := portalDiscordMessage{
  360. msg: m,
  361. user: u,
  362. }
  363. portal.discordMessages <- msg
  364. }
  365. func (u *User) reactionRemoveHandler(s *discordgo.Session, m *discordgo.MessageReactionRemove) {
  366. if m.GuildID != "" {
  367. u.log.Debugln("ignoring reaction for guild message")
  368. return
  369. }
  370. key := database.NewPortalKey(m.ChannelID, u.User.ID)
  371. portal := u.bridge.GetPortalByID(key)
  372. msg := portalDiscordMessage{
  373. msg: m,
  374. user: u,
  375. }
  376. portal.discordMessages <- msg
  377. }
  378. func (u *User) ensureInvited(intent *appservice.IntentAPI, roomID id.RoomID, isDirect bool) bool {
  379. ret := false
  380. inviteContent := event.Content{
  381. Parsed: &event.MemberEventContent{
  382. Membership: event.MembershipInvite,
  383. IsDirect: isDirect,
  384. },
  385. Raw: map[string]interface{}{},
  386. }
  387. customPuppet := u.bridge.GetPuppetByCustomMXID(u.MXID)
  388. if customPuppet != nil && customPuppet.CustomIntent() != nil {
  389. inviteContent.Raw["fi.mau.will_auto_accept"] = true
  390. }
  391. _, err := intent.SendStateEvent(roomID, event.StateMember, u.MXID.String(), &inviteContent)
  392. var httpErr mautrix.HTTPError
  393. if err != nil && errors.As(err, &httpErr) && httpErr.RespError != nil && strings.Contains(httpErr.RespError.Err, "is already in the room") {
  394. u.bridge.StateStore.SetMembership(roomID, u.MXID, event.MembershipJoin)
  395. ret = true
  396. } else if err != nil {
  397. u.log.Warnfln("Failed to invite user to %s: %v", roomID, err)
  398. } else {
  399. ret = true
  400. }
  401. if customPuppet != nil && customPuppet.CustomIntent() != nil {
  402. err = customPuppet.CustomIntent().EnsureJoined(roomID, appservice.EnsureJoinedParams{IgnoreCache: true})
  403. if err != nil {
  404. u.log.Warnfln("Failed to auto-join %s: %v", roomID, err)
  405. ret = false
  406. } else {
  407. ret = true
  408. }
  409. }
  410. return ret
  411. }
  412. func (u *User) getDirectChats() map[id.UserID][]id.RoomID {
  413. chats := map[id.UserID][]id.RoomID{}
  414. privateChats := u.bridge.db.Portal.FindPrivateChats(u.ID)
  415. for _, portal := range privateChats {
  416. if portal.MXID != "" {
  417. puppetMXID := u.bridge.FormatPuppetMXID(portal.Key.Receiver)
  418. chats[puppetMXID] = []id.RoomID{portal.MXID}
  419. }
  420. }
  421. return chats
  422. }
  423. func (u *User) updateDirectChats(chats map[id.UserID][]id.RoomID) {
  424. if !u.bridge.Config.Bridge.SyncDirectChatList {
  425. return
  426. }
  427. puppet := u.bridge.GetPuppetByMXID(u.MXID)
  428. if puppet == nil {
  429. return
  430. }
  431. intent := puppet.CustomIntent()
  432. if intent == nil {
  433. return
  434. }
  435. method := http.MethodPatch
  436. if chats == nil {
  437. chats = u.getDirectChats()
  438. method = http.MethodPut
  439. }
  440. u.log.Debugln("Updating m.direct list on homeserver")
  441. var err error
  442. if u.bridge.Config.Homeserver.Asmux {
  443. urlPath := intent.BuildBaseURL("_matrix", "client", "unstable", "com.beeper.asmux", "dms")
  444. _, err = intent.MakeFullRequest(mautrix.FullRequest{
  445. Method: method,
  446. URL: urlPath,
  447. Headers: http.Header{"X-Asmux-Auth": {u.bridge.as.Registration.AppToken}},
  448. RequestJSON: chats,
  449. })
  450. } else {
  451. existingChats := map[id.UserID][]id.RoomID{}
  452. err = intent.GetAccountData(event.AccountDataDirectChats.Type, &existingChats)
  453. if err != nil {
  454. u.log.Warnln("Failed to get m.direct list to update it:", err)
  455. return
  456. }
  457. for userID, rooms := range existingChats {
  458. if _, ok := u.bridge.ParsePuppetMXID(userID); !ok {
  459. // This is not a ghost user, include it in the new list
  460. chats[userID] = rooms
  461. } else if _, ok := chats[userID]; !ok && method == http.MethodPatch {
  462. // This is a ghost user, but we're not replacing the whole list, so include it too
  463. chats[userID] = rooms
  464. }
  465. }
  466. err = intent.SetAccountData(event.AccountDataDirectChats.Type, &chats)
  467. }
  468. if err != nil {
  469. u.log.Warnln("Failed to update m.direct list:", err)
  470. }
  471. }