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.

388 lines
10 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. package clients
  2. import (
  3. "database/sql"
  4. "fmt"
  5. "net/http"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/matrix-org/go-neb/api"
  10. "github.com/matrix-org/go-neb/database"
  11. "github.com/matrix-org/go-neb/matrix"
  12. "github.com/matrix-org/go-neb/metrics"
  13. "github.com/matrix-org/go-neb/types"
  14. "github.com/matrix-org/gomatrix"
  15. shellwords "github.com/mattn/go-shellwords"
  16. log "github.com/sirupsen/logrus"
  17. )
  18. // A Clients is a collection of clients used for bot services.
  19. type Clients struct {
  20. db database.Storer
  21. httpClient *http.Client
  22. dbMutex sync.Mutex
  23. mapMutex sync.Mutex
  24. clients map[string]clientEntry
  25. }
  26. // New makes a new collection of matrix clients
  27. func New(db database.Storer, cli *http.Client) *Clients {
  28. clients := &Clients{
  29. db: db,
  30. httpClient: cli,
  31. clients: make(map[string]clientEntry), // user_id => clientEntry
  32. }
  33. return clients
  34. }
  35. // Client gets a client for the userID
  36. func (c *Clients) Client(userID string) (*gomatrix.Client, error) {
  37. entry := c.getClient(userID)
  38. if entry.client != nil {
  39. return entry.client, nil
  40. }
  41. entry, err := c.loadClientFromDB(userID)
  42. return entry.client, err
  43. }
  44. // Update updates the config for a matrix client
  45. func (c *Clients) Update(config api.ClientConfig) (api.ClientConfig, error) {
  46. _, old, err := c.updateClientInDB(config)
  47. return old.config, err
  48. }
  49. // Start listening on client /sync streams
  50. func (c *Clients) Start() error {
  51. configs, err := c.db.LoadMatrixClientConfigs()
  52. if err != nil {
  53. return err
  54. }
  55. for _, cfg := range configs {
  56. if cfg.Sync {
  57. if _, err := c.Client(cfg.UserID); err != nil {
  58. return err
  59. }
  60. }
  61. }
  62. return nil
  63. }
  64. type clientEntry struct {
  65. config api.ClientConfig
  66. client *gomatrix.Client
  67. }
  68. func (c *Clients) getClient(userID string) clientEntry {
  69. c.mapMutex.Lock()
  70. defer c.mapMutex.Unlock()
  71. return c.clients[userID]
  72. }
  73. func (c *Clients) setClient(client clientEntry) {
  74. c.mapMutex.Lock()
  75. defer c.mapMutex.Unlock()
  76. c.clients[client.config.UserID] = client
  77. }
  78. func (c *Clients) loadClientFromDB(userID string) (entry clientEntry, err error) {
  79. c.dbMutex.Lock()
  80. defer c.dbMutex.Unlock()
  81. entry = c.getClient(userID)
  82. if entry.client != nil {
  83. return
  84. }
  85. if entry.config, err = c.db.LoadMatrixClientConfig(userID); err != nil {
  86. if err == sql.ErrNoRows {
  87. err = fmt.Errorf("client with user ID %s does not exist", userID)
  88. }
  89. return
  90. }
  91. if entry.client, err = c.newClient(entry.config); err != nil {
  92. return
  93. }
  94. c.setClient(entry)
  95. return
  96. }
  97. func (c *Clients) updateClientInDB(newConfig api.ClientConfig) (new clientEntry, old clientEntry, err error) {
  98. c.dbMutex.Lock()
  99. defer c.dbMutex.Unlock()
  100. old = c.getClient(newConfig.UserID)
  101. if old.client != nil && old.config == newConfig {
  102. // Already have a client with that config.
  103. new = old
  104. return
  105. }
  106. new.config = newConfig
  107. if new.client, err = c.newClient(new.config); err != nil {
  108. return
  109. }
  110. // set the new display name if they differ
  111. if old.config.DisplayName != new.config.DisplayName {
  112. if err := new.client.SetDisplayName(new.config.DisplayName); err != nil {
  113. // whine about it but don't stop: this isn't fatal.
  114. log.WithFields(log.Fields{
  115. log.ErrorKey: err,
  116. "displayname": new.config.DisplayName,
  117. "user_id": new.config.UserID,
  118. }).Error("Failed to set display name")
  119. }
  120. }
  121. if old.config, err = c.db.StoreMatrixClientConfig(new.config); err != nil {
  122. new.client.StopSync()
  123. return
  124. }
  125. if old.client != nil {
  126. old.client.StopSync()
  127. return
  128. }
  129. c.setClient(new)
  130. return
  131. }
  132. func (c *Clients) onMessageEvent(client *gomatrix.Client, event *gomatrix.Event) {
  133. services, err := c.db.LoadServicesForUser(client.UserID)
  134. if err != nil {
  135. log.WithFields(log.Fields{
  136. log.ErrorKey: err,
  137. "room_id": event.RoomID,
  138. "service_user_id": client.UserID,
  139. }).Warn("Error loading services")
  140. }
  141. body, ok := event.Body()
  142. if !ok || body == "" {
  143. return
  144. }
  145. // filter m.notice to prevent loops
  146. if msgtype, ok := event.MessageType(); !ok || msgtype == "m.notice" {
  147. return
  148. }
  149. // replace all smart quotes with their normal counterparts so shellwords can parse it
  150. body = strings.Replace(body, ``, `'`, -1)
  151. body = strings.Replace(body, ``, `'`, -1)
  152. body = strings.Replace(body, ``, `"`, -1)
  153. body = strings.Replace(body, ``, `"`, -1)
  154. var responses []interface{}
  155. for _, service := range services {
  156. if body[0] == '!' { // message is a command
  157. args, err := shellwords.Parse(body[1:])
  158. if err != nil {
  159. args = strings.Split(body[1:], " ")
  160. }
  161. if response := runCommandForService(service.Commands(client), event, args); response != nil {
  162. responses = append(responses, response)
  163. }
  164. } else { // message isn't a command, it might need expanding
  165. expansions := runExpansionsForService(service.Expansions(client), event, body)
  166. responses = append(responses, expansions...)
  167. }
  168. }
  169. for _, content := range responses {
  170. if _, err := client.SendMessageEvent(event.RoomID, "m.room.message", content); err != nil {
  171. log.WithFields(log.Fields{
  172. log.ErrorKey: err,
  173. "room_id": event.RoomID,
  174. "user_id": event.Sender,
  175. "content": content,
  176. }).Print("Failed to send command response")
  177. }
  178. }
  179. }
  180. // runCommandForService runs a single command read from a matrix event. Runs
  181. // the matching command with the longest path. Returns the JSON encodable
  182. // content of a single matrix message event to use as a response or nil if no
  183. // response is appropriate.
  184. func runCommandForService(cmds []types.Command, event *gomatrix.Event, arguments []string) interface{} {
  185. var bestMatch *types.Command
  186. for i, command := range cmds {
  187. matches := command.Matches(arguments)
  188. betterMatch := bestMatch == nil || len(bestMatch.Path) < len(command.Path)
  189. if matches && betterMatch {
  190. bestMatch = &cmds[i]
  191. }
  192. }
  193. if bestMatch == nil {
  194. return nil
  195. }
  196. cmdArgs := arguments[len(bestMatch.Path):]
  197. log.WithFields(log.Fields{
  198. "room_id": event.RoomID,
  199. "user_id": event.Sender,
  200. "command": bestMatch.Path,
  201. }).Info("Executing command")
  202. content, err := bestMatch.Command(event.RoomID, event.Sender, cmdArgs)
  203. if err != nil {
  204. if content != nil {
  205. log.WithFields(log.Fields{
  206. log.ErrorKey: err,
  207. "room_id": event.RoomID,
  208. "user_id": event.Sender,
  209. "command": bestMatch.Path,
  210. "args": cmdArgs,
  211. }).Warn("Command returned both error and content.")
  212. }
  213. metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusFailure)
  214. content = gomatrix.TextMessage{"m.notice", err.Error()}
  215. } else {
  216. metrics.IncrementCommand(bestMatch.Path[0], metrics.StatusSuccess)
  217. }
  218. return content
  219. }
  220. // run the expansions for a matrix event.
  221. func runExpansionsForService(expans []types.Expansion, event *gomatrix.Event, body string) []interface{} {
  222. var responses []interface{}
  223. for _, expansion := range expans {
  224. matches := map[string]bool{}
  225. for _, matchingGroups := range expansion.Regexp.FindAllStringSubmatch(body, -1) {
  226. matchingText := matchingGroups[0] // first element is always the complete match
  227. if matches[matchingText] {
  228. // Only expand the first occurance of a matching string
  229. continue
  230. }
  231. matches[matchingText] = true
  232. if response := expansion.Expand(event.RoomID, event.Sender, matchingGroups); response != nil {
  233. responses = append(responses, response)
  234. }
  235. }
  236. }
  237. return responses
  238. }
  239. func (c *Clients) onBotOptionsEvent(client *gomatrix.Client, event *gomatrix.Event) {
  240. // see if these options are for us. The state key is the user ID with a leading _
  241. // to get around restrictions in the HS about having user IDs as state keys.
  242. targetUserID := strings.TrimPrefix(*event.StateKey, "_")
  243. if targetUserID != client.UserID {
  244. return
  245. }
  246. // these options fully clobber what was there previously.
  247. opts := types.BotOptions{
  248. UserID: client.UserID,
  249. RoomID: event.RoomID,
  250. SetByUserID: event.Sender,
  251. Options: event.Content,
  252. }
  253. if _, err := c.db.StoreBotOptions(opts); err != nil {
  254. log.WithFields(log.Fields{
  255. log.ErrorKey: err,
  256. "room_id": event.RoomID,
  257. "bot_user_id": client.UserID,
  258. "set_by_user_id": event.Sender,
  259. }).Error("Failed to persist bot options")
  260. }
  261. }
  262. func (c *Clients) onRoomMemberEvent(client *gomatrix.Client, event *gomatrix.Event) {
  263. if *event.StateKey != client.UserID {
  264. return // not our member event
  265. }
  266. m := event.Content["membership"]
  267. membership, ok := m.(string)
  268. if !ok {
  269. return
  270. }
  271. if membership == "invite" {
  272. logger := log.WithFields(log.Fields{
  273. "room_id": event.RoomID,
  274. "service_user_id": client.UserID,
  275. "inviter": event.Sender,
  276. })
  277. logger.Print("Accepting invite from user")
  278. content := struct {
  279. Inviter string `json:"inviter"`
  280. }{event.Sender}
  281. if _, err := client.JoinRoom(event.RoomID, "", content); err != nil {
  282. logger.WithError(err).Print("Failed to join room")
  283. } else {
  284. logger.Print("Joined room")
  285. }
  286. }
  287. }
  288. func (c *Clients) newClient(config api.ClientConfig) (*gomatrix.Client, error) {
  289. client, err := gomatrix.NewClient(config.HomeserverURL, config.UserID, config.AccessToken)
  290. if err != nil {
  291. return nil, err
  292. }
  293. client.Client = c.httpClient
  294. syncer := client.Syncer.(*gomatrix.DefaultSyncer)
  295. nebStore := &matrix.NEBStore{
  296. InMemoryStore: *gomatrix.NewInMemoryStore(),
  297. Database: c.db,
  298. ClientConfig: config,
  299. }
  300. client.Store = nebStore
  301. syncer.Store = nebStore
  302. // TODO: Check that the access token is valid for the userID by peforming
  303. // a request against the server.
  304. syncer.OnEventType("m.room.message", func(event *gomatrix.Event) {
  305. c.onMessageEvent(client, event)
  306. })
  307. syncer.OnEventType("m.room.bot.options", func(event *gomatrix.Event) {
  308. c.onBotOptionsEvent(client, event)
  309. })
  310. if config.AutoJoinRooms {
  311. syncer.OnEventType("m.room.member", func(event *gomatrix.Event) {
  312. c.onRoomMemberEvent(client, event)
  313. })
  314. }
  315. log.WithFields(log.Fields{
  316. "user_id": config.UserID,
  317. "sync": config.Sync,
  318. "auto_join_rooms": config.AutoJoinRooms,
  319. "since": nebStore.LoadNextBatch(config.UserID),
  320. }).Info("Created new client")
  321. if config.Sync {
  322. go func() {
  323. for {
  324. if e := client.Sync(); e != nil {
  325. log.WithFields(log.Fields{
  326. log.ErrorKey: e,
  327. "user_id": config.UserID,
  328. }).Error("Fatal Sync() error")
  329. time.Sleep(10 * time.Second)
  330. } else {
  331. log.WithField("user_id", config.UserID).Info("Stopping Sync()")
  332. return
  333. }
  334. }
  335. }()
  336. }
  337. return client, nil
  338. }