From 6c7c98735ca92691871f848d695c293d9550e47d Mon Sep 17 00:00:00 2001 From: Sergey <83376337+freak12techno@users.noreply.github.com> Date: Mon, 24 Apr 2023 23:32:51 +0300 Subject: [PATCH] feat: refactor events (#45) * feat: refactor events * chore: fix linting * chore: fix tests * chore: serialize errors to json correctly * chore: fix tests --- pkg/events/not_voted.go | 32 ++++++ pkg/events/proposals_query_error.go | 18 ++++ pkg/events/revoted.go | 34 ++++++ pkg/events/vote_query_error.go | 20 ++++ pkg/events/voted.go | 33 ++++++ pkg/report/entry/entry.go | 18 ++++ pkg/report/generator.go | 65 ++++++------ pkg/report/generator_test.go | 53 ++++++---- pkg/reporters/pagerduty/pagerduty.go | 100 ++++++++++-------- pkg/reporters/reporter.go | 48 +-------- pkg/reporters/telegram/add_mute.go | 2 +- pkg/reporters/telegram/help.go | 2 +- pkg/reporters/telegram/list_mutes.go | 2 +- pkg/reporters/telegram/list_proposals.go | 2 +- pkg/reporters/telegram/telegram.go | 57 +++++----- pkg/state/generator.go | 2 +- pkg/state/state.go | 12 +-- pkg/state/state_test.go | 4 +- pkg/types/error.go | 24 +++++ pkg/types/responses.go | 4 + pkg/types/types.go | 10 -- templates/telegram/not_voted.html | 8 +- ..._error.html => proposals_query_error.html} | 2 +- templates/telegram/revoted.html | 12 +-- templates/telegram/vote_query_error.html | 4 +- templates/telegram/voted.html | 10 +- 26 files changed, 368 insertions(+), 210 deletions(-) create mode 100644 pkg/events/not_voted.go create mode 100644 pkg/events/proposals_query_error.go create mode 100644 pkg/events/revoted.go create mode 100644 pkg/events/vote_query_error.go create mode 100644 pkg/events/voted.go create mode 100644 pkg/report/entry/entry.go create mode 100644 pkg/types/error.go rename templates/telegram/{proposal_query_error.html => proposals_query_error.html} (80%) diff --git a/pkg/events/not_voted.go b/pkg/events/not_voted.go new file mode 100644 index 0000000..e8d28ec --- /dev/null +++ b/pkg/events/not_voted.go @@ -0,0 +1,32 @@ +package events + +import ( + configTypes "main/pkg/config/types" + "main/pkg/types" +) + +type NotVotedEvent struct { + Chain *configTypes.Chain + Wallet *configTypes.Wallet + Proposal types.Proposal +} + +func (e NotVotedEvent) Name() string { + return "not_voted" +} + +func (e NotVotedEvent) IsAlert() bool { + return true +} + +func (e NotVotedEvent) GetChain() *configTypes.Chain { + return e.Chain +} + +func (e NotVotedEvent) GetProposal() types.Proposal { + return e.Proposal +} + +func (e NotVotedEvent) GetWallet() *configTypes.Wallet { + return e.Wallet +} diff --git a/pkg/events/proposals_query_error.go b/pkg/events/proposals_query_error.go new file mode 100644 index 0000000..25c71c3 --- /dev/null +++ b/pkg/events/proposals_query_error.go @@ -0,0 +1,18 @@ +package events + +import ( + configTypes "main/pkg/config/types" +) + +type ProposalsQueryErrorEvent struct { + Chain *configTypes.Chain + Error error +} + +func (e ProposalsQueryErrorEvent) Name() string { + return "proposals_query_error" +} + +func (e ProposalsQueryErrorEvent) IsAlert() bool { + return false +} diff --git a/pkg/events/revoted.go b/pkg/events/revoted.go new file mode 100644 index 0000000..ad2b1ae --- /dev/null +++ b/pkg/events/revoted.go @@ -0,0 +1,34 @@ +package events + +import ( + configTypes "main/pkg/config/types" + "main/pkg/types" +) + +type RevotedEvent struct { + Chain *configTypes.Chain + Wallet *configTypes.Wallet + Proposal types.Proposal + Vote *types.Vote + OldVote *types.Vote +} + +func (e RevotedEvent) Name() string { + return "revoted" +} + +func (e RevotedEvent) IsAlert() bool { + return true +} + +func (e RevotedEvent) GetChain() *configTypes.Chain { + return e.Chain +} + +func (e RevotedEvent) GetProposal() types.Proposal { + return e.Proposal +} + +func (e RevotedEvent) GetWallet() *configTypes.Wallet { + return e.Wallet +} diff --git a/pkg/events/vote_query_error.go b/pkg/events/vote_query_error.go new file mode 100644 index 0000000..c87925c --- /dev/null +++ b/pkg/events/vote_query_error.go @@ -0,0 +1,20 @@ +package events + +import ( + configTypes "main/pkg/config/types" + "main/pkg/types" +) + +type VoteQueryError struct { + Chain *configTypes.Chain + Proposal types.Proposal + Error error +} + +func (e VoteQueryError) Name() string { + return "vote_query_error" +} + +func (e VoteQueryError) IsAlert() bool { + return true +} diff --git a/pkg/events/voted.go b/pkg/events/voted.go new file mode 100644 index 0000000..d3b1599 --- /dev/null +++ b/pkg/events/voted.go @@ -0,0 +1,33 @@ +package events + +import ( + configTypes "main/pkg/config/types" + "main/pkg/types" +) + +type VotedEvent struct { + Chain *configTypes.Chain + Wallet *configTypes.Wallet + Proposal types.Proposal + Vote *types.Vote +} + +func (e VotedEvent) Name() string { + return "voted" +} + +func (e VotedEvent) IsAlert() bool { + return true +} + +func (e VotedEvent) GetChain() *configTypes.Chain { + return e.Chain +} + +func (e VotedEvent) GetProposal() types.Proposal { + return e.Proposal +} + +func (e VotedEvent) GetWallet() *configTypes.Wallet { + return e.Wallet +} diff --git a/pkg/report/entry/entry.go b/pkg/report/entry/entry.go new file mode 100644 index 0000000..870824c --- /dev/null +++ b/pkg/report/entry/entry.go @@ -0,0 +1,18 @@ +package entry + +import ( + configTypes "main/pkg/config/types" + "main/pkg/types" +) + +type ReportEntry interface { + Name() string + IsAlert() bool // only voted/not_voted are alerts, required for PagerDuty +} + +type ReportEntryNotError interface { + ReportEntry + GetChain() *configTypes.Chain + GetWallet() *configTypes.Wallet + GetProposal() types.Proposal +} diff --git a/pkg/report/generator.go b/pkg/report/generator.go index 612bbd0..e16d14b 100644 --- a/pkg/report/generator.go +++ b/pkg/report/generator.go @@ -1,13 +1,12 @@ package report import ( - "time" - configTypes "main/pkg/config/types" + "main/pkg/events" + "main/pkg/report/entry" "main/pkg/reporters" "main/pkg/state" "main/pkg/tendermint" - "main/pkg/types" "github.com/rs/zerolog" ) @@ -32,20 +31,17 @@ func NewReportGenerator( } func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Report { - entries := []reporters.ReportEntry{} + entries := []entry.ReportEntry{} for chainName, chainInfo := range newState.ChainInfos { if chainInfo.HasProposalsError() { g.Logger.Debug(). Str("chain", chainName). Msg("Error querying for proposals - sending an alert") - entry := reporters.ReportEntry{ - Chain: g.Chains.FindByName(chainName), - ProposalVoteEndingTime: time.Now(), - Type: types.ProposalQueryError, - Value: chainInfo.ProposalsError, - } - entries = append(entries, entry) + entries = append(entries, events.ProposalsQueryErrorEvent{ + Chain: g.Chains.FindByName(chainName), + Error: chainInfo.ProposalsError, + }) continue } @@ -60,15 +56,6 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep oldVote, _, _ := oldState.GetVoteAndProposal(chainName, proposalID, wallet) newVote, proposal, _ := newState.GetVoteAndProposal(chainName, proposalID, wallet) - entry := reporters.ReportEntry{ - Chain: g.Chains.FindByName(chainName), - Wallet: newVote.Wallet, - ProposalID: proposalID, - ProposalTitle: proposal.Content.Title, - ProposalDescription: proposal.Content.Description, - ProposalVoteEndingTime: proposal.VotingEndTime, - } - // Error querying for vote - need to notify via Telegram. if newVote.IsError() { g.Logger.Debug(). @@ -76,10 +63,12 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep Str("proposal", proposalID). Str("wallet", wallet). Msg("Error querying for vote - sending an alert") - entry.Type = types.VoteQueryError - entry.Value = newVote.Error + entries = append(entries, events.VoteQueryError{ + Chain: g.Chains.FindByName(chainName), + Proposal: proposal, + Error: newVote.Error, + }) - entries = append(entries, entry) continue } @@ -90,25 +79,28 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep Str("proposal", proposalID). Str("wallet", wallet). Msg("Wallet hasn't voted now - sending an alert") - entry.Type = types.NotVoted - entries = append(entries, entry) + entries = append(entries, events.NotVotedEvent{ + Chain: g.Chains.FindByName(chainName), + Wallet: newVote.Wallet, + Proposal: proposal, + }) continue } // Hasn't voted before but voted now - need to close alert/notify about new vote. if newVote.HasVoted() && !oldVote.HasVoted() { - vote := *newVote.Vote - g.Logger.Debug(). Str("chain", chainName). Str("proposal", proposal.ProposalID). Str("wallet", wallet). Msg("Wallet hasn't voted before but voted now - closing an alert") - entry.Type = types.Voted - entry.Value = vote.Option - - entries = append(entries, entry) + entries = append(entries, events.VotedEvent{ + Chain: g.Chains.FindByName(chainName), + Wallet: newVote.Wallet, + Proposal: proposal, + Vote: newVote.Vote, + }) continue } @@ -119,11 +111,14 @@ func (g *Generator) GenerateReport(oldState, newState state.State) reporters.Rep Str("proposal", proposal.ProposalID). Str("wallet", wallet). Msg("Wallet changed its vote - sending an alert") - entry.Type = types.Revoted - entry.Value = newVote.Vote.Option - entry.OldValue = oldVote.Vote.Option - entries = append(entries, entry) + entries = append(entries, events.RevotedEvent{ + Chain: g.Chains.FindByName(chainName), + Wallet: newVote.Wallet, + Proposal: proposal, + Vote: newVote.Vote, + OldVote: oldVote.Vote, + }) } } } diff --git a/pkg/report/generator_test.go b/pkg/report/generator_test.go index b60c68e..0d5c408 100644 --- a/pkg/report/generator_test.go +++ b/pkg/report/generator_test.go @@ -1,6 +1,7 @@ package report import ( + "main/pkg/events" "testing" configTypes "main/pkg/config/types" @@ -20,7 +21,7 @@ func TestReportGeneratorWithProposalError(t *testing.T) { newState := state.State{ ChainInfos: map[string]*state.ChainInfo{ "chain": { - ProposalsError: "test error", + ProposalsError: types.NewJSONError("test error"), }, }, } @@ -31,8 +32,10 @@ func TestReportGeneratorWithProposalError(t *testing.T) { report := generator.GenerateReport(oldState, newState) assert.Equal(t, len(report.Entries), 1, "Expected to have 1 entry!") - assert.Equal(t, report.Entries[0].Type, types.ProposalQueryError, "Expected to have a proposal query error!") - assert.Equal(t, report.Entries[0].Value, "test error", "Error text mismatch!") + + entry, ok := report.Entries[0].(events.ProposalsQueryErrorEvent) + assert.True(t, ok, "Expected to have a proposal query error!") + assert.Equal(t, entry.Error.Error(), "test error", "Error text mismatch!") } func TestReportGeneratorWithVoteError(t *testing.T) { @@ -47,11 +50,12 @@ func TestReportGeneratorWithVoteError(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": { - Error: "test error", + Error: types.NewJSONError("test error"), }, }, }, @@ -66,8 +70,10 @@ func TestReportGeneratorWithVoteError(t *testing.T) { report := generator.GenerateReport(oldState, newState) assert.Equal(t, len(report.Entries), 1, "Expected to have 1 entry!") - assert.Equal(t, report.Entries[0].Type, types.VoteQueryError, "Expected to have a vote query error!") - assert.Equal(t, report.Entries[0].ProposalID, "proposal", "Proposal ID mismatch!") + + entry, ok := report.Entries[0].(events.VoteQueryError) + assert.True(t, ok, "Expected to have a vote query error!") + assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithNotVoted(t *testing.T) { @@ -82,7 +88,8 @@ func TestReportGeneratorWithNotVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": {}, @@ -99,8 +106,10 @@ func TestReportGeneratorWithNotVoted(t *testing.T) { report := generator.GenerateReport(oldState, newState) assert.Equal(t, len(report.Entries), 1, "Expected to have 1 entry!") - assert.Equal(t, report.Entries[0].Type, types.NotVoted, "Expected to have not voted type!") - assert.Equal(t, report.Entries[0].ProposalID, "proposal", "Proposal ID mismatch!") + + entry, ok := report.Entries[0].(events.NotVotedEvent) + assert.True(t, ok, "Expected to have not voted type!") + assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithVoted(t *testing.T) { @@ -114,7 +123,8 @@ func TestReportGeneratorWithVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": {}, @@ -130,7 +140,8 @@ func TestReportGeneratorWithVoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -151,8 +162,10 @@ func TestReportGeneratorWithVoted(t *testing.T) { report := generator.GenerateReport(oldState, newState) assert.Equal(t, len(report.Entries), 1, "Expected to have 1 entry!") - assert.Equal(t, report.Entries[0].Type, types.Voted, "Expected to have voted type!") - assert.Equal(t, report.Entries[0].ProposalID, "proposal", "Proposal ID mismatch!") + + entry, ok := report.Entries[0].(events.VotedEvent) + assert.True(t, ok, "Expected to have voted type!") + assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") } func TestReportGeneratorWithRevoted(t *testing.T) { @@ -166,7 +179,8 @@ func TestReportGeneratorWithRevoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -186,7 +200,8 @@ func TestReportGeneratorWithRevoted(t *testing.T) { ProposalVotes: map[string]state.WalletVotes{ "proposal": { Proposal: types.Proposal{ - Content: &types.ProposalContent{}, + ProposalID: "proposal", + Content: &types.ProposalContent{}, }, Votes: map[string]state.ProposalVote{ "wallet": { @@ -207,6 +222,8 @@ func TestReportGeneratorWithRevoted(t *testing.T) { report := generator.GenerateReport(oldState, newState) assert.Equal(t, len(report.Entries), 1, "Expected to have 1 entry!") - assert.Equal(t, report.Entries[0].Type, types.Revoted, "Expected to have revoted type!") - assert.Equal(t, report.Entries[0].ProposalID, "proposal", "Proposal ID mismatch!") + + entry, ok := report.Entries[0].(events.RevotedEvent) + assert.True(t, ok, "Expected to have revoted type!") + assert.Equal(t, entry.Proposal.ProposalID, "proposal", "Proposal ID mismatch!") } diff --git a/pkg/reporters/pagerduty/pagerduty.go b/pkg/reporters/pagerduty/pagerduty.go index 20fc162..0b64a5f 100644 --- a/pkg/reporters/pagerduty/pagerduty.go +++ b/pkg/reporters/pagerduty/pagerduty.go @@ -3,7 +3,10 @@ package pagerduty import ( "bytes" "encoding/json" + "errors" "fmt" + "main/pkg/events" + "main/pkg/report/entry" "net/http" "os" "time" @@ -14,13 +17,13 @@ import ( "github.com/rs/zerolog" ) -type PagerDutyReporter struct { +type Reporter struct { PagerDutyURL string APIKey string Logger zerolog.Logger } -type PagerDutyAlertPayload struct { +type AlertPayload struct { Summary string `json:"summary"` Timestamp string `json:"timestamp"` Severity string `json:"severity"` @@ -28,37 +31,42 @@ type PagerDutyAlertPayload struct { CustomDetails map[string]string `json:"custom_details"` } -type PagerDutyLink struct { +type Link struct { Href string `json:"href"` Text string `json:"text"` } -type PagerDutyAlert struct { - Payload PagerDutyAlertPayload `json:"payload"` - RoutingKey string `json:"routing_key"` - EventAction string `json:"event_action"` - DedupKey string `json:"dedup_key"` - Client string `json:"client"` - Links []PagerDutyLink `json:"links"` - ClientURL string `json:"client_url"` +type Alert struct { + Payload AlertPayload `json:"payload"` + RoutingKey string `json:"routing_key"` + EventAction string `json:"event_action"` + DedupKey string `json:"dedup_key"` + Client string `json:"client"` + Links []Link `json:"links"` + ClientURL string `json:"client_url"` } -type PagerDutyResponse struct { +type Response struct { Status string Message string } -func (r *PagerDutyReporter) NewPagerDutyAlertFromReportEntry(e reporters.ReportEntry) PagerDutyAlert { +func (r *Reporter) NewAlertFromReportEntry(eventRaw entry.ReportEntry) (Alert, error) { + event, ok := eventRaw.(entry.ReportEntryNotError) + if !ok { + return Alert{}, errors.New("error converting alert entry") + } + eventAction := "trigger" - if e.HasVoted() { + if _, ok := event.(events.VotedEvent); ok { eventAction = "resolve" } dedupKey := fmt.Sprintf( "cosmos-proposals-checker alert chain=%s proposal=%s wallet=%s", - e.Chain.Name, - e.ProposalID, - e.Wallet, + event.GetChain().Name, + event.GetProposal().ProposalID, + event.GetWallet().AddressOrAlias(), ) hostname, err := os.Hostname() @@ -66,33 +74,33 @@ func (r *PagerDutyReporter) NewPagerDutyAlertFromReportEntry(e reporters.ReportE hostname = "unknown-host" } - links := []PagerDutyLink{} - explorerLinks := e.Chain.GetExplorerProposalsLinks(e.ProposalID) + links := []Link{} + explorerLinks := event.GetChain().GetExplorerProposalsLinks(event.GetProposal().ProposalID) for _, link := range explorerLinks { - links = append(links, PagerDutyLink{ + links = append(links, Link{ Href: link.Href, Text: link.Name, }) } - return PagerDutyAlert{ - Payload: PagerDutyAlertPayload{ + return Alert{ + Payload: AlertPayload{ Summary: fmt.Sprintf( "Wallet %s hasn't voted on proposal %s on %s: %s", - e.Wallet, - e.ProposalID, - e.Chain.GetName(), - e.ProposalTitle, + event.GetWallet().AddressOrAlias(), + event.GetProposal().ProposalID, + event.GetChain().GetName(), + event.GetProposal().Content.Title, ), Timestamp: time.Now().Format(time.RFC3339), Severity: "error", Source: hostname, CustomDetails: map[string]string{ - "Wallet": e.Wallet.AddressOrAlias(), - "Chain": e.Chain.GetName(), - "Proposal ID": e.ProposalID, - "Proposal title": e.ProposalTitle, - "Proposal description": e.ProposalDescription, + "Wallet": event.GetWallet().AddressOrAlias(), + "Chain": event.GetChain().GetName(), + "Proposal ID": event.GetProposal().ProposalID, + "Proposal title": event.GetProposal().Content.Title, + "Proposal description": event.GetProposal().Content.Description, }, }, Links: links, @@ -101,37 +109,41 @@ func (r *PagerDutyReporter) NewPagerDutyAlertFromReportEntry(e reporters.ReportE DedupKey: dedupKey, Client: "cosmos-proposals-checker", ClientURL: "https://github.com/QuokkaStake/cosmos-proposals-checker", - } + }, nil } -func NewPagerDutyReporter(config config.PagerDutyConfig, logger *zerolog.Logger) PagerDutyReporter { - return PagerDutyReporter{ +func NewPagerDutyReporter(config config.PagerDutyConfig, logger *zerolog.Logger) Reporter { + return Reporter{ PagerDutyURL: config.PagerDutyURL, APIKey: config.APIKey, Logger: logger.With().Str("component", "pagerduty_reporter").Logger(), } } -func (r PagerDutyReporter) Init() { +func (r Reporter) Init() { } -func (r PagerDutyReporter) Enabled() bool { +func (r Reporter) Enabled() bool { return r.APIKey != "" } -func (r PagerDutyReporter) Name() string { +func (r Reporter) Name() string { return "pagerduty-reporter" } -func (r PagerDutyReporter) SendReport(report reporters.Report) error { +func (r Reporter) SendReport(report reporters.Report) error { var err error - for _, entry := range report.Entries { - if !entry.IsVoteOrNotVoted() { + for _, reportEntry := range report.Entries { + if !reportEntry.IsAlert() { continue } - alert := r.NewPagerDutyAlertFromReportEntry(entry) + alert, alertCreateErr := r.NewAlertFromReportEntry(reportEntry) + if alertCreateErr != nil { + err = alertCreateErr + continue + } if alertErr := r.SendAlert(alert); alertErr != nil { err = alertErr @@ -141,8 +153,8 @@ func (r PagerDutyReporter) SendReport(report reporters.Report) error { return err } -func (r PagerDutyReporter) SendAlert(alert PagerDutyAlert) error { - var response PagerDutyResponse +func (r Reporter) SendAlert(alert Alert) error { + var response Response err := r.DoRequest(r.PagerDutyURL+"/v2/enqueue", alert, &response) if err != nil { return err @@ -155,7 +167,7 @@ func (r PagerDutyReporter) SendAlert(alert PagerDutyAlert) error { return nil } -func (r PagerDutyReporter) DoRequest(url string, body interface{}, target interface{}) error { +func (r Reporter) DoRequest(url string, body interface{}, target interface{}) error { client := &http.Client{Timeout: 10 * 1000000000} start := time.Now() diff --git a/pkg/reporters/reporter.go b/pkg/reporters/reporter.go index 5187924..1f8c88c 100644 --- a/pkg/reporters/reporter.go +++ b/pkg/reporters/reporter.go @@ -1,11 +1,7 @@ package reporters import ( - "time" - - configTypes "main/pkg/config/types" - "main/pkg/types" - "main/pkg/utils" + "main/pkg/report/entry" ) type Reporter interface { @@ -16,49 +12,9 @@ type Reporter interface { } type Report struct { - Entries []ReportEntry + Entries []entry.ReportEntry } func (r *Report) Empty() bool { return len(r.Entries) == 0 } - -type ReportEntry struct { - Chain *configTypes.Chain - Wallet *configTypes.Wallet - ProposalID string - ProposalTitle string - ProposalDescription string - ProposalVoteEndingTime time.Time - Type types.ReportEntryType - Value string - OldValue string -} - -func (e ReportEntry) HasVoted() bool { - return e.Value != "" -} - -func (e ReportEntry) HasRevoted() bool { - return e.Value != "" && e.OldValue != "" -} - -func (e ReportEntry) IsVoteOrNotVoted() bool { - return e.Type == types.NotVoted || e.Type == types.Voted -} - -func (e ReportEntry) GetProposalTime() string { - return e.ProposalVoteEndingTime.Format(time.RFC1123) -} - -func (e ReportEntry) GetProposalTimeLeft() string { - return utils.FormatDuration(time.Until(e.ProposalVoteEndingTime).Round(time.Second)) -} - -func (e ReportEntry) GetVote() string { - return utils.ResolveVote(e.Value) -} - -func (e ReportEntry) GetOldVote() string { - return utils.ResolveVote(e.OldValue) -} diff --git a/pkg/reporters/telegram/add_mute.go b/pkg/reporters/telegram/add_mute.go index 13e0bb3..93081f2 100644 --- a/pkg/reporters/telegram/add_mute.go +++ b/pkg/reporters/telegram/add_mute.go @@ -7,7 +7,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleAddMute(c tele.Context) error { +func (reporter *Reporter) HandleAddMute(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/help.go b/pkg/reporters/telegram/help.go index 55db765..a280f13 100644 --- a/pkg/reporters/telegram/help.go +++ b/pkg/reporters/telegram/help.go @@ -6,7 +6,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleHelp(c tele.Context) error { +func (reporter *Reporter) HandleHelp(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/list_mutes.go b/pkg/reporters/telegram/list_mutes.go index a13861b..54e1f21 100644 --- a/pkg/reporters/telegram/list_mutes.go +++ b/pkg/reporters/telegram/list_mutes.go @@ -8,7 +8,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleListMutes(c tele.Context) error { +func (reporter *Reporter) HandleListMutes(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/list_proposals.go b/pkg/reporters/telegram/list_proposals.go index 581224c..ddafa1e 100644 --- a/pkg/reporters/telegram/list_proposals.go +++ b/pkg/reporters/telegram/list_proposals.go @@ -7,7 +7,7 @@ import ( tele "gopkg.in/telebot.v3" ) -func (reporter *TelegramReporter) HandleProposals(c tele.Context) error { +func (reporter *Reporter) HandleProposals(c tele.Context) error { reporter.Logger.Info(). Str("sender", c.Sender().Username). Str("text", c.Text()). diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index 76098d9..b9fce88 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -5,6 +5,7 @@ import ( "fmt" "html/template" mutes "main/pkg/mutes" + "main/pkg/report/entry" "main/pkg/state" "strings" "time" @@ -18,7 +19,7 @@ import ( tele "gopkg.in/telebot.v3" ) -type TelegramReporter struct { +type Reporter struct { TelegramToken string TelegramChat int64 MutesManager *mutes.Manager @@ -26,7 +27,7 @@ type TelegramReporter struct { TelegramBot *tele.Bot Logger zerolog.Logger - Templates map[types.ReportEntryType]*template.Template + Templates map[string]*template.Template } const ( @@ -38,18 +39,18 @@ func NewTelegramReporter( mutesManager *mutes.Manager, stateGenerator *state.Generator, logger *zerolog.Logger, -) *TelegramReporter { - return &TelegramReporter{ +) *Reporter { + return &Reporter{ TelegramToken: config.TelegramToken, TelegramChat: config.TelegramChat, MutesManager: mutesManager, StateGenerator: stateGenerator, Logger: logger.With().Str("component", "telegram_reporter").Logger(), - Templates: make(map[types.ReportEntryType]*template.Template, 0), + Templates: make(map[string]*template.Template, 0), } } -func (reporter *TelegramReporter) Init() { +func (reporter *Reporter) Init() { if reporter.TelegramToken == "" || reporter.TelegramChat == 0 { reporter.Logger.Debug().Msg("Telegram credentials not set, not creating Telegram reporter.") return @@ -75,17 +76,17 @@ func (reporter *TelegramReporter) Init() { go reporter.TelegramBot.Start() } -func (reporter *TelegramReporter) Enabled() bool { +func (reporter *Reporter) Enabled() bool { return reporter.TelegramToken != "" && reporter.TelegramChat != 0 } -func (reporter *TelegramReporter) GetTemplate(tmlpType types.ReportEntryType) (*template.Template, error) { +func (reporter *Reporter) GetTemplate(tmlpType string) (*template.Template, error) { if cachedTemplate, ok := reporter.Templates[tmlpType]; ok { - reporter.Logger.Trace().Str("type", string(tmlpType)).Msg("Using cached template") + reporter.Logger.Trace().Str("type", tmlpType).Msg("Using cached template") return cachedTemplate, nil } - reporter.Logger.Trace().Str("type", string(tmlpType)).Msg("Loading template") + reporter.Logger.Trace().Str("type", tmlpType).Msg("Loading template") filename := fmt.Sprintf("%s.html", tmlpType) @@ -101,34 +102,38 @@ func (reporter *TelegramReporter) GetTemplate(tmlpType types.ReportEntryType) (* return t, nil } -func (reporter *TelegramReporter) SerializeReportEntry(e reporters.ReportEntry) (string, error) { - parsedTemplate, err := reporter.GetTemplate(e.Type) +func (reporter *Reporter) SerializeReportEntry(e entry.ReportEntry) (string, error) { + parsedTemplate, err := reporter.GetTemplate(e.Name()) if err != nil { - reporter.Logger.Error().Err(err).Str("type", string(e.Type)).Msg("Error loading template") + reporter.Logger.Error().Err(err).Str("type", e.Name()).Msg("Error loading template") return "", err } var buffer bytes.Buffer err = parsedTemplate.Execute(&buffer, e) if err != nil { - reporter.Logger.Error().Err(err).Str("type", string(e.Type)).Msg("Error rendering template") + reporter.Logger.Error().Err(err).Str("type", e.Name()).Msg("Error rendering template") return "", err } return buffer.String(), nil } -func (reporter *TelegramReporter) SendReport(report reporters.Report) error { - for _, entry := range report.Entries { - if !entry.HasVoted() && reporter.MutesManager.IsMuted(entry.Chain.Name, entry.ProposalID) { - reporter.Logger.Debug(). - Str("chain", entry.Chain.Name). - Str("proposal", entry.ProposalID). - Msg("Notifications are muted, not sending.") - continue +func (reporter *Reporter) SendReport(report reporters.Report) error { + for _, reportEntry := range report.Entries { + if entryConverted, ok := reportEntry.(entry.ReportEntryNotError); ok { + chain := entryConverted.GetChain() + proposal := entryConverted.GetProposal() + if reporter.MutesManager.IsMuted(chain.Name, proposal.ProposalID) { + reporter.Logger.Debug(). + Str("chain", chain.Name). + Str("proposal", proposal.ProposalID). + Msg("Notifications are muted, not sending.") + continue + } } - serializedEntry, err := reporter.SerializeReportEntry(entry) + serializedEntry, err := reporter.SerializeReportEntry(reportEntry) if err != nil { reporter.Logger.Err(err).Msg("Could not serialize report entry") return err @@ -151,11 +156,11 @@ func (reporter *TelegramReporter) SendReport(report reporters.Report) error { return nil } -func (reporter *TelegramReporter) Name() string { +func (reporter *Reporter) Name() string { return "telegram-reporter" } -func (reporter *TelegramReporter) BotReply(c tele.Context, msg string) error { +func (reporter *Reporter) BotReply(c tele.Context, msg string) error { msgsByNewline := strings.Split(msg, "\n") var sb strings.Builder @@ -226,7 +231,7 @@ func ParseMuteOptions(query string, c tele.Context) (*mutes.Mute, string) { return mute, "" } -func (reporter *TelegramReporter) SerializeLink(link types.Link) template.HTML { +func (reporter *Reporter) SerializeLink(link types.Link) template.HTML { if link.Href != "" { return template.HTML(fmt.Sprintf("%s", link.Href, link.Name)) } diff --git a/pkg/state/generator.go b/pkg/state/generator.go index becbd1c..6e0f62c 100644 --- a/pkg/state/generator.go +++ b/pkg/state/generator.go @@ -125,7 +125,7 @@ func (g *Generator) ProcessProposalAndWallet( } if err != nil { - proposalVote.Error = err.Error() + proposalVote.Error = types.NewJSONError(err.Error()) } else { proposalVote.Vote = voteResponse.Vote } diff --git a/pkg/state/state.go b/pkg/state/state.go index d45b89d..35a3c48 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -8,15 +8,15 @@ import ( type ProposalVote struct { Wallet *configTypes.Wallet Vote *types.Vote - Error string + Error *types.JSONError } func (v ProposalVote) HasVoted() bool { - return v.Vote != nil && v.Error == "" + return v.Vote != nil && v.Error == nil } func (v ProposalVote) IsError() bool { - return v.Error != "" + return v.Error != nil } type WalletVotes struct { @@ -27,11 +27,11 @@ type WalletVotes struct { type ChainInfo struct { Chain *configTypes.Chain ProposalVotes map[string]WalletVotes - ProposalsError string + ProposalsError *types.JSONError } func (c ChainInfo) HasProposalsError() bool { - return c.ProposalsError != "" + return c.ProposalsError != nil } type State struct { @@ -65,7 +65,7 @@ func (s *State) SetVote(chain *configTypes.Chain, proposal types.Proposal, walle func (s *State) SetChainProposalsError(chain *configTypes.Chain, err error) { s.ChainInfos[chain.Name] = &ChainInfo{ Chain: chain, - ProposalsError: err.Error(), + ProposalsError: types.NewJSONError(err.Error()), } } diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index 1d4ab44..46370f0 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -44,7 +44,7 @@ func TestSetProposalErrorWithoutChainInfo(t *testing.T) { assert.Equal(t, hasError2, true, "Chain info should have a proposal error!") err := state.ChainInfos["test"].ProposalsError - assert.Equal(t, err, "test error", "Errors text should match!") + assert.Equal(t, err.Error(), "test error", "Errors text should match!") } func TestSetVotes(t *testing.T) { @@ -90,7 +90,7 @@ func TestSetProposalErrorWithChainInfo(t *testing.T) { assert.Equal(t, hasError2, true, "Chain info should have a proposal error!") err := state.ChainInfos["test"].ProposalsError - assert.Equal(t, err, "test error", "Errors text should match!") + assert.Equal(t, err.Error(), "test error", "Errors text should match!") } func TestGetVoteWithoutChainInfo(t *testing.T) { diff --git a/pkg/types/error.go b/pkg/types/error.go new file mode 100644 index 0000000..2196b49 --- /dev/null +++ b/pkg/types/error.go @@ -0,0 +1,24 @@ +package types + +import "encoding/json" + +type JSONError struct { + error string +} + +func NewJSONError(err string) *JSONError { + return &JSONError{error: err} +} + +func (e *JSONError) Error() string { + return e.error +} + +func (e *JSONError) MarshalJSON() ([]byte, error) { + return json.Marshal(e.error) +} + +func (e *JSONError) UnmarshalJSON(data []byte) error { + e.error = string(data) + return nil +} diff --git a/pkg/types/responses.go b/pkg/types/responses.go index c6c9899..9237057 100644 --- a/pkg/types/responses.go +++ b/pkg/types/responses.go @@ -17,6 +17,10 @@ func (p Proposal) GetTimeLeft() string { return utils.FormatDuration(time.Until(p.VotingEndTime).Round(time.Second)) } +func (p Proposal) GetProposalTime() string { + return p.VotingEndTime.Format(time.RFC1123) +} + type ProposalContent struct { Title string `json:"title"` Description string `json:"description"` diff --git a/pkg/types/types.go b/pkg/types/types.go index 3f96b4f..9627c33 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -16,13 +16,3 @@ func (l Link) Serialize() string { return fmt.Sprintf("%s", l.Href, l.Name) } - -type ReportEntryType string - -const ( - NotVoted ReportEntryType = "not_voted" - Voted ReportEntryType = "voted" - Revoted ReportEntryType = "revoted" - ProposalQueryError ReportEntryType = "proposal_query_error" - VoteQueryError ReportEntryType = "vote_query_error" -) diff --git a/templates/telegram/not_voted.html b/templates/telegram/not_voted.html index 7ce8a2d..822f7e1 100644 --- a/templates/telegram/not_voted.html +++ b/templates/telegram/not_voted.html @@ -1,9 +1,9 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -🔴 Wallet {{ SerializeLink $walletLink }} hasn't voted on proposal {{ .ProposalID }} on {{ .Chain.GetName }} -{{ .ProposalTitle }} +🔴 Wallet {{ SerializeLink $walletLink }} hasn't voted on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} +{{ .Proposal.Content.Title }} -Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) +Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/proposal_query_error.html b/templates/telegram/proposals_query_error.html similarity index 80% rename from templates/telegram/proposal_query_error.html rename to templates/telegram/proposals_query_error.html index e59ba26..628d9a0 100644 --- a/templates/telegram/proposal_query_error.html +++ b/templates/telegram/proposals_query_error.html @@ -1,4 +1,4 @@ ❌ There was an error querying proposals on {{ .Chain.GetName }}. -Error text: {{ .Value }} +Error text: {{ .Error }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/revoted.html b/templates/telegram/revoted.html index 1e156a8..3e18eb3 100644 --- a/templates/telegram/revoted.html +++ b/templates/telegram/revoted.html @@ -1,11 +1,11 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -↔️ Wallet {{ SerializeLink $walletLink }} has changed its vote on proposal {{ .ProposalID }} on {{ .Chain.GetName }} -{{ .ProposalTitle }} +↔️ Wallet {{ SerializeLink $walletLink }} has changed its vote on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} +{{ .Proposal.Content.Title }} -Vote: {{ .GetVote }} -Old vote: {{ .GetOldVote }} -Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) +Vote: {{ .Vote.ResolveVote }} +Old vote: {{ .OldVote.ResolveVote }} +Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/vote_query_error.html b/templates/telegram/vote_query_error.html index 3ba526b..ffd6dc5 100644 --- a/templates/telegram/vote_query_error.html +++ b/templates/telegram/vote_query_error.html @@ -1,5 +1,5 @@ ❌ There was an error querying proposal on {{ .Chain.GetName }} -Proposal ID: {{ .ProposalID }} -Error text: {{ .Value }} +Proposal ID: {{ .Proposal.ProposalID }} +Error text: {{ .Error }} Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/voted.html b/templates/telegram/voted.html index df5ceb5..3f2d1cf 100644 --- a/templates/telegram/voted.html +++ b/templates/telegram/voted.html @@ -1,10 +1,10 @@ {{- $walletLink := .Chain.GetWalletLink .Wallet -}} -✅ Wallet {{ SerializeLink $walletLink }} has voted on proposal {{ .ProposalID }} on {{ .Chain.GetName }} -{{ .ProposalTitle }} +✅ Wallet {{ SerializeLink $walletLink }} has voted on proposal {{ .Proposal.ProposalID }} on {{ .Chain.GetName }} +{{ .Proposal.Content.Title }} -Vote: {{ .GetVote }} -Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) +Vote: {{ .Vote.ResolveVote }} +Voting ends at: {{ .Proposal.GetProposalTime }} (in {{ .Proposal.GetTimeLeft }}) -{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ SerializeLink .}} +{{ range .Chain.GetExplorerProposalsLinks .Proposal.ProposalID }}{{ SerializeLink .}} {{ end }} Sent by cosmos-proposals-checker. \ No newline at end of file