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.

143 lines
5.0 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. sqlCryptoStore := crypto.NewSQLCryptoStore(db, dialect, client.DeviceID, []byte(client.DeviceID.String()+"pickle"), cryptoLogger)
  31. // Try to create the tables if they are missing
  32. if err = sqlCryptoStore.CreateTables(); err != nil {
  33. return
  34. }
  35. cryptoStore = sqlCryptoStore
  36. cryptoLogger.Debug("Using SQL backend as the crypto store")
  37. } else {
  38. deviceID := client.DeviceID.String()
  39. if deviceID == "" {
  40. deviceID = "_empty_device_id"
  41. }
  42. cryptoStore, err = crypto.NewGobStore(deviceID + ".gob")
  43. if err != nil {
  44. return
  45. }
  46. cryptoLogger.Debug("Using gob storage as the crypto store")
  47. }
  48. botClient.stateStore = &NebStateStore{&nebStore.InMemoryStore}
  49. olmMachine := crypto.NewOlmMachine(client, cryptoLogger, cryptoStore, botClient.stateStore)
  50. if err = olmMachine.Load(); err != nil {
  51. return
  52. }
  53. botClient.olmMachine = olmMachine
  54. return nil
  55. }
  56. // Register registers a BotClient's Sync and StateMember event callbacks to update its internal state
  57. // when new events arrive.
  58. func (botClient *BotClient) Register(syncer mautrix.ExtensibleSyncer) {
  59. syncer.OnEventType(mevt.StateMember, func(_ mautrix.EventSource, evt *mevt.Event) {
  60. botClient.olmMachine.HandleMemberEvent(evt)
  61. })
  62. syncer.OnSync(botClient.syncCallback)
  63. }
  64. func (botClient *BotClient) syncCallback(resp *mautrix.RespSync, since string) bool {
  65. botClient.stateStore.UpdateStateStore(resp)
  66. botClient.olmMachine.ProcessSyncResponse(resp, since)
  67. if err := botClient.olmMachine.CryptoStore.Flush(); err != nil {
  68. log.WithError(err).Error("Could not flush crypto store")
  69. }
  70. return true
  71. }
  72. // DecryptMegolmEvent attempts to decrypt an incoming m.room.encrypted message using the session information
  73. // already present in the OlmMachine. The corresponding decrypted event is then returned.
  74. // If it fails, usually because the session is not known, an error is returned.
  75. func (botClient *BotClient) DecryptMegolmEvent(evt *mevt.Event) (*mevt.Event, error) {
  76. return botClient.olmMachine.DecryptMegolmEvent(evt)
  77. }
  78. // SendMessageEvent sends the given content to the given room ID using this BotClient as a message event.
  79. // If the target room has enabled encryption, a megolm session is created if one doesn't already exist
  80. // and the message is sent after being encrypted.
  81. func (botClient *BotClient) SendMessageEvent(roomID id.RoomID, evtType mevt.Type, content interface{},
  82. extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) {
  83. olmMachine := botClient.olmMachine
  84. if olmMachine.StateStore.IsEncrypted(roomID) {
  85. // Check if there is already a megolm session
  86. if sess, err := olmMachine.CryptoStore.GetOutboundGroupSession(roomID); err != nil {
  87. return nil, err
  88. } else if sess == nil || sess.Expired() || !sess.Shared {
  89. // No error but valid, shared session does not exist
  90. memberIDs, err := botClient.stateStore.GetJoinedMembers(roomID)
  91. if err != nil {
  92. return nil, err
  93. }
  94. // Share group session with room members
  95. if err = olmMachine.ShareGroupSession(roomID, memberIDs); err != nil {
  96. return nil, err
  97. }
  98. }
  99. enc, err := olmMachine.EncryptMegolmEvent(roomID, mevt.EventMessage, content)
  100. if err != nil {
  101. return nil, err
  102. }
  103. content = enc
  104. evtType = mevt.EventEncrypted
  105. }
  106. return botClient.Client.SendMessageEvent(roomID, evtType, content, extra...)
  107. }
  108. // Sync loops to keep syncing the client with the homeserver by calling the /sync endpoint.
  109. func (botClient *BotClient) Sync() {
  110. // Get the state store up to date
  111. resp, err := botClient.SyncRequest(30000, "", "", true, mevt.PresenceOnline)
  112. if err != nil {
  113. log.WithError(err).Error("Error performing initial sync")
  114. return
  115. }
  116. botClient.stateStore.UpdateStateStore(resp)
  117. for {
  118. if e := botClient.Client.Sync(); e != nil {
  119. log.WithFields(log.Fields{
  120. log.ErrorKey: e,
  121. "user_id": botClient.config.UserID,
  122. }).Error("Fatal Sync() error")
  123. time.Sleep(10 * time.Second)
  124. } else {
  125. log.WithField("user_id", botClient.config.UserID).Info("Stopping Sync()")
  126. return
  127. }
  128. }
  129. }