1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-01 17:38:37 +00:00

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
This commit is contained in:
Frédéric Guillot 2023-09-09 16:16:45 -07:00
parent 3b94217fb7
commit 3d84b07532
5 changed files with 220 additions and 30 deletions

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -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
}