From be16232bec79039defbd76604e92811b227616b7 Mon Sep 17 00:00:00 2001 From: Techno Freak <83376337+freak12techno@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:22:47 +0300 Subject: [PATCH] feat: add telegram templates (#13) Fixes #11 --- config.go | 6 +- telegram.go | 134 ++++++------------- templates/telegram/help.html | 11 ++ templates/telegram/not_voted.html | 9 ++ templates/telegram/proposal_query_error.html | 4 + templates/telegram/revoted.html | 11 ++ templates/telegram/vote_query_error.html | 5 + templates/telegram/voted.html | 10 ++ tendermint.go | 3 +- types.go | 49 +++++-- 10 files changed, 139 insertions(+), 103 deletions(-) create mode 100644 templates/telegram/help.html create mode 100644 templates/telegram/not_voted.html create mode 100644 templates/telegram/proposal_query_error.html create mode 100644 templates/telegram/revoted.html create mode 100644 templates/telegram/vote_query_error.html create mode 100644 templates/telegram/voted.html diff --git a/config.go b/config.go index 6a0943e..0444c77 100644 --- a/config.go +++ b/config.go @@ -34,7 +34,7 @@ func (c *Chain) Validate() error { return nil } -func (c *Chain) GetName() string { +func (c Chain) GetName() string { if c.PrettyName != "" { return c.PrettyName } @@ -42,11 +42,11 @@ func (c *Chain) GetName() string { return c.Name } -func (c *Chain) GetKeplrLink(proposalID string) string { +func (c Chain) GetKeplrLink(proposalID string) string { return fmt.Sprintf("https://wallet.keplr.app/#/%s/governance?detailId=%s", c.KeplrName, proposalID) } -func (c *Chain) GetExplorerProposalsLinks(proposalID string) []ExplorerLink { +func (c Chain) GetExplorerProposalsLinks(proposalID string) []ExplorerLink { if c.MintscanPrefix == "" { return []ExplorerLink{} } diff --git a/telegram.go b/telegram.go index 0e36586..7b8a283 100644 --- a/telegram.go +++ b/telegram.go @@ -1,7 +1,10 @@ package main import ( + "bytes" + "embed" "fmt" + "html/template" "strings" "time" @@ -16,19 +19,23 @@ type TelegramReporter struct { TelegramBot *tele.Bot Logger zerolog.Logger + Templates map[ReportEntryType]*template.Template } const ( - MaxMessageSize = 4096 - AuthorDisclaimer = "\nSent by cosmos-proposals-checker." + MaxMessageSize = 4096 ) +//go:embed templates/* +var templatesFs embed.FS + func NewTelegramReporter(config TelegramConfig, mutesManager *MutesManager, logger *zerolog.Logger) *TelegramReporter { return &TelegramReporter{ TelegramToken: config.TelegramToken, TelegramChat: config.TelegramChat, MutesManager: mutesManager, Logger: logger.With().Str("component", "telegram_reporter").Logger(), + Templates: make(map[ReportEntryType]*template.Template, 0), } } @@ -61,93 +68,40 @@ func (reporter TelegramReporter) Enabled() bool { return reporter.TelegramToken != "" && reporter.TelegramChat != 0 } -func (reporter *TelegramReporter) SerializeReportEntry(e ReportEntry) string { - if e.Type == ProposalQueryError { - return reporter.SerializeProposalsError(e) - } - if e.Type == VoteQueryError { - return reporter.SerializeVoteError(e) +func (reporter TelegramReporter) GetTemplate(t ReportEntryType) (*template.Template, error) { + if template, ok := reporter.Templates[t]; ok { + reporter.Logger.Trace().Str("type", string(t)).Msg("Using cached template") + return template, nil } - var sb strings.Builder - - messageText := "🔴 Wallet %s hasn't voted on proposal %s on %s\n%s\n\n" - if e.HasRevoted() { - messageText = "↔️ Wallet %s hasn changed its vote on proposal %s on %s\n%s\n\n" - } else if e.HasVoted() { - messageText = "✅ Wallet %s has voted on proposal %s on %s\n%s\n\n" - } + reporter.Logger.Trace().Str("type", string(t)).Msg("Loading template") - sb.WriteString(fmt.Sprintf( - messageText, - e.Wallet, - e.ProposalID, - e.Chain.GetName(), - e.ProposalTitle, - )) - - if e.HasVoted() { - sb.WriteString(fmt.Sprintf( - "Vote: %s\n", - e.Value, - )) - } - if e.HasRevoted() { - sb.WriteString(fmt.Sprintf( - "Old vote: %s\n", - e.OldValue, - )) + filename := fmt.Sprintf("templates/telegram/%s.html", t) + template, err := template.ParseFS(templatesFs, filename) + if err != nil { + return nil, err } - sb.WriteString(fmt.Sprintf( - "Voting ends at: %s (in %s)\n\n", - e.ProposalVoteEndingTime.Format(time.RFC3339Nano), - time.Until(e.ProposalVoteEndingTime).Round(time.Second), - )) + reporter.Templates[t] = template - sb.WriteString(reporter.SerializeLinks(e)) - sb.WriteString(AuthorDisclaimer) - - return sb.String() + return template, nil } -func (reporter TelegramReporter) SerializeLinks(e ReportEntry) string { - var sb strings.Builder - - if e.Chain.KeplrName != "" { - sb.WriteString(fmt.Sprintf( - "Keplr\n", - e.Chain.GetKeplrLink(e.ProposalID), - )) +func (reporter *TelegramReporter) SerializeReportEntry(e ReportEntry) (string, error) { + template, err := reporter.GetTemplate(e.Type) + if err != nil { + reporter.Logger.Error().Err(err).Str("type", string(e.Type)).Msg("Error loading template") + return "", err } - explorerLinks := e.Chain.GetExplorerProposalsLinks(e.ProposalID) - for _, link := range explorerLinks { - sb.WriteString(fmt.Sprintf( - "%s\n", - link.Link, - link.Name, - )) + var buffer bytes.Buffer + err = template.Execute(&buffer, e) + if err != nil { + reporter.Logger.Error().Err(err).Str("type", string(e.Type)).Msg("Error rendering template") + return "", err } - return sb.String() -} - -func (reporter TelegramReporter) SerializeProposalsError(e ReportEntry) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf("❌ There was an error querying proposals on %s.\n", e.Chain.GetName())) - sb.WriteString(fmt.Sprintf("Error text: %s.\n", e.Value)) - sb.WriteString(AuthorDisclaimer) - return sb.String() -} - -func (reporter TelegramReporter) SerializeVoteError(e ReportEntry) string { - var sb strings.Builder - sb.WriteString(fmt.Sprintf("❌ There was an error querying proposal on %s.\n", e.Chain.GetName())) - sb.WriteString(fmt.Sprintf("Proposal ID: %s.\n", e.ProposalID)) - sb.WriteString(fmt.Sprintf("Error text: %s.\n", e.Value)) - sb.WriteString(AuthorDisclaimer) - return sb.String() + return buffer.String(), nil } func (reporter TelegramReporter) SendReport(report Report) error { @@ -160,9 +114,13 @@ func (reporter TelegramReporter) SendReport(report Report) error { continue } - serializedEntry := reporter.SerializeReportEntry(entry) + serializedEntry, err := reporter.SerializeReportEntry(entry) + if err != nil { + reporter.Logger.Err(err).Msg("Could not serialize report entry") + return err + } - _, err := reporter.TelegramBot.Send( + _, err = reporter.TelegramBot.Send( &tele.User{ ID: reporter.TelegramChat, }, @@ -240,18 +198,14 @@ func (reporter *TelegramReporter) HandleHelp(c tele.Context) error { Str("text", c.Text()). Msg("Got help query") - var sb strings.Builder - sb.WriteString("cosmos-proposals-checker\n\n") - sb.WriteString("Notifies you about the proposals your wallets hasn't voted upon.\n") - sb.WriteString("Can understand the following commands:\n") - sb.WriteString("- /proposals_mute <duration> <chain> <proposal ID> - mute notifications for a specific proposal\n") - sb.WriteString("- /proposals_mutes - display the active proposals mutes list\n") - sb.WriteString("- /help - display this command\n") - sb.WriteString("Created by freak12techno with ❤️.\n") - sb.WriteString("This bot is open-sourced, you can get the source code at https://github.com/freak12techno/cosmos-proposals-checker.\n\n") - sb.WriteString("If you like what we're doing, consider staking with us!\n") + template, _ := reporter.GetTemplate("help") + var buffer bytes.Buffer + if err := template.Execute(&buffer, nil); err != nil { + reporter.Logger.Error().Err(err).Msg("Error rendering telp template") + return err + } - return reporter.BotReply(c, sb.String()) + return reporter.BotReply(c, buffer.String()) } func (reporter *TelegramReporter) BotReply(c tele.Context, msg string) error { diff --git a/templates/telegram/help.html b/templates/telegram/help.html new file mode 100644 index 0000000..9bbfac8 --- /dev/null +++ b/templates/telegram/help.html @@ -0,0 +1,11 @@ +cosmos-proposals-checker +Notifies you about the proposals your wallets hasn't voted upon. +Can understand the following commands: +- /proposals_mute <duration> <chain> <proposal ID> - mute notifications for a specific proposal +- /proposals_mutes - display the active proposals mutes list +- /help - display this command + +Created by freak12techno with ❤️. +This bot is open-sourced, you can get the source code at https://github.com/freak12techno/cosmos-proposals-checker. + +If you like what we're doing, consider staking with us! diff --git a/templates/telegram/not_voted.html b/templates/telegram/not_voted.html new file mode 100644 index 0000000..4402633 --- /dev/null +++ b/templates/telegram/not_voted.html @@ -0,0 +1,9 @@ +🔴 Wallet {{ .Wallet }} hasn't voted on proposal {{ .ProposalID }} on {{ .Chain.GetName }} +{{ .ProposalTitle }} + +Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) + +{{ if .Chain.KeplrName }}Keplr{{ end }} +{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ .Name }} +{{ end }} +Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/proposal_query_error.html b/templates/telegram/proposal_query_error.html new file mode 100644 index 0000000..bdba3a6 --- /dev/null +++ b/templates/telegram/proposal_query_error.html @@ -0,0 +1,4 @@ +❌ There was an error querying proposals on {{ .Chain.GetName }}. +Error text: {{ .Value }} + +Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/revoted.html b/templates/telegram/revoted.html new file mode 100644 index 0000000..8c9abcb --- /dev/null +++ b/templates/telegram/revoted.html @@ -0,0 +1,11 @@ +↔️ Wallet {{ .Wallet }} has changed its vote on proposal {{ .ProposalID }} on {{ .Chain.GetName }} +{{ .ProposalTitle }} + +Vote: {{ .GetVote }} +Old vote: {{ .GetOldVote }} +Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) + +{{ if .Chain.KeplrName }}Keplr{{ end }} +{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ .Name }} +{{ 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 new file mode 100644 index 0000000..085d907 --- /dev/null +++ b/templates/telegram/vote_query_error.html @@ -0,0 +1,5 @@ +❌ There was an error querying proposal on {{ .Chain.GetName }} +Proposal ID: {{ .ProposalID }} +Error text: {{ .Value }} + +Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/templates/telegram/voted.html b/templates/telegram/voted.html new file mode 100644 index 0000000..a5b2e67 --- /dev/null +++ b/templates/telegram/voted.html @@ -0,0 +1,10 @@ +✅ Wallet {{ .Wallet }} has voted on proposal {{ .ProposalID }} on {{ .Chain.GetName }} +{{ .ProposalTitle }} + +Vote: {{ .GetVote }} +Voting ends at: {{ .GetProposalTime }} (in {{ .GetProposalTimeLeft }}) + +{{ if .Chain.KeplrName }}Keplr{{ end }} +{{ range .Chain.GetExplorerProposalsLinks .ProposalID }}{{ .Name }} +{{ end }} +Sent by cosmos-proposals-checker. \ No newline at end of file diff --git a/tendermint.go b/tendermint.go index 9a442ed..b17de21 100644 --- a/tendermint.go +++ b/tendermint.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "strings" "time" "github.com/rs/zerolog" @@ -62,7 +63,7 @@ func (rpc *RPC) GetVote(proposal, voter string) (*VoteRPCResponse, error) { return nil, err } - if vote.IsError() && vote.Code != 3 { + if vote.IsError() && !strings.Contains(vote.Message, "not found") { return nil, errors.New(vote.Message) } diff --git a/types.go b/types.go index 2f18388..31e6e50 100644 --- a/types.go +++ b/types.go @@ -45,14 +45,14 @@ func (r *Report) Empty() bool { return len(r.Entries) == 0 } -type ReportEntryType int +type ReportEntryType string const ( - NotVoted ReportEntryType = iota - Voted - Revoted - ProposalQueryError - VoteQueryError + NotVoted ReportEntryType = "not_voted" + Voted ReportEntryType = "voted" + Revoted ReportEntryType = "revoted" + ProposalQueryError ReportEntryType = "proposal_query_error" + VoteQueryError ReportEntryType = "vote_query_error" ) type ReportEntry struct { @@ -67,18 +67,49 @@ type ReportEntry struct { OldValue string } -func (e *ReportEntry) HasVoted() bool { +func (e ReportEntry) HasVoted() bool { return e.Value != "" } -func (e *ReportEntry) HasRevoted() bool { +func (e ReportEntry) HasRevoted() bool { return e.Value != "" && e.OldValue != "" } -func (e *ReportEntry) IsVoteOrNotVoted() bool { +func (e ReportEntry) IsVoteOrNotVoted() bool { return e.Type == NotVoted || e.Type == Voted } +func (e ReportEntry) GetProposalTime() string { + return e.ProposalVoteEndingTime.Format(time.RFC3339Nano) +} + +func (e ReportEntry) GetProposalTimeLeft() string { + return time.Until(e.ProposalVoteEndingTime).Round(time.Second).String() +} + +func (e ReportEntry) GetVote() string { + return ResolveVote(e.Value) +} + +func (e ReportEntry) GetOldVote() string { + return ResolveVote(e.OldValue) +} + +func ResolveVote(value string) string { + votes := map[string]string{ + "VOTE_OPTION_YES": "Yes", + "VOTE_OPTION_ABSTAIN": "Abstain", + "VOTE_OPTION_NO": "No", + "VOTE_OPTION_NO_WITH_VETO": "No with veto", + } + + if vote, ok := votes[value]; ok && vote != "" { + return vote + } + + return value +} + type Reporter interface { Init() Enabled() bool