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