aboutsummaryrefslogtreecommitdiff
path: root/internal/integration/matrixbot
diff options
context:
space:
mode:
Diffstat (limited to 'internal/integration/matrixbot')
-rw-r--r--internal/integration/matrixbot/client.go200
-rw-r--r--internal/integration/matrixbot/matrixbot.go45
2 files changed, 219 insertions, 26 deletions
diff --git a/internal/integration/matrixbot/client.go b/internal/integration/matrixbot/client.go
new file mode 100644
index 00000000..cdc28a8a
--- /dev/null
+++ b/internal/integration/matrixbot/client.go
@@ -0,0 +1,200 @@
+// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package matrixbot // import "miniflux.app/v2/internal/integration/matrixbot"
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "miniflux.app/v2/internal/crypto"
+ "miniflux.app/v2/internal/version"
+)
+
+const defaultClientTimeout = 10 * time.Second
+
+type Client struct {
+ matrixBaseURL string
+}
+
+func NewClient(matrixBaseURL string) *Client {
+ return &Client{matrixBaseURL: matrixBaseURL}
+}
+
+// Specs: https://spec.matrix.org/v1.8/client-server-api/#getwell-knownmatrixclient
+func (c *Client) DiscoverEndpoints() (*DiscoveryEndpointResponse, error) {
+ endpointURL, err := url.JoinPath(c.matrixBaseURL, "/.well-known/matrix/client")
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
+ }
+
+ request, err := http.NewRequest(http.MethodGet, endpointURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to create request: %v", err)
+ }
+
+ request.Header.Set("Accept", "application/json")
+ request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+ httpClient := &http.Client{Timeout: defaultClientTimeout}
+ response, err := httpClient.Do(request)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to send request: %v", err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode >= 400 {
+ return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
+ }
+
+ var discoveryEndpointResponse DiscoveryEndpointResponse
+ if err := json.NewDecoder(response.Body).Decode(&discoveryEndpointResponse); err != nil {
+ return nil, fmt.Errorf("matrix: unable to decode discovery response: %w", err)
+ }
+
+ return &discoveryEndpointResponse, nil
+}
+
+// Specs https://spec.matrix.org/v1.8/client-server-api/#post_matrixclientv3login
+func (c *Client) Login(homeServerURL, matrixUsername, matrixPassword string) (*LoginResponse, error) {
+ endpointURL, err := url.JoinPath(homeServerURL, "/_matrix/client/v3/login")
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
+ }
+
+ loginRequest := LoginRequest{
+ Type: "m.login.password",
+ Identifier: UserIdentifier{
+ Type: "m.id.user",
+ User: matrixUsername,
+ },
+ Password: matrixPassword,
+ }
+
+ requestBody, err := json.Marshal(loginRequest)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to encode request body: %v", err)
+ }
+
+ request, err := http.NewRequest(http.MethodPost, endpointURL, bytes.NewReader(requestBody))
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to create request: %v", err)
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("Accept", "application/json")
+ request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+
+ httpClient := &http.Client{Timeout: defaultClientTimeout}
+ response, err := httpClient.Do(request)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to send request: %v", err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode >= 400 {
+ return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
+ }
+
+ var loginResponse LoginResponse
+ if err := json.NewDecoder(response.Body).Decode(&loginResponse); err != nil {
+ return nil, fmt.Errorf("matrix: unable to decode login response: %w", err)
+ }
+
+ return &loginResponse, nil
+}
+
+// Specs https://spec.matrix.org/v1.8/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
+func (c *Client) SendFormattedTextMessage(homeServerURL, accessToken, roomID, textMessage, formattedMessage string) (*RoomEventResponse, error) {
+ txnID := crypto.GenerateRandomStringHex(10)
+ endpointURL, err := url.JoinPath(homeServerURL, "/_matrix/client/v3/rooms/", roomID, "/send/m.room.message/", txnID)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to join base URL and path: %w", err)
+ }
+
+ messageEvent := TextMessageEventRequest{
+ MsgType: "m.text",
+ Body: textMessage,
+ Format: "org.matrix.custom.html",
+ FormattedBody: formattedMessage,
+ }
+
+ requestBody, err := json.Marshal(messageEvent)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to encode request body: %v", err)
+ }
+
+ request, err := http.NewRequest(http.MethodPut, endpointURL, bytes.NewReader(requestBody))
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to create request: %v", err)
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+ request.Header.Set("Accept", "application/json")
+ request.Header.Set("User-Agent", "Miniflux/"+version.Version)
+ request.Header.Set("Authorization", "Bearer "+accessToken)
+
+ httpClient := &http.Client{Timeout: defaultClientTimeout}
+ response, err := httpClient.Do(request)
+ if err != nil {
+ return nil, fmt.Errorf("matrix: unable to send request: %v", err)
+ }
+ defer response.Body.Close()
+
+ if response.StatusCode >= 400 {
+ return nil, fmt.Errorf("matrix: unexpected response from %s status code is %d", endpointURL, response.StatusCode)
+ }
+
+ var eventResponse RoomEventResponse
+ if err := json.NewDecoder(response.Body).Decode(&eventResponse); err != nil {
+ return nil, fmt.Errorf("matrix: unable to decode event response: %w", err)
+ }
+
+ return &eventResponse, nil
+}
+
+type HomeServerInformation struct {
+ BaseURL string `json:"base_url"`
+}
+
+type IdentityServerInformation struct {
+ BaseURL string `json:"base_url"`
+}
+
+type DiscoveryEndpointResponse struct {
+ HomeServerInformation HomeServerInformation `json:"m.homeserver"`
+ IdentityServerInformation IdentityServerInformation `json:"m.identity_server"`
+}
+
+type UserIdentifier struct {
+ Type string `json:"type"`
+ User string `json:"user"`
+}
+
+type LoginRequest struct {
+ Type string `json:"type"`
+ Identifier UserIdentifier `json:"identifier"`
+ Password string `json:"password"`
+}
+
+type LoginResponse struct {
+ UserID string `json:"user_id"`
+ AccessToken string `json:"access_token"`
+ DeviceID string `json:"device_id"`
+ HomeServer string `json:"home_server"`
+}
+
+type TextMessageEventRequest struct {
+ MsgType string `json:"msgtype"`
+ Body string `json:"body"`
+ Format string `json:"format"`
+ FormattedBody string `json:"formatted_body"`
+}
+
+type RoomEventResponse struct {
+ EventID string `json:"event_id"`
+}
diff --git a/internal/integration/matrixbot/matrixbot.go b/internal/integration/matrixbot/matrixbot.go
index 7b3198ed..8b2c6961 100644
--- a/internal/integration/matrixbot/matrixbot.go
+++ b/internal/integration/matrixbot/matrixbot.go
@@ -5,46 +5,39 @@ package matrixbot // import "miniflux.app/v2/internal/integration/matrixbot"
import (
"fmt"
+ "strings"
- "miniflux.app/v2/internal/logger"
"miniflux.app/v2/internal/model"
-
- "github.com/matrix-org/gomatrix"
)
// PushEntry pushes entries to matrix chat using integration settings provided
-func PushEntries(entries model.Entries, serverURL, botLogin, botPassword, chatID string) error {
- bot, err := gomatrix.NewClient(serverURL, "", "")
+func PushEntries(feed *model.Feed, entries model.Entries, matrixBaseURL, matrixUsername, matrixPassword, matrixRoomID string) error {
+ client := NewClient(matrixBaseURL)
+ discovery, err := client.DiscoverEndpoints()
if err != nil {
- return fmt.Errorf("matrixbot: bot creation failed: %w", err)
+ return err
}
- resp, err := bot.Login(&gomatrix.ReqLogin{
- Type: "m.login.password",
- User: botLogin,
- Password: botPassword,
- })
-
+ loginResponse, err := client.Login(discovery.HomeServerInformation.BaseURL, matrixUsername, matrixPassword)
if err != nil {
- logger.Debug("matrixbot: login failed: %w", err)
- return fmt.Errorf("matrixbot: login failed, please check your credentials or turn on debug mode")
+ return err
}
- bot.SetCredentials(resp.UserID, resp.AccessToken)
- defer func() {
- bot.Logout()
- bot.ClearCredentials()
- }()
+ var textMessages []string
+ var formattedTextMessages []string
- message := ""
for _, entry := range entries {
- message = message + entry.Title + " " + entry.URL + "\n"
+ textMessages = append(textMessages, fmt.Sprintf(`[%s] %s - %s`, feed.Title, entry.Title, entry.URL))
+ formattedTextMessages = append(formattedTextMessages, fmt.Sprintf(`<li><strong>%s</strong>: <a href="%s">%s</a></li>`, feed.Title, entry.URL, entry.Title))
}
- if _, err = bot.SendText(chatID, message); err != nil {
- logger.Debug("matrixbot: sending message failed: %w", err)
- return fmt.Errorf("matrixbot: sending message failed, turn on debug mode for more informations")
- }
+ _, err = client.SendFormattedTextMessage(
+ discovery.HomeServerInformation.BaseURL,
+ loginResponse.AccessToken,
+ matrixRoomID,
+ strings.Join(textMessages, "\n"),
+ "<ul>"+strings.Join(formattedTextMessages, "\n")+"</ul>",
+ )
- return nil
+ return err
}