diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index e9121729..66aa8172 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -995,10 +995,7 @@ func (s *ServerCommand) makeNotifyDestinations(authenticator *auth.Service) ([]n } if contains("slack", s.Notify.Admins) { - slack, err := notify.NewSlack(s.Notify.Slack.Token, s.Notify.Slack.Channel) - if err != nil { - return destinations, fmt.Errorf("failed to create slack notification destination: %w", err) - } + slack := notify.NewSlack(s.Notify.Slack.Token, s.Notify.Slack.Channel) destinations = append(destinations, slack) } diff --git a/backend/app/notify/slack.go b/backend/app/notify/slack.go index 9cee44ef..43783689 100644 --- a/backend/app/notify/slack.go +++ b/backend/app/notify/slack.go @@ -3,40 +3,31 @@ package notify import ( "context" "fmt" + "net/url" log "github.com/go-pkgz/lgr" - "github.com/slack-go/slack" + ntf "github.com/go-pkgz/notify" ) // Slack implements notify.Destination for Slack type Slack struct { - channelID string + *ntf.Slack + channelName string - client *slack.Client } // NewSlack makes Slack bot for notifications -func NewSlack(token, channelName string, opts ...slack.Option) (*Slack, error) { +func NewSlack(token, channelName string) *Slack { + log.Printf("[DEBUG] create new slack notifier for chan %s", channelName) if channelName == "" { channelName = "general" } - client := slack.New(token, opts...) - res := &Slack{client: client, channelName: channelName} - - channelID, err := res.findChannelIDByName(channelName) - if err != nil { - return nil, fmt.Errorf("can not find slack channel '"+channelName+"': %w", err) - } - - res.channelID = channelID - log.Printf("[DEBUG] create new slack notifier for chan %s", channelID) - - return res, nil + return &Slack{Slack: ntf.NewSlack(token), channelName: channelName} } // Send to Slack channel -func (t *Slack) Send(ctx context.Context, req Request) error { +func (s *Slack) Send(ctx context.Context, req Request) error { log.Printf("[DEBUG] send slack notification, comment id %s", req.Comment.ID) user := req.Comment.User.Name @@ -49,47 +40,22 @@ func (t *Slack) Send(ctx context.Context, req Request) error { title = "↦ " + req.Comment.PostTitle } - _, _, err := t.client.PostMessageContext(ctx, t.channelID, - slack.MsgOptionText("New comment from "+user, false), - slack.MsgOptionAttachments( - slack.Attachment{ - TitleLink: req.Comment.Locator.URL + uiNav + req.Comment.ID, - Title: title, - Text: req.Comment.Orig, - }, - ), + destination := fmt.Sprintf( + "slack:%s?title=%s&attachmentText=%s&titleLink=%s", + s.channelName, + url.QueryEscape(title), + url.QueryEscape(req.Comment.Orig), + url.QueryEscape(req.Comment.Locator.URL+uiNav+req.Comment.ID), ) - return err + return s.Slack.Send(ctx, destination, "New comment from "+user) } // SendVerification is not implemented for Slack -func (t *Slack) SendVerification(_ context.Context, _ VerificationRequest) error { +func (s *Slack) SendVerification(_ context.Context, _ VerificationRequest) error { return nil } -func (t *Slack) String() string { - return "slack: " + t.channelName + " (" + t.channelID + ")" -} - -func (t *Slack) findChannelIDByName(name string) (string, error) { - params := slack.GetConversationsParameters{} - for { - chans, next, err := t.client.GetConversations(¶ms) - if err != nil { - return "", err - } - - for _, channel := range chans { - if channel.Name == name { - return channel.ID, nil - } - } - - if next == "" { - break - } - params.Cursor = next - } - return "", fmt.Errorf("no such channel") +func (s *Slack) String() string { + return s.Slack.String() + " for channel " + s.channelName + "" } diff --git a/backend/app/notify/slack_test.go b/backend/app/notify/slack_test.go index ba9c796f..1ff87f8a 100644 --- a/backend/app/notify/slack_test.go +++ b/backend/app/notify/slack_test.go @@ -2,162 +2,39 @@ package notify import ( "context" - "log" - "net/http" - "net/http/httptest" "testing" - "github.com/go-chi/chi/v5" - "github.com/slack-go/slack" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/umputun/remark42/backend/app/store" ) func TestSlack_New(t *testing.T) { - ts := newMockSlackServer() - defer ts.Close() - - tb, err := ts.newClient("general") - assert.NoError(t, err) - assert.NotNil(t, tb) - assert.Equal(t, "C12345678", tb.channelID) - - _, err = ts.newClient("unknown-channel") - require.Error(t, err) - assert.Contains(t, err.Error(), "no such channel") + ts := NewSlack("", "") + assert.NotNil(t, ts) + assert.Equal(t, "general", ts.channelName) } func TestSlack_Send(t *testing.T) { - ts := newMockSlackServer() - defer ts.Close() + ts := NewSlack("", "") - tb, err := ts.newClient("general") - assert.NoError(t, err) - assert.NotNil(t, tb) - - c := store.Comment{Text: "some text", ParentID: "1", ID: "999"} + c := store.Comment{PostTitle: "test title", Text: "some text", ParentID: "1", ID: "999"} c.User.Name = "from" cp := store.Comment{Text: "some parent text"} cp.User.Name = "to" - err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) - assert.NoError(t, err) - c.PostTitle = "test title" - err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) - assert.NoError(t, err) - - err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) - assert.NoError(t, err) - c.PostTitle = "[test title]" - err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) - assert.NoError(t, err) - - tb, err = ts.newClient("general") - assert.NoError(t, err) - ts.isServerDown = true - err = tb.Send(context.TODO(), Request{Comment: c, parent: cp}) - require.Error(t, err) - assert.Contains(t, err.Error(), "slack server error", "send on broken client") + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := ts.Send(ctx, Request{Comment: c, parent: cp}) + assert.Error(t, err) } func TestSlack_Name(t *testing.T) { - ts := newMockSlackServer() - defer ts.Close() - - tb, err := ts.newClient("general") - assert.NoError(t, err) - assert.NotNil(t, tb) - assert.Equal(t, "slack: general (C12345678)", tb.String()) + tb := NewSlack("", "test-channel") + assert.Equal(t, "slack notifications destination for channel test-channel", tb.String()) } func TestSlack_SendVerification(t *testing.T) { - ts := newMockSlackServer() - defer ts.Close() - - tb, err := ts.newClient("general") - assert.NoError(t, err) - assert.NotNil(t, tb) - - err = tb.SendVerification(context.TODO(), VerificationRequest{}) - assert.NoError(t, err) -} - -type mockSlackServer struct { - *httptest.Server - isServerDown bool -} - -func (ts *mockSlackServer) newClient(channelName string) (*Slack, error) { - return NewSlack("any-token", channelName, slack.OptionAPIURL(ts.URL+"/")) -} - -func newMockSlackServer() *mockSlackServer { - mockServer := mockSlackServer{} - router := chi.NewRouter() - router.Post("/conversations.list", func(w http.ResponseWriter, r *http.Request) { - s := `{ - "ok": true, - "channels": [ - { - "id": "C12345678", - "name": "general", - "is_channel": true, - "is_group": false, - "is_im": false, - "created": 1503888888, - "is_archived": false, - "is_general": false, - "unlinked": 0, - "name_normalized": "random", - "is_shared": false, - "parent_conversation": null, - "creator": "U12345678", - "is_ext_shared": false, - "is_org_shared": false, - "pending_shared": [], - "pending_connected_team_ids": [], - "is_pending_ext_shared": false, - "is_member": false, - "is_private": false, - "is_mpim": false, - "previous_names": [], - "num_members": 1 - } - ], - "response_metadata": { - "next_cursor": "" - } - }` - _, _ = w.Write([]byte(s)) - }) - - router.Post("/chat.postMessage", func(w http.ResponseWriter, r *http.Request) { - if mockServer.isServerDown { - w.WriteHeader(500) - } else { - s := `{ - "ok": true, - "channel": "C12345678", - "ts": "1617008342.000100", - "message": { - "type": "message", - "subtype": "bot_message", - "text": "wowo", - "ts": "1617008342.000100", - "username": "slackbot", - "bot_id": "B12345678" - } - }` - _, _ = w.Write([]byte(s)) - } - }) - - router.NotFound(func(w http.ResponseWriter, r *http.Request) { - log.Printf("..... 404 for %s .....\n", r.URL) - }) - - mockServer.Server = httptest.NewServer(router) - return &mockServer + ts := NewSlack("", "") + assert.NoError(t, ts.SendVerification(context.Background(), VerificationRequest{})) }