mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-24 23:10:13 +08:00
feat(provider): new notification provider: matrix
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core/notifier"
|
||||
matrixsdk "github.com/certimate-go/certimate/pkg/sdk3rd/matrix"
|
||||
)
|
||||
|
||||
type NotifierConfig struct {
|
||||
// Homeserver URL (Element web URL or Matrix homeserver base URL).
|
||||
// URL homeserver (адрес Element или базовый URL Matrix).
|
||||
ServerUrl string `json:"serverUrl"`
|
||||
// User ID (MXID), e.g. @bot:example.org.
|
||||
// Идентификатор пользователя (MXID), например @bot:example.org.
|
||||
UserId string `json:"userId"`
|
||||
// Access token from the homeserver (bot or user).
|
||||
// Access token с homeserver (бот или пользователь).
|
||||
AccessToken string `json:"accessToken"`
|
||||
// Room ID (!room:server) for notifications.
|
||||
// ID комнаты (!room:server) для уведомлений.
|
||||
RoomId string `json:"roomId"`
|
||||
}
|
||||
|
||||
type Notifier struct {
|
||||
config *NotifierConfig
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
var _ notifier.Provider = (*Notifier)(nil)
|
||||
|
||||
func NewNotifier(config *NotifierConfig) (*Notifier, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("the configuration of the notifier provider is nil")
|
||||
}
|
||||
|
||||
return &Notifier{
|
||||
config: config,
|
||||
logger: slog.Default(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *Notifier) SetLogger(logger *slog.Logger) {
|
||||
if logger == nil {
|
||||
n.logger = slog.New(slog.DiscardHandler)
|
||||
} else {
|
||||
n.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Notifier) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) {
|
||||
if n.config.RoomId == "" {
|
||||
return nil, errors.New("matrix: config `roomId` is required")
|
||||
}
|
||||
|
||||
client, err := matrixsdk.NewClient(n.config.ServerUrl,
|
||||
matrixsdk.WithUserId(n.config.UserId),
|
||||
matrixsdk.WithAccessToken(n.config.AccessToken),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("matrix: %w", err)
|
||||
}
|
||||
|
||||
body := fmt.Sprintf("%s\n\n%s", subject, message)
|
||||
if err := client.SendTextMessageToRoom(ctx, n.config.RoomId, body); err != nil {
|
||||
return nil, fmt.Errorf("matrix: %w", err)
|
||||
}
|
||||
|
||||
return ¬ifier.NotifyResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package matrix_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/certimate-go/certimate/pkg/core/notifier/internal/tester"
|
||||
impl "github.com/certimate-go/certimate/pkg/core/notifier/providers/matrix"
|
||||
)
|
||||
|
||||
var (
|
||||
fp = tester.Args("MATRIX_")
|
||||
fServerUrl string
|
||||
fUserId string
|
||||
fAccessToken string
|
||||
fRoomId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
fp.DefineString(&fServerUrl, "SERVERURL")
|
||||
fp.DefineString(&fUserId, "USERID")
|
||||
fp.DefineString(&fAccessToken, "ACCESSTOKEN")
|
||||
fp.DefineString(&fRoomId, "ROOMID")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ./matrix_test.go -args \
|
||||
--MATRIX_SERVERURL="https://example.com/your-matrix-server" \
|
||||
--MATRIX_USERID="@bot:example.org" \
|
||||
--MATRIX_ACCESSTOKEN="your-access-token" \
|
||||
--MATRIX_ROOMID="!room:example.org"
|
||||
*/
|
||||
func TestProvider(t *testing.T) {
|
||||
fp.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
provider, err := impl.NewNotifier(&impl.NotifierConfig{
|
||||
ServerUrl: fServerUrl,
|
||||
UserId: fUserId,
|
||||
AccessToken: fAccessToken,
|
||||
RoomId: fRoomId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tester.TestNotify(t, provider, tester.TestNotifyArgs{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SendText posts an m.room.message event (m.text).
|
||||
// Отправляет текстовое сообщение в комнату (m.room.message, msgtype m.text).
|
||||
// REF: https://spec.matrix.org/latest/client-server-api/#put_matrixclientv3roomsroomidsendeventtypetxnid
|
||||
func (c *Client) SendTextMessageToRoom(ctx context.Context, roomId, msgBody string) error {
|
||||
if strings.TrimSpace(roomId) == "" {
|
||||
return fmt.Errorf("sdkerr: unset roomId")
|
||||
}
|
||||
|
||||
txnId := newTransactionId()
|
||||
path := fmt.Sprintf("/_matrix/client/v3/rooms/%s/send/m.room.message/%s",
|
||||
url.PathEscape(roomId), url.PathEscape(txnId))
|
||||
|
||||
payload := map[string]any{
|
||||
"msgtype": "m.text",
|
||||
"body": msgBody,
|
||||
}
|
||||
|
||||
_, err := c.rc.R().
|
||||
SetContext(ctx).
|
||||
SetBody(payload).
|
||||
Put(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sdkerr: api error: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// probeVersions checks that the homeserver exposes the Client-Server API.
|
||||
// Проверяет доступность homeserver (GET /_matrix/client/versions).
|
||||
// REF: https://spec.matrix.org/latest/client-server-api/#get_matrixclientversions
|
||||
func (c *Client) probeVersions() error {
|
||||
_, err := c.rc.R().Get("/_matrix/client/versions")
|
||||
if err != nil {
|
||||
return fmt.Errorf("sdkerr: failed to probe Matrix Client API versions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package matrix
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
|
||||
"github.com/certimate-go/certimate/internal/app"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
rc *resty.Client
|
||||
}
|
||||
|
||||
func NewClient(serverUrl string, optFns ...OptionsFunc) (*Client, error) {
|
||||
opts := &Options{}
|
||||
for _, fn := range optFns {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
if serverUrl == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset serverUrl")
|
||||
}
|
||||
if _, err := url.Parse(serverUrl); err != nil {
|
||||
return nil, fmt.Errorf("sdkerr: invalid serverUrl: %w", err)
|
||||
}
|
||||
if opts.AccessToken == "" {
|
||||
return nil, fmt.Errorf("sdkerr: unset accessToken")
|
||||
}
|
||||
|
||||
baseUrl, _ := resolveBaseUrl(strings.TrimSuffix(serverUrl, "/"))
|
||||
if baseUrl == "" {
|
||||
baseUrl = serverUrl
|
||||
}
|
||||
|
||||
client := &Client{}
|
||||
client.rc = resty.New().
|
||||
SetBaseURL(strings.TrimSuffix(baseUrl, "/")).
|
||||
SetHeader("Authorization", "Bearer "+opts.AccessToken).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetHeader("User-Agent", app.AppUserAgent).
|
||||
if err := client.probeVersions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetTimeout(timeout time.Duration) *Client {
|
||||
c.rc.SetTimeout(timeout)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
|
||||
c.rc.SetTLSClientConfig(config)
|
||||
return c
|
||||
}
|
||||
|
||||
func resolveBaseUrl(serverUrl string) (string, error) {
|
||||
var wkJSON struct {
|
||||
Homeserver struct {
|
||||
BaseURL string `json:"base_url"`
|
||||
} `json:"m.homeserver"`
|
||||
}
|
||||
|
||||
_, err := resty.New().R().
|
||||
SetResult(&wkJSON).
|
||||
Get(serverUrl + "/.well-known/matrix/client")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to discovery Matrix Client API: %w", err)
|
||||
} else if strings.TrimSpace(wkJSON.Homeserver.BaseURL) != "" {
|
||||
return wkJSON.Homeserver.BaseURL, nil
|
||||
} else {
|
||||
return serverUrl, nil
|
||||
}
|
||||
}
|
||||
|
||||
func newTransactionId() string {
|
||||
b := make([]byte, 8)
|
||||
_, _ = rand.Read(b)
|
||||
return fmt.Sprintf("certimate_%d_%s", time.Now().UnixNano(), hex.EncodeToString(b))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package matrix
|
||||
|
||||
type Options struct {
|
||||
UserId string
|
||||
AccessToken string
|
||||
}
|
||||
|
||||
type OptionsFunc func(*Options)
|
||||
|
||||
func WithUserId(userId string) OptionsFunc {
|
||||
return func(o *Options) {
|
||||
o.UserId = userId
|
||||
}
|
||||
}
|
||||
|
||||
func WithAccessToken(accessToken string) OptionsFunc {
|
||||
return func(o *Options) {
|
||||
o.AccessToken = accessToken
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user