You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

144 lines
5.1 KiB

  1. package clients
  2. import (
  3. "time"
  4. "github.com/matrix-org/go-neb/api"
  5. "github.com/matrix-org/go-neb/database"
  6. "github.com/matrix-org/go-neb/matrix"
  7. log "github.com/sirupsen/logrus"
  8. "maunium.net/go/mautrix"
  9. "maunium.net/go/mautrix/crypto"
  10. mevt "maunium.net/go/mautrix/event"
  11. "maunium.net/go/mautrix/id"
  12. )
  13. // BotClient represents one of the bot's sessions, with a specific User and Device ID.
  14. // It can be used for sending messages and retrieving information about the rooms that
  15. // the client has joined.
  16. type BotClient struct {
  17. *mautrix.Client
  18. config api.ClientConfig
  19. olmMachine *crypto.OlmMachine
  20. stateStore *NebStateStore
  21. }
  22. // InitOlmMachine initializes a BotClient's internal OlmMachine given a client object and a Neb store,
  23. // which will be used to store room information.
  24. func (botClient *BotClient) InitOlmMachine(client *mautrix.Client, nebStore *matrix.NEBStore) (err error) {
  25. var cryptoStore crypto.Store
  26. cryptoLogger := CryptoMachineLogger{}
  27. if sdb, ok := database.GetServiceDB().(*database.ServiceDB); ok {
  28. // Create an SQL crypto store based on the ServiceDB used
  29. db, dialect := sdb.GetSQLDb()
  30. accountID := botClient.config.UserID.String() + "-" + client.DeviceID.String()
  31. sqlCryptoStore := crypto.NewSQLCryptoStore(db, dialect, accountID, client.DeviceID, []byte(client.DeviceID.String()+"pickle"), cryptoLogger)
  32. // Try to create the tables if they are missing
  33. if err = sqlCryptoStore.CreateTables(); err != nil {
  34. return
  35. }
  36. cryptoStore = sqlCryptoStore
  37. cryptoLogger.Debug("Using SQL backend as the crypto store")
  38. } else {
  39. deviceID := client.DeviceID.String()
  40. if deviceID == "" {
  41. deviceID = "_empty_device_id"
  42. }
  43. cryptoStore, err = crypto.NewGobStore(deviceID + ".gob")
  44. if err != nil {
  45. return
  46. }
  47. cryptoLogger.Debug("Using gob storage as the crypto store")
  48. }
  49. botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore}
  50. olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore)
  51. if err = olmMachine.Load(); err != nil {
  52. return
  53. }
  54. botClient.olmMachine = olmMachine
  55. return nil
  56. }
  57. // Register registers a BotClient's Sync and StateMember event callbacks to update its internal state
  58. // when new events arrive.
  59. func (botClient *BotClient) Register(syncer mautrix.ExtensibleSyncer) {
  60. syncer.OnEventType(mevt.StateMember, func(_ mautrix.EventSource, evt *mevt.Event) {
  61. botClient.olmMachine.HandleMemberEvent(evt)
  62. })
  63. syncer.OnSync(botClient.syncCallback)
  64. }
  65. func (botClient *BotClient) syncCallback(resp *mautrix.RespSync, since string) bool {
  66. botClient.stateStore.UpdateStateStore(resp)
  67. botClient.olmMachine.ProcessSyncResponse(resp, since)
  68. if err := botClient.olmMachine.CryptoStore.Flush(); err != nil {
  69. log.WithError(err).Error("Could not flush crypto store")
  70. }
  71. return true
  72. }
  73. // DecryptMegolmEvent attempts to decrypt an incoming m.room.encrypted message using the session information
  74. // already present in the OlmMachine. The corresponding decrypted event is then returned.
  75. // If it fails, usually because the session is not known, an error is returned.
  76. func (botClient *BotClient) DecryptMegolmEvent(evt *mevt.Event) (*mevt.Event, error) {
  77. return botClient.olmMachine.DecryptMegolmEvent(evt)
  78. }
  79. // SendMessageEvent sends the given content to the given room ID using this BotClient as a message event.
  80. // If the target room has enabled encryption, a megolm session is created if one doesn't already exist
  81. // and the message is sent after being encrypted.
  82. func (botClient *BotClient) SendMessageEvent(roomID id.RoomID, evtType mevt.Type, content interface{},
  83. extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
  84. olmMachine := botClient.olmMachine
  85. if olmMachine.StateStore.IsEncrypted(roomID) {
  86. // Check if there is already a megolm session
  87. if sess, err := olmMachine.CryptoStore.GetOutboundGroupSession(roomID); err != nil {
  88. return nil, err
  89. } else if sess == nil || sess.Expired() || !sess.Shared {
  90. // No error but valid, shared session does not exist
  91. memberIDs, err := botClient.stateStore.GetJoinedMembers(roomID)
  92. if err != nil {
  93. return nil, err
  94. }
  95. // Share group session with room members
  96. if err = olmMachine.ShareGroupSession(roomID, memberIDs); err != nil {
  97. return nil, err
  98. }
  99. }
  100. enc, err := olmMachine.EncryptMegolmEvent(roomID, mevt.EventMessage, content)
  101. if err != nil {
  102. return nil, err
  103. }
  104. content = enc
  105. evtType = mevt.EventEncrypted
  106. }
  107. return botClient.Client.SendMessageEvent(roomID, evtType, content, extra...)
  108. }
  109. // Sync loops to keep syncing the client with the homeserver by calling the /sync endpoint.
  110. func (botClient *BotClient) Sync() {
  111. // Get the state store up to date
  112. resp, err := botClient.SyncRequest(30000, "", "", true, mevt.PresenceOnline)
  113. if err != nil {
  114. log.WithError(err).Error("Error performing initial sync")
  115. return
  116. }
  117. botClient.stateStore.UpdateStateStore(resp)
  118. for {
  119. if e := botClient.Client.Sync(); e != nil {
  120. log.WithFields(log.Fields{
  121. log.ErrorKey: e,
  122. "user_id": botClient.config.UserID,
  123. }).Error("Fatal Sync() error")
  124. time.Sleep(10 * time.Second)
  125. } else {
  126. log.WithField("user_id", botClient.config.UserID).Info("Stopping Sync()")
  127. return
  128. }
  129. }
  130. }