client.go 6.5 KB

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