client.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. // Package gomatrix implements the Matrix Client-Server API.
  2. //
  3. // Specification can be found at http://matrix.org/docs/spec/client_server/r0.2.0.html
  4. package gomatrix
  5. import (
  6. "bytes"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "maunium.net/go/maulogger"
  13. "net/http"
  14. "net/url"
  15. "path"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  20. )
  21. // Client represents a Matrix client.
  22. type Client struct {
  23. HomeserverURL *url.URL // The base homeserver URL
  24. Prefix string // The API prefix eg '/_matrix/client/r0'
  25. UserID string // The user ID of the client. Used for forming HTTP paths which use the client's user ID.
  26. AccessToken string // The access_token for the client.
  27. Client *http.Client // The underlying HTTP client which will be used to make HTTP requests.
  28. Syncer Syncer // The thing which can process /sync responses
  29. Store Storer // The thing which can store rooms/tokens/ids
  30. Logger maulogger.Logger
  31. // The ?user_id= query parameter for application services. This must be set *prior* to calling a method. If this is empty,
  32. // no user_id parameter will be sent.
  33. // See http://matrix.org/docs/spec/application_service/unstable.html#identity-assertion
  34. AppServiceUserID string
  35. syncingMutex sync.Mutex // protects syncingID
  36. syncingID uint32 // Identifies the current Sync. Only one Sync can be active at any given time.
  37. }
  38. // HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
  39. type HTTPError struct {
  40. WrappedError error
  41. RespError *RespError
  42. Message string
  43. Code int
  44. }
  45. func (e HTTPError) Error() string {
  46. var wrappedErrMsg string
  47. if e.WrappedError != nil {
  48. wrappedErrMsg = e.WrappedError.Error()
  49. }
  50. return fmt.Sprintf("msg=%s code=%d wrapped=%s", e.Message, e.Code, wrappedErrMsg)
  51. }
  52. // BuildURL builds a URL with the Client's homserver/prefix/access_token set already.
  53. func (cli *Client) BuildURL(urlPath ...string) string {
  54. ps := []string{cli.Prefix}
  55. for _, p := range urlPath {
  56. ps = append(ps, p)
  57. }
  58. return cli.BuildBaseURL(ps...)
  59. }
  60. // BuildBaseURL builds a URL with the Client's homeserver/access_token set already. You must
  61. // supply the prefix in the path.
  62. func (cli *Client) BuildBaseURL(urlPath ...string) string {
  63. // copy the URL. Purposefully ignore error as the input is from a valid URL already
  64. hsURL, _ := url.Parse(cli.HomeserverURL.String())
  65. parts := []string{hsURL.Path}
  66. parts = append(parts, urlPath...)
  67. hsURL.Path = path.Join(parts...)
  68. query := hsURL.Query()
  69. if cli.AccessToken != "" {
  70. query.Set("access_token", cli.AccessToken)
  71. }
  72. if cli.AppServiceUserID != "" {
  73. query.Set("user_id", cli.AppServiceUserID)
  74. }
  75. hsURL.RawQuery = query.Encode()
  76. return hsURL.String()
  77. }
  78. // BuildURLWithQuery builds a URL with query parameters in addition to the Client's homeserver/prefix/access_token set already.
  79. func (cli *Client) BuildURLWithQuery(urlPath []string, urlQuery map[string]string) string {
  80. u, _ := url.Parse(cli.BuildURL(urlPath...))
  81. q := u.Query()
  82. for k, v := range urlQuery {
  83. q.Set(k, v)
  84. }
  85. u.RawQuery = q.Encode()
  86. return u.String()
  87. }
  88. // SetCredentials sets the user ID and access token on this client instance.
  89. func (cli *Client) SetCredentials(userID, accessToken string) {
  90. cli.AccessToken = accessToken
  91. cli.UserID = userID
  92. }
  93. // ClearCredentials removes the user ID and access token on this client instance.
  94. func (cli *Client) ClearCredentials() {
  95. cli.AccessToken = ""
  96. cli.UserID = ""
  97. }
  98. // Sync starts syncing with the provided Homeserver. If Sync() is called twice then the first sync will be stopped and the
  99. // error will be nil.
  100. //
  101. // This function will block until a fatal /sync error occurs, so it should almost always be started as a new goroutine.
  102. // Fatal sync errors can be caused by:
  103. // - The failure to create a filter.
  104. // - Client.Syncer.OnFailedSync returning an error in response to a failed sync.
  105. // - Client.Syncer.ProcessResponse returning an error.
  106. // If you wish to continue retrying in spite of these fatal errors, call Sync() again.
  107. func (cli *Client) Sync() error {
  108. // Mark the client as syncing.
  109. // We will keep syncing until the syncing state changes. Either because
  110. // Sync is called or StopSync is called.
  111. syncingID := cli.incrementSyncingID()
  112. nextBatch := cli.Store.LoadNextBatch(cli.UserID)
  113. filterID := cli.Store.LoadFilterID(cli.UserID)
  114. if filterID == "" {
  115. filterJSON := cli.Syncer.GetFilterJSON(cli.UserID)
  116. resFilter, err := cli.CreateFilter(filterJSON)
  117. if err != nil {
  118. return err
  119. }
  120. filterID = resFilter.FilterID
  121. cli.Store.SaveFilterID(cli.UserID, filterID)
  122. }
  123. for {
  124. resSync, err := cli.SyncRequest(30000, nextBatch, filterID, false, "")
  125. if err != nil {
  126. duration, err2 := cli.Syncer.OnFailedSync(resSync, err)
  127. if err2 != nil {
  128. return err2
  129. }
  130. time.Sleep(duration)
  131. continue
  132. }
  133. // Check that the syncing state hasn't changed
  134. // Either because we've stopped syncing or another sync has been started.
  135. // We discard the response from our sync.
  136. if cli.getSyncingID() != syncingID {
  137. return nil
  138. }
  139. // Save the token now *before* processing it. This means it's possible
  140. // to not process some events, but it means that we won't get constantly stuck processing
  141. // a malformed/buggy event which keeps making us panic.
  142. cli.Store.SaveNextBatch(cli.UserID, resSync.NextBatch)
  143. if err = cli.Syncer.ProcessResponse(resSync, nextBatch); err != nil {
  144. return err
  145. }
  146. nextBatch = resSync.NextBatch
  147. }
  148. }
  149. func (cli *Client) incrementSyncingID() uint32 {
  150. cli.syncingMutex.Lock()
  151. defer cli.syncingMutex.Unlock()
  152. cli.syncingID++
  153. return cli.syncingID
  154. }
  155. func (cli *Client) getSyncingID() uint32 {
  156. cli.syncingMutex.Lock()
  157. defer cli.syncingMutex.Unlock()
  158. return cli.syncingID
  159. }
  160. // StopSync stops the ongoing sync started by Sync.
  161. func (cli *Client) StopSync() {
  162. // Advance the syncing state so that any running Syncs will terminate.
  163. cli.incrementSyncingID()
  164. }
  165. func (cli *Client) LogRequest(req *http.Request, body string) {
  166. if cli.Logger == nil {
  167. return
  168. }
  169. cli.Logger.Debugfln("%s %s %s", req.Method, req.URL.Path, body)
  170. }
  171. // MakeRequest makes a JSON HTTP request to the given URL.
  172. // If "resBody" is not nil, the response body will be json.Unmarshalled into it.
  173. //
  174. // Returns the HTTP body as bytes on 2xx with a nil error. Returns an error if the response is not 2xx along
  175. // with the HTTP body bytes if it got that far. This error is an HTTPError which includes the returned
  176. // HTTP status code and possibly a RespError as the WrappedError, if the HTTP body could be decoded as a RespError.
  177. func (cli *Client) MakeRequest(method string, httpURL string, reqBody interface{}, resBody interface{}) ([]byte, error) {
  178. var req *http.Request
  179. var err error
  180. logBody := "{}"
  181. if reqBody != nil {
  182. var jsonStr []byte
  183. jsonStr, err = json.Marshal(reqBody)
  184. if err != nil {
  185. return nil, err
  186. }
  187. logBody = string(jsonStr)
  188. req, err = http.NewRequest(method, httpURL, bytes.NewBuffer(jsonStr))
  189. } else {
  190. req, err = http.NewRequest(method, httpURL, nil)
  191. }
  192. if err != nil {
  193. return nil, err
  194. }
  195. req.Header.Set("Content-Type", "application/json")
  196. cli.LogRequest(req, logBody)
  197. res, err := cli.Client.Do(req)
  198. if res != nil {
  199. defer res.Body.Close()
  200. }
  201. if err != nil {
  202. return nil, err
  203. }
  204. contents, err := ioutil.ReadAll(res.Body)
  205. if res.StatusCode/100 != 2 { // not 2xx
  206. var wrap error
  207. respErr := &RespError{}
  208. if _ = json.Unmarshal(contents, respErr); respErr.ErrCode != "" {
  209. wrap = respErr
  210. } else {
  211. respErr = nil
  212. }
  213. // If we failed to decode as RespError, don't just drop the HTTP body, include it in the
  214. // HTTP error instead (e.g proxy errors which return HTML).
  215. msg := "Failed to " + method + " JSON to " + req.URL.Path
  216. if wrap == nil {
  217. msg = msg + ": " + string(contents)
  218. }
  219. return contents, HTTPError{
  220. Code: res.StatusCode,
  221. Message: msg,
  222. WrappedError: wrap,
  223. RespError: respErr,
  224. }
  225. }
  226. if err != nil {
  227. return nil, err
  228. }
  229. if resBody != nil {
  230. if err = json.Unmarshal(contents, &resBody); err != nil {
  231. return nil, err
  232. }
  233. }
  234. return contents, nil
  235. }
  236. // CreateFilter makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-user-userid-filter
  237. func (cli *Client) CreateFilter(filter json.RawMessage) (resp *RespCreateFilter, err error) {
  238. urlPath := cli.BuildURL("user", cli.UserID, "filter")
  239. _, err = cli.MakeRequest("POST", urlPath, &filter, &resp)
  240. return
  241. }
  242. // SyncRequest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-sync
  243. func (cli *Client) SyncRequest(timeout int, since, filterID string, fullState bool, setPresence string) (resp *RespSync, err error) {
  244. query := map[string]string{
  245. "timeout": strconv.Itoa(timeout),
  246. }
  247. if since != "" {
  248. query["since"] = since
  249. }
  250. if filterID != "" {
  251. query["filter"] = filterID
  252. }
  253. if setPresence != "" {
  254. query["set_presence"] = setPresence
  255. }
  256. if fullState {
  257. query["full_state"] = "true"
  258. }
  259. urlPath := cli.BuildURLWithQuery([]string{"sync"}, query)
  260. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  261. return
  262. }
  263. func (cli *Client) register(u string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) {
  264. var bodyBytes []byte
  265. bodyBytes, err = cli.MakeRequest("POST", u, req, nil)
  266. if err != nil {
  267. httpErr, ok := err.(HTTPError)
  268. if !ok { // network error
  269. return
  270. }
  271. if httpErr.Code == 401 {
  272. // body should be RespUserInteractive, if it isn't, fail with the error
  273. err = json.Unmarshal(bodyBytes, &uiaResp)
  274. return
  275. }
  276. return
  277. }
  278. // body should be RespRegister
  279. err = json.Unmarshal(bodyBytes, &resp)
  280. return
  281. }
  282. // Register makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
  283. //
  284. // Registers with kind=user. For kind=guest, see RegisterGuest.
  285. func (cli *Client) Register(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
  286. u := cli.BuildURL("register")
  287. return cli.register(u, req)
  288. }
  289. // RegisterGuest makes an HTTP request according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register
  290. // with kind=guest.
  291. //
  292. // For kind=user, see Register.
  293. func (cli *Client) RegisterGuest(req *ReqRegister) (*RespRegister, *RespUserInteractive, error) {
  294. query := map[string]string{
  295. "kind": "guest",
  296. }
  297. u := cli.BuildURLWithQuery([]string{"register"}, query)
  298. return cli.register(u, req)
  299. }
  300. // RegisterDummy performs m.login.dummy registration according to https://matrix.org/docs/spec/client_server/r0.2.0.html#dummy-auth
  301. //
  302. // Only a username and password need to be provided on the ReqRegister struct. Most local/developer homeservers will allow registration
  303. // this way. If the homeserver does not, an error is returned.
  304. //
  305. // This does not set credentials on the client instance. See SetCredentials() instead.
  306. //
  307. // res, err := cli.RegisterDummy(&gomatrix.ReqRegister{
  308. // Username: "alice",
  309. // Password: "wonderland",
  310. // })
  311. // if err != nil {
  312. // panic(err)
  313. // }
  314. // token := res.AccessToken
  315. func (cli *Client) RegisterDummy(req *ReqRegister) (*RespRegister, error) {
  316. res, uia, err := cli.Register(req)
  317. if err != nil && uia == nil {
  318. return nil, err
  319. }
  320. if uia != nil && uia.HasSingleStageFlow("m.login.dummy") {
  321. req.Auth = struct {
  322. Type string `json:"type"`
  323. Session string `json:"session,omitempty"`
  324. }{"m.login.dummy", uia.Session}
  325. res, _, err = cli.Register(req)
  326. if err != nil {
  327. return nil, err
  328. }
  329. }
  330. if res == nil {
  331. return nil, fmt.Errorf("registration failed: does this server support m.login.dummy? ")
  332. }
  333. return res, nil
  334. }
  335. // Login a user to the homeserver according to http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-login
  336. // This does not set credentials on this client instance. See SetCredentials() instead.
  337. func (cli *Client) Login(req *ReqLogin) (resp *RespLogin, err error) {
  338. urlPath := cli.BuildURL("login")
  339. _, err = cli.MakeRequest("POST", urlPath, req, &resp)
  340. return
  341. }
  342. // Logout the current user. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-logout
  343. // This does not clear the credentials from the client instance. See ClearCredentials() instead.
  344. func (cli *Client) Logout() (resp *RespLogout, err error) {
  345. urlPath := cli.BuildURL("logout")
  346. _, err = cli.MakeRequest("POST", urlPath, nil, &resp)
  347. return
  348. }
  349. // Versions returns the list of supported Matrix versions on this homeserver. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-versions
  350. func (cli *Client) Versions() (resp *RespVersions, err error) {
  351. urlPath := cli.BuildBaseURL("_matrix", "client", "versions")
  352. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  353. return
  354. }
  355. // JoinRoom joins the client to a room ID or alias. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
  356. //
  357. // If serverName is specified, this will be added as a query param to instruct the homeserver to join via that server. If content is specified, it will
  358. // be JSON encoded and used as the request body.
  359. func (cli *Client) JoinRoom(roomIDorAlias, serverName string, content interface{}) (resp *RespJoinRoom, err error) {
  360. var urlPath string
  361. if serverName != "" {
  362. urlPath = cli.BuildURLWithQuery([]string{"join", roomIDorAlias}, map[string]string{
  363. "server_name": serverName,
  364. })
  365. } else {
  366. urlPath = cli.BuildURL("join", roomIDorAlias)
  367. }
  368. _, err = cli.MakeRequest("POST", urlPath, content, &resp)
  369. return
  370. }
  371. // GetDisplayName returns the display name of the user from the specified MXID. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
  372. func (cli *Client) GetDisplayName(mxid string) (resp *RespUserDisplayName, err error) {
  373. urlPath := cli.BuildURL("profile", mxid, "displayname")
  374. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  375. return
  376. }
  377. // GetOwnDisplayName returns the user's display name. See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-displayname
  378. func (cli *Client) GetOwnDisplayName() (resp *RespUserDisplayName, err error) {
  379. urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
  380. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  381. return
  382. }
  383. // SetDisplayName sets the user's profile display name. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-displayname
  384. func (cli *Client) SetDisplayName(displayName string) (err error) {
  385. urlPath := cli.BuildURL("profile", cli.UserID, "displayname")
  386. s := struct {
  387. DisplayName string `json:"displayname"`
  388. }{displayName}
  389. _, err = cli.MakeRequest("PUT", urlPath, &s, nil)
  390. return
  391. }
  392. // GetAvatarURL gets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-profile-userid-avatar-url
  393. func (cli *Client) GetAvatarURL() (url string, err error) {
  394. urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
  395. s := struct {
  396. AvatarURL string `json:"avatar_url"`
  397. }{}
  398. _, err = cli.MakeRequest("GET", urlPath, nil, &s)
  399. if err != nil {
  400. return "", err
  401. }
  402. return s.AvatarURL, nil
  403. }
  404. // SetAvatarURL sets the user's avatar URL. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-profile-userid-avatar-url
  405. func (cli *Client) SetAvatarURL(url string) (err error) {
  406. urlPath := cli.BuildURL("profile", cli.UserID, "avatar_url")
  407. s := struct {
  408. AvatarURL string `json:"avatar_url"`
  409. }{url}
  410. _, err = cli.MakeRequest("PUT", urlPath, &s, nil)
  411. if err != nil {
  412. return err
  413. }
  414. return nil
  415. }
  416. // SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
  417. // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
  418. func (cli *Client) SendMessageEvent(roomID string, eventType EventType, contentJSON interface{}) (resp *RespSendEvent, err error) {
  419. txnID := txnID()
  420. urlPath := cli.BuildURL("rooms", roomID, "send", eventType.String(), txnID)
  421. _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
  422. return
  423. }
  424. // SendMessageEvent sends a message event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
  425. // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
  426. func (cli *Client) SendMassagedMessageEvent(roomID string, eventType EventType, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
  427. txnID := txnID()
  428. urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "send", eventType.String(), txnID}, map[string]string{
  429. "ts": strconv.FormatInt(ts, 10),
  430. })
  431. _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
  432. return
  433. }
  434. // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
  435. // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
  436. func (cli *Client) SendStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}) (resp *RespSendEvent, err error) {
  437. urlPath := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey)
  438. _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
  439. return
  440. }
  441. // SendStateEvent sends a state event into a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
  442. // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal.
  443. func (cli *Client) SendMassagedStateEvent(roomID string, eventType EventType, stateKey string, contentJSON interface{}, ts int64) (resp *RespSendEvent, err error) {
  444. urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "state", eventType.String(), stateKey}, map[string]string{
  445. "ts": strconv.FormatInt(ts, 10),
  446. })
  447. _, err = cli.MakeRequest("PUT", urlPath, contentJSON, &resp)
  448. return
  449. }
  450. // SendText sends an m.room.message event into the given room with a msgtype of m.text
  451. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-text
  452. func (cli *Client) SendText(roomID, text string) (*RespSendEvent, error) {
  453. return cli.SendMessageEvent(roomID, EventMessage, Content{
  454. MsgType: MsgText,
  455. Body: text,
  456. })
  457. }
  458. // SendImage sends an m.room.message event into the given room with a msgtype of m.image
  459. // See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-image
  460. func (cli *Client) SendImage(roomID, body, url string) (*RespSendEvent, error) {
  461. return cli.SendMessageEvent(roomID, EventMessage, Content{
  462. MsgType: MsgImage,
  463. Body: body,
  464. URL: url,
  465. })
  466. }
  467. // SendVideo sends an m.room.message event into the given room with a msgtype of m.video
  468. // See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-video
  469. func (cli *Client) SendVideo(roomID, body, url string) (*RespSendEvent, error) {
  470. return cli.SendMessageEvent(roomID, EventMessage, Content{
  471. MsgType: MsgVideo,
  472. Body: body,
  473. URL: url,
  474. })
  475. }
  476. // SendNotice sends an m.room.message event into the given room with a msgtype of m.notice
  477. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#m-notice
  478. func (cli *Client) SendNotice(roomID, text string) (*RespSendEvent, error) {
  479. return cli.SendMessageEvent(roomID, EventMessage, Content{
  480. MsgType: MsgNotice,
  481. Body: text,
  482. })
  483. }
  484. // RedactEvent redacts the given event. See http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
  485. func (cli *Client) RedactEvent(roomID, eventID string, req *ReqRedact) (resp *RespSendEvent, err error) {
  486. txnID := txnID()
  487. urlPath := cli.BuildURL("rooms", roomID, "redact", eventID, txnID)
  488. _, err = cli.MakeRequest("PUT", urlPath, req, &resp)
  489. return
  490. }
  491. // CreateRoom creates a new Matrix room. See https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
  492. // resp, err := cli.CreateRoom(&gomatrix.ReqCreateRoom{
  493. // Preset: "public_chat",
  494. // })
  495. // fmt.Println("Room:", resp.RoomID)
  496. func (cli *Client) CreateRoom(req *ReqCreateRoom) (resp *RespCreateRoom, err error) {
  497. urlPath := cli.BuildURL("createRoom")
  498. _, err = cli.MakeRequest("POST", urlPath, req, &resp)
  499. return
  500. }
  501. // LeaveRoom leaves the given room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-leave
  502. func (cli *Client) LeaveRoom(roomID string) (resp *RespLeaveRoom, err error) {
  503. u := cli.BuildURL("rooms", roomID, "leave")
  504. _, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
  505. return
  506. }
  507. // ForgetRoom forgets a room entirely. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-forget
  508. func (cli *Client) ForgetRoom(roomID string) (resp *RespForgetRoom, err error) {
  509. u := cli.BuildURL("rooms", roomID, "forget")
  510. _, err = cli.MakeRequest("POST", u, struct{}{}, &resp)
  511. return
  512. }
  513. // InviteUser invites a user to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-invite
  514. func (cli *Client) InviteUser(roomID string, req *ReqInviteUser) (resp *RespInviteUser, err error) {
  515. u := cli.BuildURL("rooms", roomID, "invite")
  516. _, err = cli.MakeRequest("POST", u, req, &resp)
  517. return
  518. }
  519. // InviteUserByThirdParty invites a third-party identifier to a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#invite-by-third-party-id-endpoint
  520. func (cli *Client) InviteUserByThirdParty(roomID string, req *ReqInvite3PID) (resp *RespInviteUser, err error) {
  521. u := cli.BuildURL("rooms", roomID, "invite")
  522. _, err = cli.MakeRequest("POST", u, req, &resp)
  523. return
  524. }
  525. // KickUser kicks a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-kick
  526. func (cli *Client) KickUser(roomID string, req *ReqKickUser) (resp *RespKickUser, err error) {
  527. u := cli.BuildURL("rooms", roomID, "kick")
  528. _, err = cli.MakeRequest("POST", u, req, &resp)
  529. return
  530. }
  531. // BanUser bans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-ban
  532. func (cli *Client) BanUser(roomID string, req *ReqBanUser) (resp *RespBanUser, err error) {
  533. u := cli.BuildURL("rooms", roomID, "ban")
  534. _, err = cli.MakeRequest("POST", u, req, &resp)
  535. return
  536. }
  537. // UnbanUser unbans a user from a room. See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-rooms-roomid-unban
  538. func (cli *Client) UnbanUser(roomID string, req *ReqUnbanUser) (resp *RespUnbanUser, err error) {
  539. u := cli.BuildURL("rooms", roomID, "unban")
  540. _, err = cli.MakeRequest("POST", u, req, &resp)
  541. return
  542. }
  543. // UserTyping sets the typing status of the user. See https://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-typing-userid
  544. func (cli *Client) UserTyping(roomID string, typing bool, timeout int64) (resp *RespTyping, err error) {
  545. req := ReqTyping{Typing: typing, Timeout: timeout}
  546. u := cli.BuildURL("rooms", roomID, "typing", cli.UserID)
  547. _, err = cli.MakeRequest("PUT", u, req, &resp)
  548. return
  549. }
  550. func (cli *Client) SetPresence(status string) (err error) {
  551. req := ReqPresence{Presence: status}
  552. u := cli.BuildURL("presence", cli.UserID, "status")
  553. _, err = cli.MakeRequest("PUT", u, req, nil)
  554. return
  555. }
  556. // StateEvent gets a single state event in a room. It will attempt to JSON unmarshal into the given "outContent" struct with
  557. // the HTTP response body, or return an error.
  558. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-state-eventtype-statekey
  559. func (cli *Client) StateEvent(roomID string, eventType EventType, stateKey string, outContent interface{}) (err error) {
  560. u := cli.BuildURL("rooms", roomID, "state", eventType.String(), stateKey)
  561. _, err = cli.MakeRequest("GET", u, nil, outContent)
  562. return
  563. }
  564. // UploadLink uploads an HTTP URL and then returns an MXC URI.
  565. func (cli *Client) UploadLink(link string) (*RespMediaUpload, error) {
  566. res, err := cli.Client.Get(link)
  567. if res != nil {
  568. defer res.Body.Close()
  569. }
  570. if err != nil {
  571. return nil, err
  572. }
  573. return cli.Upload(res.Body, res.Header.Get("Content-Type"), res.ContentLength)
  574. }
  575. func (cli *Client) Download(mxcURL string) (io.ReadCloser, error) {
  576. if !strings.HasPrefix(mxcURL, "mxc://") {
  577. return nil, errors.New("invalid Matrix content URL")
  578. }
  579. parts := strings.Split(mxcURL[len("mxc://"):], "/")
  580. if len(parts) != 2 {
  581. return nil, errors.New("invalid Matrix content URL")
  582. }
  583. u := cli.BuildBaseURL("_matrix/media/r0/download", parts[0], parts[1])
  584. resp, err := cli.Client.Get(u)
  585. if err != nil {
  586. return nil, err
  587. }
  588. return resp.Body, nil
  589. }
  590. func (cli *Client) DownloadBytes(mxcURL string) ([]byte, error) {
  591. resp, err := cli.Download(mxcURL)
  592. if err != nil {
  593. return nil, err
  594. }
  595. defer resp.Close()
  596. return ioutil.ReadAll(resp)
  597. }
  598. func (cli *Client) UploadBytes(data []byte, contentType string) (*RespMediaUpload, error) {
  599. return cli.Upload(bytes.NewReader(data), contentType, int64(len(data)))
  600. }
  601. // UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI.
  602. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-media-r0-upload
  603. func (cli *Client) Upload(content io.Reader, contentType string, contentLength int64) (*RespMediaUpload, error) {
  604. req, err := http.NewRequest("POST", cli.BuildBaseURL("_matrix/media/r0/upload"), content)
  605. if err != nil {
  606. return nil, err
  607. }
  608. req.Header.Set("Content-Type", contentType)
  609. req.ContentLength = contentLength
  610. cli.LogRequest(req, fmt.Sprintf("%d bytes", contentLength))
  611. res, err := cli.Client.Do(req)
  612. if res != nil {
  613. defer res.Body.Close()
  614. }
  615. if err != nil {
  616. return nil, err
  617. }
  618. if res.StatusCode != 200 {
  619. contents, err := ioutil.ReadAll(res.Body)
  620. if err != nil {
  621. return nil, HTTPError{
  622. Message: "Upload request failed - Failed to read response body: " + err.Error(),
  623. Code: res.StatusCode,
  624. }
  625. }
  626. return nil, HTTPError{
  627. Message: "Upload request failed: " + string(contents),
  628. Code: res.StatusCode,
  629. }
  630. }
  631. var m RespMediaUpload
  632. if err := json.NewDecoder(res.Body).Decode(&m); err != nil {
  633. return nil, err
  634. }
  635. return &m, nil
  636. }
  637. // JoinedMembers returns a map of joined room members. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
  638. //
  639. // In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
  640. // This API is primarily designed for application services which may want to efficiently look up joined members in a room.
  641. func (cli *Client) JoinedMembers(roomID string) (resp *RespJoinedMembers, err error) {
  642. u := cli.BuildURL("rooms", roomID, "joined_members")
  643. _, err = cli.MakeRequest("GET", u, nil, &resp)
  644. return
  645. }
  646. // JoinedRooms returns a list of rooms which the client is joined to. See TODO-SPEC. https://github.com/matrix-org/synapse/pull/1680
  647. //
  648. // In general, usage of this API is discouraged in favour of /sync, as calling this API can race with incoming membership changes.
  649. // This API is primarily designed for application services which may want to efficiently look up joined rooms.
  650. func (cli *Client) JoinedRooms() (resp *RespJoinedRooms, err error) {
  651. u := cli.BuildURL("joined_rooms")
  652. _, err = cli.MakeRequest("GET", u, nil, &resp)
  653. return
  654. }
  655. // Messages returns a list of message and state events for a room. It uses
  656. // pagination query parameters to paginate history in the room.
  657. // See https://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-rooms-roomid-messages
  658. func (cli *Client) Messages(roomID, from, to string, dir rune, limit int) (resp *RespMessages, err error) {
  659. query := map[string]string{
  660. "from": from,
  661. "dir": string(dir),
  662. }
  663. if to != "" {
  664. query["to"] = to
  665. }
  666. if limit != 0 {
  667. query["limit"] = strconv.Itoa(limit)
  668. }
  669. urlPath := cli.BuildURLWithQuery([]string{"rooms", roomID, "messages"}, query)
  670. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  671. return
  672. }
  673. func (cli *Client) GetEvent(roomID, eventID string) (resp *Event, err error) {
  674. urlPath := cli.BuildURL("rooms", roomID, "event", eventID)
  675. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  676. return
  677. }
  678. func (cli *Client) MarkRead(roomID, eventID string) (err error) {
  679. urlPath := cli.BuildURL("rooms", roomID, "receipt", "m.read", eventID)
  680. _, err = cli.MakeRequest("POST", urlPath, struct{}{}, nil)
  681. return
  682. }
  683. // TurnServer returns turn server details and credentials for the client to use when initiating calls.
  684. // See http://matrix.org/docs/spec/client_server/r0.2.0.html#get-matrix-client-r0-voip-turnserver
  685. func (cli *Client) TurnServer() (resp *RespTurnServer, err error) {
  686. urlPath := cli.BuildURL("voip", "turnServer")
  687. _, err = cli.MakeRequest("GET", urlPath, nil, &resp)
  688. return
  689. }
  690. func txnID() string {
  691. return "go" + strconv.FormatInt(time.Now().UnixNano(), 10)
  692. }
  693. // NewClient creates a new Matrix Client ready for syncing
  694. func NewClient(homeserverURL, userID, accessToken string) (*Client, error) {
  695. hsURL, err := url.Parse(homeserverURL)
  696. if err != nil {
  697. return nil, err
  698. }
  699. // By default, use an in-memory store which will never save filter ids / next batch tokens to disk.
  700. // The client will work with this storer: it just won't remember across restarts.
  701. // In practice, a database backend should be used.
  702. store := NewInMemoryStore()
  703. cli := Client{
  704. AccessToken: accessToken,
  705. HomeserverURL: hsURL,
  706. UserID: userID,
  707. Prefix: "/_matrix/client/r0",
  708. Syncer: NewDefaultSyncer(userID, store),
  709. Store: store,
  710. }
  711. // By default, use the default HTTP client.
  712. cli.Client = http.DefaultClient
  713. return &cli, nil
  714. }