diff --git a/go.mod b/go.mod index 7f69247f..660d0339 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible github.com/gorilla/mux v1.8.0 github.com/lib/pq v1.10.9 - github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 github.com/mccutchen/go-httpbin/v2 v2.11.0 github.com/prometheus/client_golang v1.16.0 github.com/rylans/getlang v0.0.0-20201227074721-9e7f44ff8aa0 diff --git a/go.sum b/go.sum index 3ef20763..7cb56f1f 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530 h1:kHKxCOLcHH8r4Fzarl4+Y3K5hjothkVW5z7T1dUM11U= -github.com/matrix-org/gomatrix v0.0.0-20220926102614-ceba4d9f7530/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mccutchen/go-httpbin/v2 v2.11.0 h1:uDVmzefIbcG9NeEmWxwy7PdxztY+oCRlOLbHSaXdSF4= diff --git a/internal/integration/integration.go b/internal/integration/integration.go index 07b70308..fbb3f3dc 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -174,7 +174,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode if userIntegrations.MatrixBotEnabled { logger.Debug("[Integration] Sending %d entries for User #%d to Matrix", len(entries), userIntegrations.UserID) - err := matrixbot.PushEntries(entries, userIntegrations.MatrixBotURL, userIntegrations.MatrixBotUser, userIntegrations.MatrixBotPassword, userIntegrations.MatrixBotChatID) + err := matrixbot.PushEntries(feed, entries, userIntegrations.MatrixBotURL, userIntegrations.MatrixBotUser, userIntegrations.MatrixBotPassword, userIntegrations.MatrixBotChatID) if err != nil { logger.Error("[Integration] push entries to matrix bot failed: %v", err) } 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(`
  • %s: %s
  • `, 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"), + "", + ) - return nil + return err }