Forráskód Böngészése

Add builtin Matrix client and send HTML formatted messages to Matrix

- Add builtin Matrix client
- Remove dependency on `gomatrix` client
- Send HTML formatted messages to Matrix
Frédéric Guillot 2 éve
szülő
commit
3d84b07532

+ 0 - 1
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

+ 0 - 2
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=

+ 1 - 1
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)
 		}

+ 200 - 0
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"`
+}

+ 19 - 26
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
 }