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 }}