3d84b07532
- Add builtin Matrix client - Remove dependency on `gomatrix` client - Send HTML formatted messages to Matrix
200 lines
6.2 KiB
Go
200 lines
6.2 KiB
Go
// 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"`
|
|
}
|