diff --git a/README.md b/README.md index b626451..824d5d5 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ To have fancy commands auto-suggestion, go to @BotFather again, select your bot and paste the following: ``` proposals - List proposals and wallets' votes on them -proposals_mute - Mutes a proposal +proposals_mute - Mutes notifications on a chain/proposal +proposals_unmute - Unmutes notifications on a chain/proposal proposals_mutes - List active proposal mutes tally - Show the tally for proposals that are in voting period params - Show chains params related to governance diff --git a/config.example.toml b/config.example.toml index f0d909c..aa02038 100644 --- a/config.example.toml +++ b/config.example.toml @@ -20,6 +20,21 @@ level = "trace" # solition, like ELK. Defaults to false. json = false +# Tracing configuration. +# If enabled, all reports traces are going to be sent to a remote storage +# via OpenTelemetry HTTP protocol. +# You probably don't need this, so feel free to omit this whole block. +[tracing] +# Whether tracing reporting is enabled. Defaults to false. +enabled = false +# OpenTelemetry HTTP host to send traces to +open-telemetry-http-host = "localhost:4123" +# If true, then requests would be done via HTTP rather than HTTPS. Defaults to true. +open-telemetry-http-insecure = true +# OTLP login and password, if needed. +open-telemetry-http-user = "admin" +open-telemetry-http-password = "password" + # Per-chain config. There can be multiple chains. [[chains]] # Chain name, used internally. Required. Should be unique. diff --git a/pkg/mutes/manager.go b/pkg/mutes/manager.go index c7572ec..83d0567 100644 --- a/pkg/mutes/manager.go +++ b/pkg/mutes/manager.go @@ -79,3 +79,9 @@ func (m *Manager) AddMute(mute *Mute) { m.Mutes.AddMute(mute) m.Save() } + +func (m *Manager) DeleteMute(mute *Mute) bool { + found := m.Mutes.DeleteMute(mute) + m.Save() + return found +} diff --git a/pkg/mutes/manager_test.go b/pkg/mutes/manager_test.go index 4186d52..a3285c6 100644 --- a/pkg/mutes/manager_test.go +++ b/pkg/mutes/manager_test.go @@ -98,7 +98,7 @@ func TestMuteManagerSaveWithoutError(t *testing.T) { assert.Empty(t, manager.Mutes.Mutes) } -func TestMuteManagerAddMuteIsMuted(t *testing.T) { +func TestMuteManagerAddAndDeleteMuteIsMuted(t *testing.T) { t.Parallel() log := logger.GetNopLogger() @@ -120,6 +120,20 @@ func TestMuteManagerAddMuteIsMuted(t *testing.T) { Chain: &types.Chain{Name: "chain2"}, Proposal: types.Proposal{ID: "proposal"}, })) + + deleted := manager.DeleteMute(&Mute{ + Chain: "chain", + }) + assert.True(t, deleted) + + assert.False(t, manager.IsEntryMuted(events.VotedEvent{ + Chain: &types.Chain{Name: "chain"}, + Proposal: types.Proposal{ID: "proposal"}, + })) + assert.False(t, manager.IsEntryMuted(events.VotedEvent{ + Chain: &types.Chain{Name: "chain2"}, + Proposal: types.Proposal{ID: "proposal"}, + })) } func TestMuteManagerIsMutedNoPath(t *testing.T) { diff --git a/pkg/mutes/mutes.go b/pkg/mutes/mutes.go index 6c6d946..fca2528 100644 --- a/pkg/mutes/mutes.go +++ b/pkg/mutes/mutes.go @@ -71,3 +71,20 @@ func (m *Mutes) AddMute(mute *Mute) { return !m.IsExpired() }) } + +func (m *Mutes) DeleteMute(mute *Mute) bool { + for index, existingMute := range m.Mutes { + if existingMute.LabelsEqual(mute) { + m.Mutes = append(m.Mutes[:index], m.Mutes[index+1:]...) + m.Mutes = utils.Filter(m.Mutes, func(m *Mute) bool { + return !m.IsExpired() + }) + return true + } + } + + m.Mutes = utils.Filter(m.Mutes, func(m *Mute) bool { + return !m.IsExpired() + }) + return false +} diff --git a/pkg/mutes/mutes_test.go b/pkg/mutes/mutes_test.go index add7cfe..58bc301 100644 --- a/pkg/mutes/mutes_test.go +++ b/pkg/mutes/mutes_test.go @@ -111,3 +111,34 @@ func TestMutesAddsMuteOverride(t *testing.T) { assert.Equal(t, "newcomment", mutes.Mutes[0].Comment) assert.Equal(t, newExpireTime, mutes.Mutes[0].Expires) } + +func TestMutesDeleteMuteNotExisting(t *testing.T) { + t.Parallel() + + mutes := Mutes{ + Mutes: []*Mute{ + {Chain: "chain1", Expires: time.Now().Add(time.Hour)}, + }, + } + + deleted := mutes.DeleteMute(&Mute{Chain: "chain2"}) + assert.False(t, deleted) + assert.Len(t, mutes.Mutes, 1) +} + +func TestMutesDeleteMuteExisting(t *testing.T) { + t.Parallel() + + mutes := Mutes{ + Mutes: []*Mute{ + {Chain: "chain2", ProposalID: "proposal1", Expires: time.Now().Add(time.Hour)}, + {Chain: "chain1", ProposalID: "proposal2", Expires: time.Now().Add(time.Hour)}, + {Chain: "chain1", ProposalID: "proposal1", Expires: time.Now().Add(time.Hour)}, + {Chain: "chain2", ProposalID: "proposal2", Expires: time.Now().Add(time.Hour)}, + }, + } + + deleted := mutes.DeleteMute(&Mute{Chain: "chain1", ProposalID: "proposal1"}) + assert.True(t, deleted) + assert.Len(t, mutes.Mutes, 3) +} diff --git a/pkg/reporters/telegram/delete_mute.go b/pkg/reporters/telegram/delete_mute.go new file mode 100644 index 0000000..827f58f --- /dev/null +++ b/pkg/reporters/telegram/delete_mute.go @@ -0,0 +1,29 @@ +package telegram + +import ( + tele "gopkg.in/telebot.v3" +) + +func (reporter *Reporter) HandleDeleteMute(c tele.Context) error { + reporter.Logger.Info(). + Str("sender", c.Sender().Username). + Str("text", c.Text()). + Msg("Got delete mute query") + + mute, err := ParseMuteDeleteOptions(c.Text(), c) + if err != "" { + return c.Reply("Error deleting mute: " + err) + } + + if found := reporter.MutesManager.DeleteMute(mute); !found { + return c.Reply("Could not find the mute to delete!") + } + + templateRendered, renderErr := reporter.TemplatesManager.Render("mute_deleted", mute) + if renderErr != nil { + reporter.Logger.Error().Err(renderErr).Msg("Error rendering template") + return reporter.BotReply(c, "Error rendering template") + } + + return reporter.BotReply(c, templateRendered) +} diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index d1de4e7..c9845c8 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -80,6 +80,7 @@ func (reporter *Reporter) Init() error { bot.Handle("/start", reporter.HandleHelp) bot.Handle("/help", reporter.HandleHelp) bot.Handle("/proposals_mute", reporter.HandleAddMute) + bot.Handle("/proposals_unmute", reporter.HandleDeleteMute) bot.Handle("/proposals_mutes", reporter.HandleListMutes) bot.Handle("/proposals", reporter.HandleProposals) bot.Handle("/tally", reporter.HandleTally) @@ -199,3 +200,34 @@ func ParseMuteOptions(query string, c tele.Context) (*mutes.Mute, string) { return mute, "" } + +func ParseMuteDeleteOptions(query string, c tele.Context) (*mutes.Mute, string) { + // we only construct mute with chain/proposal to compare, no need to take care + // about the expiration/comment + mute := &mutes.Mute{ + Chain: "", + ProposalID: "", + Expires: time.Now(), + Comment: "", + } + + for index, arg := range c.Args() { + argSplit := strings.SplitN(arg, "=", 2) + if len(argSplit) < 2 { + return nil, fmt.Sprintf( + "Invalid param at position %d: expected an expression like \"[chain=cosmos]\", but got %s", + index+1, + arg, + ) + } + + switch argSplit[0] { + case "chain": + mute.Chain = argSplit[1] + case "proposal": + mute.ProposalID = argSplit[1] + } + } + + return mute, "" +} diff --git a/templates/telegram/mute_deleted.html b/templates/telegram/mute_deleted.html new file mode 100644 index 0000000..769b355 --- /dev/null +++ b/templates/telegram/mute_deleted.html @@ -0,0 +1,11 @@ +Deleted mute with the following params: +{{- if .Chain }} +Chain: {{ .Chain }} +{{- else }} +Chain: all chains +{{- end }} +{{- if .ProposalID }} +Proposal ID: {{ .ProposalID }} +{{- else }} +Proposal ID: all proposals +{{- end }}