client.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package matrixbot // import "miniflux.app/v2/internal/integration/matrixbot"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "time"
  11. "miniflux.app/v2/internal/crypto"
  12. "miniflux.app/v2/internal/version"
  13. )
  14. const defaultClientTimeout = 10 * time.Second
  15. type Client struct {
  16. matrixBaseURL string
  17. }
  18. func NewClient(matrixBaseURL string) *Client {
  19. return &Client{matrixBaseURL: matrixBaseURL}
  20. }
  21. // Specs: https://spec.matrix.org/v1.8/client-server-api/#getwell-knownmatrixclient
  22. func (c *Client) DiscoverEndpoints() (*DiscoveryEndpointResponse, error) {
  23. endpointURL, err := url.JoinPath(c.matrixBaseURL, "/.well-known/matrix/client")
  24. if err != nil {
  25. return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
  26. }
  27. request, err := http.NewRequest(http.MethodGet, endpointURL, nil)
  28. if err != nil {
  29. return nil, fmt.Errorf("matrix: unable to create request: %v", err)
  30. }
  31. request.Header.Set("Accept", "application/json")
  32. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  33. httpClient := &http.Client{Timeout: defaultClientTimeout}
  34. response, err := httpClient.Do(request)
  35. if err != nil {
  36. return nil, fmt.Errorf("matrix: unable to send request: %v", err)
  37. }
  38. defer response.Body.Close()
  39. if response.StatusCode >= 400 {
  40. return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
  41. }
  42. var discoveryEndpointResponse DiscoveryEndpointResponse
  43. if err := json.NewDecoder(response.Body).Decode(&discoveryEndpointResponse); err != nil {
  44. return nil, fmt.Errorf("matrix: unable to decode discovery response: %w", err)
  45. }
  46. return &discoveryEndpointResponse, nil
  47. }
  48. // Specs https://spec.matrix.org/v1.8/client-server-api/#post_matrixclientv3login
  49. func (c *Client) Login(homeServerURL, matrixUsername, matrixPassword string) (*LoginResponse, error) {
  50. endpointURL, err := url.JoinPath(homeServerURL, "/_matrix/client/v3/login")
  51. if err != nil {
  52. return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
  53. }
  54. loginRequest := LoginRequest{
  55. Type: "m.login.password",
  56. Identifier: UserIdentifier{
  57. Type: "m.id.user",
  58. User: matrixUsername,
  59. },
  60. Password: matrixPassword,
  61. }
  62. requestBody, err := json.Marshal(loginRequest)
  63. if err != nil {
  64. return nil, fmt.Errorf("matrix: unable to encode request body: %v", err)
  65. }
  66. request, err := http.NewRequest(http.MethodPost, endpointURL, bytes.NewReader(requestBody))
  67. if err != nil {
  68. return nil, fmt.Errorf("matrix: unable to create request: %v", err)
  69. }
  70. request.Header.Set("Content-Type", "application/json")
  71. request.Header.Set("Accept", "application/json")
  72. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  73. httpClient := &http.Client{Timeout: defaultClientTimeout}
  74. response, err := httpClient.Do(request)
  75. if err != nil {
  76. return nil, fmt.Errorf("matrix: unable to send request: %v", err)
  77. }
  78. defer response.Body.Close()
  79. if response.StatusCode >= 400 {
  80. return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
  81. }
  82. var loginResponse LoginResponse
  83. if err := json.NewDecoder(response.Body).Decode(&loginResponse); err != nil {
  84. return nil, fmt.Errorf("matrix: unable to decode login response: %w", err)
  85. }
  86. return &loginResponse, nil
  87. }
  88. // Specs https://spec.matrix.org/v1.8/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
  89. func (c *Client) SendFormattedTextMessage(homeServerURL, accessToken, roomID, textMessage, formattedMessage string) (*RoomEventResponse, error) {
  90. txnID := crypto.GenerateRandomStringHex(10)
  91. endpointURL, err := url.JoinPath(homeServerURL, "/_matrix/client/v3/rooms/", roomID, "/send/m.room.message/", txnID)
  92. if err != nil {
  93. return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
  94. }
  95. messageEvent := TextMessageEventRequest{
  96. MsgType: "m.text",
  97. Body: textMessage,
  98. Format: "org.matrix.custom.html",
  99. FormattedBody: formattedMessage,
  100. }
  101. requestBody, err := json.Marshal(messageEvent)
  102. if err != nil {
  103. return nil, fmt.Errorf("matrix: unable to encode request body: %v", err)
  104. }
  105. request, err := http.NewRequest(http.MethodPut, endpointURL, bytes.NewReader(requestBody))
  106. if err != nil {
  107. return nil, fmt.Errorf("matrix: unable to create request: %v", err)
  108. }
  109. request.Header.Set("Content-Type", "application/json")
  110. request.Header.Set("Accept", "application/json")
  111. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  112. request.Header.Set("Authorization", "Bearer "+accessToken)
  113. httpClient := &http.Client{Timeout: defaultClientTimeout}
  114. response, err := httpClient.Do(request)
  115. if err != nil {
  116. return nil, fmt.Errorf("matrix: unable to send request: %v", err)
  117. }
  118. defer response.Body.Close()
  119. if response.StatusCode >= 400 {
  120. return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
  121. }
  122. var eventResponse RoomEventResponse
  123. if err := json.NewDecoder(response.Body).Decode(&eventResponse); err != nil {
  124. return nil, fmt.Errorf("matrix: unable to decode event response: %w", err)
  125. }
  126. return &eventResponse, nil
  127. }
  128. type HomeServerInformation struct {
  129. BaseURL string `json:"base_url"`
  130. }
  131. type IdentityServerInformation struct {
  132. BaseURL string `json:"base_url"`
  133. }
  134. type DiscoveryEndpointResponse struct {
  135. HomeServerInformation HomeServerInformation `json:"m.homeserver"`
  136. IdentityServerInformation IdentityServerInformation `json:"m.identity_server"`
  137. }
  138. type UserIdentifier struct {
  139. Type string `json:"type"`
  140. User string `json:"user"`
  141. }
  142. type LoginRequest struct {
  143. Type string `json:"type"`
  144. Identifier UserIdentifier `json:"identifier"`
  145. Password string `json:"password"`
  146. }
  147. type LoginResponse struct {
  148. UserID string `json:"user_id"`
  149. AccessToken string `json:"access_token"`
  150. DeviceID string `json:"device_id"`
  151. HomeServer string `json:"home_server"`
  152. }
  153. type TextMessageEventRequest struct {
  154. MsgType string `json:"msgtype"`
  155. Body string `json:"body"`
  156. Format string `json:"format"`
  157. FormattedBody string `json:"formatted_body"`
  158. }
  159. type RoomEventResponse struct {
  160. EventID string `json:"event_id"`
  161. }