diff --git a/.mockery.yaml b/.mockery.yaml index bc347d0..8951873 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -1,6 +1,6 @@ --- all: True -dir: mocks +dir: "{{.PackageName}}/mocks" outpkg: "mocks" packages: - github.com/thegeeklab/wp-matrix/plugin: + github.com/thegeeklab/wp-matrix/matrix: diff --git a/Makefile b/Makefile index 2a5a56b..2e58f75 100644 --- a/Makefile +++ b/Makefile @@ -11,13 +11,14 @@ IMPORT := github.com/thegeeklab/$(EXECUTABLE) GO ?= go CWD ?= $(shell pwd) -PACKAGES ?= $(shell go list ./... | grep -Ev 'mocks') +PACKAGES ?= $(shell go list ./... | grep -Ev '/mocks$$') SOURCES ?= $(shell find . -name "*.go" -type f) GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@$(GOFUMPT_PACKAGE_VERSION) GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_PACKAGE_VERSION) XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest GOTESTSUM_PACKAGE ?= gotest.tools/gotestsum@latest +MOCKERY_PACKAGE ?= github.com/vektra/mockery/v2@latest XGO_VERSION := go-1.22.x XGO_TARGETS ?= linux/amd64,linux/arm-6,linux/arm-7,linux/arm64 @@ -65,6 +66,7 @@ lint: golangci-lint .PHONY: generate generate: $(GO) generate $(PACKAGES) + $(GO) run $(MOCKERY_PACKAGE) .PHONY: test test: diff --git a/matrix/api.go b/matrix/api.go new file mode 100644 index 0000000..b278ea4 --- /dev/null +++ b/matrix/api.go @@ -0,0 +1,14 @@ +package matrix + +import ( + "context" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +//nolint:lll +type APIClient interface { + SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (resp *mautrix.RespSendEvent, err error) +} diff --git a/matrix/matrix.go b/matrix/matrix.go new file mode 100644 index 0000000..4ba5bfc --- /dev/null +++ b/matrix/matrix.go @@ -0,0 +1,89 @@ +package matrix + +import ( + "context" + "fmt" + + "github.com/microcosm-cc/bluemonday" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/format" + "maunium.net/go/mautrix/id" +) + +type Client struct { + client APIClient + Message *Message +} + +type Message struct { + client APIClient + Opt MessageOptions +} + +type MessageOptions struct { + RoomID id.RoomID + Message string + TemplateUnsafe bool +} + +// NewClient creates a new Matrix client with the given parameters and joins the specified room. +// It authenticates the user if the userID and token are not provided, and returns a Client struct +// that can be used to send messages to the room. +func NewClient(ctx context.Context, url, roomID, userID, token, username, password string) (*Client, error) { + muid := id.NewUserID(EnsurePrefix("@", userID), url) + + c, err := mautrix.NewClient(url, muid, token) + if err != nil { + return nil, err + } + + if userID == "" || token == "" { + _, err := c.Login( + ctx, + &mautrix.ReqLogin{ + Type: "m.login.password", + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: username}, + Password: password, + InitialDeviceDisplayName: "Woodpecker CI", + StoreCredentials: true, + }, + ) + if err != nil { + return nil, fmt.Errorf("failed to authenticate user: %w", err) + } + } + + joinResp, err := c.JoinRoom(ctx, EnsurePrefix("!", roomID), "", nil) + if err != nil { + return nil, fmt.Errorf("failed to join room: %w", err) + } + + return &Client{ + client: c, + Message: &Message{ + client: c, + Opt: MessageOptions{ + RoomID: joinResp.RoomID, + }, + }, + }, nil +} + +// Send sends a message to the specified room. It sanitizes the message content +// to remove potentially unsafe HTML. +func (m *Message) Send(ctx context.Context) error { + content := format.RenderMarkdown(m.Opt.Message, true, m.Opt.TemplateUnsafe) + + if content.FormattedBody != "" { + content.Body = format.HTMLToMarkdown(bluemonday.UGCPolicy().Sanitize(content.FormattedBody)) + content.FormattedBody = bluemonday.UGCPolicy().Sanitize(content.FormattedBody) + } + + _, err := m.client.SendMessageEvent(ctx, m.Opt.RoomID, event.EventMessage, content) + if err != nil { + return err + } + + return nil +} diff --git a/plugin/matrix_test.go b/matrix/matrix_test.go similarity index 85% rename from plugin/matrix_test.go rename to matrix/matrix_test.go index 3f41231..c33d49a 100644 --- a/plugin/matrix_test.go +++ b/matrix/matrix_test.go @@ -1,4 +1,4 @@ -package plugin +package matrix import ( "context" @@ -6,21 +6,21 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" - "github.com/thegeeklab/wp-matrix/plugin/mocks" + "github.com/thegeeklab/wp-matrix/matrix/mocks" "maunium.net/go/mautrix" "maunium.net/go/mautrix/event" ) -func TestMatrixMessageSend(t *testing.T) { +func TestMessageSend(t *testing.T) { tests := []struct { name string - messageOpt MatrixMessageOpt + messageOpt MessageOptions want event.MessageEventContent wantErr bool }{ { name: "plain text message", - messageOpt: MatrixMessageOpt{ + messageOpt: MessageOptions{ RoomID: "test-room", Message: "hello world", }, @@ -31,7 +31,7 @@ func TestMatrixMessageSend(t *testing.T) { }, { name: "markdown message", - messageOpt: MatrixMessageOpt{ + messageOpt: MessageOptions{ RoomID: "test-room", Message: "**hello world**", }, @@ -44,7 +44,7 @@ func TestMatrixMessageSend(t *testing.T) { }, { name: "html message", - messageOpt: MatrixMessageOpt{ + messageOpt: MessageOptions{ RoomID: "test-room", Message: "hello
world", TemplateUnsafe: true, @@ -58,7 +58,7 @@ func TestMatrixMessageSend(t *testing.T) { }, { name: "safe html message", - messageOpt: MatrixMessageOpt{ + messageOpt: MessageOptions{ RoomID: "test-room", Message: "hello world", TemplateUnsafe: false, @@ -72,7 +72,7 @@ func TestMatrixMessageSend(t *testing.T) { }, { name: "unsafe html message", - messageOpt: MatrixMessageOpt{ + messageOpt: MessageOptions{ RoomID: "test-room", Message: "hello world", TemplateUnsafe: true, @@ -89,8 +89,8 @@ func TestMatrixMessageSend(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - mockClient := mocks.NewMockMautrixClient(t) - m := &MatrixMessage{ + mockClient := mocks.NewMockAPIClient(t) + m := &Message{ Opt: tt.messageOpt, client: mockClient, } diff --git a/plugin/mocks/mock_MautrixClient.go b/matrix/mocks/mock_APIClient.go similarity index 55% rename from plugin/mocks/mock_MautrixClient.go rename to matrix/mocks/mock_APIClient.go index b23a7f6..8e733d7 100644 --- a/plugin/mocks/mock_MautrixClient.go +++ b/matrix/mocks/mock_APIClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.42.1. DO NOT EDIT. +// Code generated by mockery v2.43.0. DO NOT EDIT. package mocks @@ -13,21 +13,21 @@ import ( mock "github.com/stretchr/testify/mock" ) -// MockMautrixClient is an autogenerated mock type for the MautrixClient type -type MockMautrixClient struct { +// MockAPIClient is an autogenerated mock type for the APIClient type +type MockAPIClient struct { mock.Mock } -type MockMautrixClient_Expecter struct { +type MockAPIClient_Expecter struct { mock *mock.Mock } -func (_m *MockMautrixClient) EXPECT() *MockMautrixClient_Expecter { - return &MockMautrixClient_Expecter{mock: &_m.Mock} +func (_m *MockAPIClient) EXPECT() *MockAPIClient_Expecter { + return &MockAPIClient_Expecter{mock: &_m.Mock} } // SendMessageEvent provides a mock function with given fields: ctx, roomID, eventType, contentJSON, extra -func (_m *MockMautrixClient) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { +func (_m *MockAPIClient) SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { _va := make([]interface{}, len(extra)) for _i := range extra { _va[_i] = extra[_i] @@ -63,8 +63,8 @@ func (_m *MockMautrixClient) SendMessageEvent(ctx context.Context, roomID id.Roo return r0, r1 } -// MockMautrixClient_SendMessageEvent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessageEvent' -type MockMautrixClient_SendMessageEvent_Call struct { +// MockAPIClient_SendMessageEvent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendMessageEvent' +type MockAPIClient_SendMessageEvent_Call struct { *mock.Call } @@ -74,12 +74,12 @@ type MockMautrixClient_SendMessageEvent_Call struct { // - eventType event.Type // - contentJSON interface{} // - extra ...mautrix.ReqSendEvent -func (_e *MockMautrixClient_Expecter) SendMessageEvent(ctx interface{}, roomID interface{}, eventType interface{}, contentJSON interface{}, extra ...interface{}) *MockMautrixClient_SendMessageEvent_Call { - return &MockMautrixClient_SendMessageEvent_Call{Call: _e.mock.On("SendMessageEvent", +func (_e *MockAPIClient_Expecter) SendMessageEvent(ctx interface{}, roomID interface{}, eventType interface{}, contentJSON interface{}, extra ...interface{}) *MockAPIClient_SendMessageEvent_Call { + return &MockAPIClient_SendMessageEvent_Call{Call: _e.mock.On("SendMessageEvent", append([]interface{}{ctx, roomID, eventType, contentJSON}, extra...)...)} } -func (_c *MockMautrixClient_SendMessageEvent_Call) Run(run func(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent)) *MockMautrixClient_SendMessageEvent_Call { +func (_c *MockAPIClient_SendMessageEvent_Call) Run(run func(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent)) *MockAPIClient_SendMessageEvent_Call { _c.Call.Run(func(args mock.Arguments) { variadicArgs := make([]mautrix.ReqSendEvent, len(args)-4) for i, a := range args[4:] { @@ -92,23 +92,23 @@ func (_c *MockMautrixClient_SendMessageEvent_Call) Run(run func(ctx context.Cont return _c } -func (_c *MockMautrixClient_SendMessageEvent_Call) Return(resp *mautrix.RespSendEvent, err error) *MockMautrixClient_SendMessageEvent_Call { +func (_c *MockAPIClient_SendMessageEvent_Call) Return(resp *mautrix.RespSendEvent, err error) *MockAPIClient_SendMessageEvent_Call { _c.Call.Return(resp, err) return _c } -func (_c *MockMautrixClient_SendMessageEvent_Call) RunAndReturn(run func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error)) *MockMautrixClient_SendMessageEvent_Call { +func (_c *MockAPIClient_SendMessageEvent_Call) RunAndReturn(run func(context.Context, id.RoomID, event.Type, interface{}, ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error)) *MockAPIClient_SendMessageEvent_Call { _c.Call.Return(run) return _c } -// NewMockMautrixClient creates a new instance of MockMautrixClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// NewMockAPIClient creates a new instance of MockAPIClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockMautrixClient(t interface { +func NewMockAPIClient(t interface { mock.TestingT Cleanup(func()) -}) *MockMautrixClient { - mock := &MockMautrixClient{} +}) *MockAPIClient { + mock := &MockAPIClient{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/plugin/util.go b/matrix/util.go similarity index 95% rename from plugin/util.go rename to matrix/util.go index 9299b3d..63905ce 100644 --- a/plugin/util.go +++ b/matrix/util.go @@ -1,4 +1,4 @@ -package plugin +package matrix import "strings" diff --git a/plugin/util_test.go b/matrix/util_test.go similarity index 98% rename from plugin/util_test.go rename to matrix/util_test.go index be1e7b8..d79d4dc 100644 --- a/plugin/util_test.go +++ b/matrix/util_test.go @@ -1,4 +1,4 @@ -package plugin +package matrix import "testing" diff --git a/plugin/impl.go b/plugin/impl.go index 4080255..7accc74 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -12,9 +12,8 @@ import ( "fmt" "github.com/rs/zerolog/log" + "github.com/thegeeklab/wp-matrix/matrix" "github.com/thegeeklab/wp-plugin-go/v2/template" - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/id" ) var ErrAuthSourceNotSet = errors.New("either username and password or userid and accesstoken are required") @@ -44,44 +43,26 @@ func (p *Plugin) Validate() error { // Execute provides the implementation of the plugin. func (p *Plugin) Execute() error { - muid := id.NewUserID(EnsurePrefix("@", p.Settings.UserID), p.Settings.Homeserver) - - matrix, err := mautrix.NewClient(p.Settings.Homeserver, muid, p.Settings.AccessToken) - if err != nil { - return fmt.Errorf("failed to initialize client: %w", err) - } - - if p.Settings.UserID == "" || p.Settings.AccessToken == "" { - _, err := matrix.Login( - p.Network.Context, - &mautrix.ReqLogin{ - Type: "m.login.password", - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: p.Settings.Username}, - Password: p.Settings.Password, - InitialDeviceDisplayName: "Woodpecker CI", - StoreCredentials: true, - }, - ) - if err != nil { - return fmt.Errorf("failed to authenticate user: %w", err) - } - } - - log.Info().Msg("logged in successfully") - - joinResp, err := matrix.JoinRoom(p.Network.Context, EnsurePrefix("!", p.Settings.RoomID), "", nil) - if err != nil { - return fmt.Errorf("failed to join room: %w", err) - } - msg, err := p.CreateMessage() if err != nil { return fmt.Errorf("failed to create message: %w", err) } - client := NewMatrixClient(matrix) - client.Message.Opt = MatrixMessageOpt{ - RoomID: joinResp.RoomID, + client, err := matrix.NewClient( + p.Network.Context, + p.Settings.Homeserver, + p.Settings.RoomID, + p.Settings.UserID, + p.Settings.AccessToken, + p.Settings.Username, + p.Settings.Password, + ) + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + client.Message.Opt = matrix.MessageOptions{ + RoomID: client.Message.Opt.RoomID, Message: msg, TemplateUnsafe: p.Settings.TemplateUnsafe, } diff --git a/plugin/matrix.go b/plugin/matrix.go deleted file mode 100644 index 82b3a94..0000000 --- a/plugin/matrix.go +++ /dev/null @@ -1,62 +0,0 @@ -package plugin - -import ( - "context" - "fmt" - - "github.com/microcosm-cc/bluemonday" - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/format" - "maunium.net/go/mautrix/id" -) - -//nolint:lll -type MautrixClient interface { - SendMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, extra ...mautrix.ReqSendEvent) (resp *mautrix.RespSendEvent, err error) -} - -type MatrixClient struct { - client MautrixClient - Message *MatrixMessage -} - -type MatrixMessage struct { - client MautrixClient - Opt MatrixMessageOpt -} - -type MatrixMessageOpt struct { - RoomID id.RoomID - Message string - TemplateUnsafe bool -} - -// NewMatrixClient creates a new MatrixClient instance with the provided mautrix.Client. -func NewMatrixClient(client *mautrix.Client) *MatrixClient { - return &MatrixClient{ - client: client, - Message: &MatrixMessage{ - client: client, - Opt: MatrixMessageOpt{}, - }, - } -} - -// Send sends a message to the specified room. It sanitizes the message content -// to remove potentially unsafe HTML. -func (m *MatrixMessage) Send(ctx context.Context) error { - content := format.RenderMarkdown(m.Opt.Message, true, m.Opt.TemplateUnsafe) - - if content.FormattedBody != "" { - content.Body = format.HTMLToMarkdown(bluemonday.UGCPolicy().Sanitize(content.FormattedBody)) - content.FormattedBody = bluemonday.UGCPolicy().Sanitize(content.FormattedBody) - } - - _, err := m.client.SendMessageEvent(ctx, m.Opt.RoomID, event.EventMessage, content) - if err != nil { - return fmt.Errorf("failed to send message: %w", err) - } - - return nil -} diff --git a/plugin/plugin.go b/plugin/plugin.go index 4dbfabf..de5a3a1 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -13,7 +13,6 @@ import ( "github.com/urfave/cli/v2" ) -//go:generate mockery //go:generate go run ../internal/doc/main.go -output=../docs/data/data-raw.yaml //nolint:lll