From fb01b976e592dfb93421304b2f34e005fe5b3c84 Mon Sep 17 00:00:00 2001 From: Sergey <83376337+freak12techno@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:34:06 +0300 Subject: [PATCH] feat: allow confirming silence and clearing the keyboard (#65) --- pkg/app/app.go | 1 + pkg/app/silences_create.go | 6 +- pkg/app/silences_create_test.go | 17 +++-- pkg/app/silences_delete.go | 10 ++- pkg/app/silences_delete_test.go | 58 +++++++++++++++ pkg/app/utils.go | 24 ++++++ pkg/app/utils_test.go | 126 ++++++++++++++++++++++++++++++++ pkg/constants/constants.go | 1 + 8 files changed, 233 insertions(+), 10 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index 5ccd1ad..d90e957 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -102,6 +102,7 @@ func (a *App) Start() { a.Bot.Handle("\f"+constants.GrafanaRenderChooseDashboardPrefix, a.HandleRenderChooseDashboardFromCallback) a.Bot.Handle("\f"+constants.GrafanaRenderChoosePanelPrefix, a.HandleRenderPanelChoosePanelFromCallback) a.Bot.Handle("\f"+constants.GrafanaRenderRenderPanelPrefix, a.HandleRenderPanelFromCallback) + a.Bot.Handle("\f"+constants.ClearKeyboardPrefix, a.ClearKeyboard) for _, alertSourceWithSilenceManager := range a.AlertSourcesWithSilenceManager { alertSource := alertSourceWithSilenceManager.AlertSource diff --git a/pkg/app/silences_create.go b/pkg/app/silences_create.go index 707b30e..a7f8a9e 100644 --- a/pkg/app/silences_create.go +++ b/pkg/app/silences_create.go @@ -3,6 +3,7 @@ package app import ( "fmt" "main/pkg/alert_source" + "main/pkg/constants" "main/pkg/silence_manager" "main/pkg/types" "main/pkg/types/render" @@ -139,9 +140,12 @@ func (a *App) HandleNewSilenceGeneric( menu := &tele.ReplyMarkup{ResizeKeyboard: true} menu.Inline(menu.Row(menu.Data( + "✅Confirm", + constants.ClearKeyboardPrefix, + )), menu.Row(menu.Data( "❌Unsilence", silenceManager.Prefixes().Unsilence, - silence.ID, + silence.ID+" 1", ))) return a.ReplyRender(c, "silences_create", render.RenderStruct{ diff --git a/pkg/app/silences_create_test.go b/pkg/app/silences_create_test.go index 414c79b..60ff610 100644 --- a/pkg/app/silences_create_test.go +++ b/pkg/app/silences_create_test.go @@ -283,13 +283,16 @@ func TestAppCreateSilenceOk(t *testing.T) { "https://api.telegram.org/botxxx:yyy/sendMessage", types.TelegramResponseHasBytesAndMarkup(assets.GetBytesOrPanic("responses/silence-create-ok.html"), types.TelegramInlineKeyboardResponse{ InlineKeyboard: [][]types.TelegramInlineKeyboard{ - { - { - Unique: "grafana_unsilence_", - Text: "❌Unsilence", - CallbackData: "\fgrafana_unsilence_|4de5faa2-8c0c-4c66-bd31-25c3bf5fa231", - }, - }, + {{ + Unique: "clear_keyboard_", + Text: "✅Confirm", + CallbackData: "\fclear_keyboard_", + }}, + {{ + Unique: "grafana_unsilence_", + Text: "❌Unsilence", + CallbackData: "\fgrafana_unsilence_|4de5faa2-8c0c-4c66-bd31-25c3bf5fa231 1", + }}, }, }), httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-send-message-ok.json")), diff --git a/pkg/app/silences_delete.go b/pkg/app/silences_delete.go index 9676fce..9772221 100644 --- a/pkg/app/silences_delete.go +++ b/pkg/app/silences_delete.go @@ -40,8 +40,14 @@ func (a *App) HandleCallbackDeleteSilence(silenceManager silence_manager.Silence callback := c.Callback() - a.RemoveKeyboardItemByCallback(c, callback) - return a.HandleDeleteSilenceGeneric(c, silenceManager, callback.Data) + dataSplit := strings.Split(callback.Data, " ") + if len(dataSplit) == 2 { + _ = a.ClearKeyboard(c) + } else { + a.RemoveKeyboardItemByCallback(c, callback) + } + + return a.HandleDeleteSilenceGeneric(c, silenceManager, dataSplit[0]) } } diff --git a/pkg/app/silences_delete_test.go b/pkg/app/silences_delete_test.go index e1cf527..533ecf2 100644 --- a/pkg/app/silences_delete_test.go +++ b/pkg/app/silences_delete_test.go @@ -407,3 +407,61 @@ func TestAppDeleteSilenceCallbackOk(t *testing.T) { err := app.HandleCallbackDeleteSilence(app.AlertSourcesWithSilenceManager[0].SilenceManager)(ctx) require.NoError(t, err) } + +//nolint:paralleltest // disabled +func TestAppDeleteSilenceCallbackOkWithDeleteKeyboard(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + config := &configPkg.Config{ + Timezone: "Etc/GMT", + Log: configPkg.LogConfig{LogLevel: "info"}, + Telegram: configPkg.TelegramConfig{Token: "xxx:yyy", Admins: []int64{1, 2}}, + Grafana: configPkg.GrafanaConfig{ + URL: "https://example.com", + Silences: null.BoolFrom(true), + }, + Alertmanager: nil, + Prometheus: nil, + } + + httpmock.RegisterResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/getMe", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json"))) + + httpmock.RegisterResponder( + "GET", + "https://example.com/api/alertmanager/grafana/api/v2/silences", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("alertmanager-silences-ok.json"))) + + httpmock.RegisterMatcherResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/sendMessage", + types.TelegramResponseHasText("Silence is not found by ID or matchers: 123"), + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-send-message-ok.json")), + ) + + app := NewApp(config, "1.2.3") + ctx := app.Bot.NewContext(tele.Update{ + ID: 1, + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_unsilence 123", + Chat: &tele.Chat{ID: 2}, + }, + Callback: &tele.Callback{ + Sender: &tele.User{Username: "testuser"}, + Unique: "\f" + constants.GrafanaUnsilencePrefix, + Data: "123 1", + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_unsilence 123 1", + Chat: &tele.Chat{ID: 2}, + }, + }, + }) + + err := app.HandleCallbackDeleteSilence(app.AlertSourcesWithSilenceManager[0].SilenceManager)(ctx) + require.NoError(t, err) +} diff --git a/pkg/app/utils.go b/pkg/app/utils.go index 4116469..cdac60b 100644 --- a/pkg/app/utils.go +++ b/pkg/app/utils.go @@ -45,6 +45,30 @@ func (a *App) RemoveKeyboardItemByCallback(c tele.Context, callback *tele.Callba } } +func (a *App) ClearKeyboard(c tele.Context) error { + a.Logger.Info(). + Str("sender", c.Sender().Username). + Msg("Got new clear keyboard query") + + callback := c.Callback() + if callback.Message == nil || callback.Message.ReplyMarkup == nil { + return nil + } + + if _, err := a.Bot.EditReplyMarkup( + callback.Message, + nil, + ); err != nil { + a.Logger.Error(). + Str("sender", c.Sender().Username). + Err(err). + Msg("Error clearing keyboard when editing a callback") + return err + } + + return nil +} + func (a *App) GenerateSilenceForAlert( c tele.Context, groups types.GrafanaAlertGroups, diff --git a/pkg/app/utils_test.go b/pkg/app/utils_test.go index 675ca81..ae3afc5 100644 --- a/pkg/app/utils_test.go +++ b/pkg/app/utils_test.go @@ -263,3 +263,129 @@ func TestAppRemoveKeyboardItemFailedToDelete(t *testing.T) { app.RemoveKeyboardItemByCallback(ctx, ctx.Callback()) } + +//nolint:paralleltest // disabled +func TestAppClearKeyboardFailedToDelete(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + config := &configPkg.Config{ + Timezone: "Etc/GMT", + Log: configPkg.LogConfig{LogLevel: "info"}, + Telegram: configPkg.TelegramConfig{Token: "xxx:yyy", Admins: []int64{1, 2}}, + Grafana: configPkg.GrafanaConfig{URL: "https://example.com", User: "admin", Password: "admin"}, + Alertmanager: nil, + Prometheus: nil, + } + + httpmock.RegisterResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/getMe", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json"))) + + httpmock.RegisterResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/editMessageReplyMarkup", + httpmock.NewErrorResponder(errors.New("custom error")), + ) + + app := NewApp(config, "1.2.3") + ctx := app.Bot.NewContext(tele.Update{ + ID: 1, + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_silence 4", + Chat: &tele.Chat{ID: 2}, + }, + Callback: &tele.Callback{ + Sender: &tele.User{Username: "testuser"}, + Unique: "\f" + constants.GrafanaSilencePrefix, + Data: "48h 123", + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_silence", + Chat: &tele.Chat{ID: 2}, + ReplyMarkup: &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + { + { + Text: "text", + Unique: "\f" + constants.GrafanaSilencePrefix, + Data: constants.GrafanaSilencePrefix + "|48h 123", + }, + { + Text: "text", + Unique: constants.GrafanaUnsilencePrefix, + Data: "random", + }, + }, + }}, + }, + }, + }) + + err := app.ClearKeyboard(ctx) + require.Error(t, err) + require.ErrorContains(t, err, "custom error") +} + +//nolint:paralleltest // disabled +func TestAppClearKeyboardOk(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + config := &configPkg.Config{ + Timezone: "Etc/GMT", + Log: configPkg.LogConfig{LogLevel: "info"}, + Telegram: configPkg.TelegramConfig{Token: "xxx:yyy", Admins: []int64{1, 2}}, + Grafana: configPkg.GrafanaConfig{URL: "https://example.com", User: "admin", Password: "admin"}, + Alertmanager: nil, + Prometheus: nil, + } + + httpmock.RegisterResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/getMe", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-bot-ok.json"))) + + httpmock.RegisterResponder( + "POST", + "https://api.telegram.org/botxxx:yyy/editMessageReplyMarkup", + httpmock.NewBytesResponder(200, assets.GetBytesOrPanic("telegram-send-message-ok.json"))) + + app := NewApp(config, "1.2.3") + ctx := app.Bot.NewContext(tele.Update{ + ID: 1, + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_silence 4", + Chat: &tele.Chat{ID: 2}, + }, + Callback: &tele.Callback{ + Sender: &tele.User{Username: "testuser"}, + Unique: "\f" + constants.GrafanaSilencePrefix, + Data: "48h 123", + Message: &tele.Message{ + Sender: &tele.User{Username: "testuser"}, + Text: "/grafana_silence", + Chat: &tele.Chat{ID: 2}, + ReplyMarkup: &tele.ReplyMarkup{InlineKeyboard: [][]tele.InlineButton{ + { + { + Text: "text", + Unique: "\f" + constants.GrafanaSilencePrefix, + Data: constants.GrafanaSilencePrefix + "|48h 123", + }, + { + Text: "text", + Unique: constants.GrafanaUnsilencePrefix, + Data: "random", + }, + }, + }}, + }, + }, + }) + + err := app.ClearKeyboard(ctx) + require.NoError(t, err) +} diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index d4cd945..bade3d1 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -31,4 +31,5 @@ const ( GrafanaRenderChooseDashboardPrefix = "grafana_render_choose_dashboard_" GrafanaRenderChoosePanelPrefix = "grafana_render_choose_panel_" GrafanaRenderRenderPanelPrefix = "grafana_render_render_panel" + ClearKeyboardPrefix = "clear_keyboard_" )