diff --git a/pkg/reporters/telegram/list_proposals.go b/pkg/reporters/telegram/list_proposals.go index ddafa1e..8c8a47a 100644 --- a/pkg/reporters/telegram/list_proposals.go +++ b/pkg/reporters/telegram/list_proposals.go @@ -14,10 +14,12 @@ func (reporter *Reporter) HandleProposals(c tele.Context) error { Msg("Got proposals list query") state := reporter.StateGenerator.GetState(statePkg.NewState()) + renderedState := state.ToRenderedState() + template, _ := reporter.GetTemplate("proposals") var buffer bytes.Buffer - if err := template.Execute(&buffer, state); err != nil { - reporter.Logger.Error().Err(err).Msg("Error rendering votes template") + if err := template.Execute(&buffer, renderedState); err != nil { + reporter.Logger.Error().Err(err).Msg("Error rendering proposals template") return err } diff --git a/pkg/reporters/telegram/telegram.go b/pkg/reporters/telegram/telegram.go index b1f24d4..b81d100 100644 --- a/pkg/reporters/telegram/telegram.go +++ b/pkg/reporters/telegram/telegram.go @@ -184,7 +184,7 @@ func (reporter *Reporter) BotReply(c tele.Context, msg string) error { for _, line := range msgsByNewline { if sb.Len()+len(line) > MaxMessageSize { - if err := c.Reply(sb.String(), tele.ModeHTML); err != nil { + if err := c.Reply(sb.String(), tele.ModeHTML, tele.NoPreview); err != nil { reporter.Logger.Error().Err(err).Msg("Could not send Telegram message") return err } @@ -195,7 +195,7 @@ func (reporter *Reporter) BotReply(c tele.Context, msg string) error { sb.WriteString(line + "\n") } - if err := c.Reply(sb.String(), tele.ModeHTML); err != nil { + if err := c.Reply(sb.String(), tele.ModeHTML, tele.NoPreview); err != nil { reporter.Logger.Error().Err(err).Msg("Could not send Telegram message") return err } diff --git a/pkg/state/rendered_state.go b/pkg/state/rendered_state.go new file mode 100644 index 0000000..68fc10b --- /dev/null +++ b/pkg/state/rendered_state.go @@ -0,0 +1,36 @@ +package state + +import "main/pkg/types" + +type RenderedState struct { + ChainInfos []RenderedChainInfo +} + +type RenderedChainInfo struct { + Chain *types.Chain + ProposalVotes []RenderedProposalVotes + ProposalsError *types.QueryError +} + +type RenderedProposalVotes struct { + Proposal types.Proposal + Votes []RenderedWalletVote +} + +type RenderedWalletVote struct { + Wallet *types.Wallet + Vote *types.Vote + Error *types.QueryError +} + +func (v RenderedWalletVote) HasVoted() bool { + return v.Vote != nil && v.Error == nil +} + +func (v RenderedWalletVote) IsError() bool { + return v.Error != nil +} + +func (c RenderedChainInfo) HasProposalsError() bool { + return c.ProposalsError != nil +} diff --git a/pkg/state/rendered_state_test.go b/pkg/state/rendered_state_test.go new file mode 100644 index 0000000..c96bfee --- /dev/null +++ b/pkg/state/rendered_state_test.go @@ -0,0 +1,165 @@ +package state + +import ( + "main/pkg/types" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToRenderedStateFilteredChain(t *testing.T) { + t.Parallel() + + state := State{ + ChainInfos: map[string]*ChainInfo{ + "chain": { + ProposalVotes: map[string]WalletVotes{}, + }, + }, + } + + renderedState := state.ToRenderedState() + assert.Empty(t, renderedState.ChainInfos) +} + +func TestToRenderedStateFilterProposalsNotInVoting(t *testing.T) { + t.Parallel() + + state := State{ + ChainInfos: map[string]*ChainInfo{ + "chain": { + ProposalVotes: map[string]WalletVotes{ + "proposal": { + Proposal: types.Proposal{ + ID: "proposal", + Status: types.ProposalStatusPassed, + }, + }, + }, + }, + }, + } + + renderedState := state.ToRenderedState() + assert.Empty(t, renderedState.ChainInfos) +} + +func TestToRenderedStateSortProposals(t *testing.T) { + t.Parallel() + + chain := &types.Chain{Name: "chain"} + state := NewState() + state.SetProposal(chain, types.Proposal{ID: "15", Status: types.ProposalStatusVoting}) + state.SetProposal(chain, types.Proposal{ID: "231", Status: types.ProposalStatusVoting}) + state.SetProposal(chain, types.Proposal{ID: "2", Status: types.ProposalStatusVoting}) + + renderedState := state.ToRenderedState() + assert.Len(t, renderedState.ChainInfos, 1) + + renderedChain := renderedState.ChainInfos[0] + assert.Len(t, renderedChain.ProposalVotes, 3) + assert.Equal(t, "231", renderedChain.ProposalVotes[0].Proposal.ID) + assert.Equal(t, "15", renderedChain.ProposalVotes[1].Proposal.ID) + assert.Equal(t, "2", renderedChain.ProposalVotes[2].Proposal.ID) +} + +func TestToRenderedStateSortProposalsInvalid(t *testing.T) { + t.Parallel() + + chain := &types.Chain{Name: "chain"} + state := NewState() + state.SetProposal(chain, types.Proposal{ID: "1", Status: types.ProposalStatusVoting}) + state.SetProposal(chain, types.Proposal{ID: "a", Status: types.ProposalStatusVoting}) + state.SetProposal(chain, types.Proposal{ID: "b", Status: types.ProposalStatusVoting}) + + renderedState := state.ToRenderedState() + assert.Len(t, renderedState.ChainInfos, 1) + + renderedChain := renderedState.ChainInfos[0] + assert.Len(t, renderedChain.ProposalVotes, 3) + + // we don't know the sorting +} + +func TestToRenderedStateSortChains(t *testing.T) { + t.Parallel() + + state := State{ + ChainInfos: map[string]*ChainInfo{ + "cosmos": { + Chain: &types.Chain{Name: "cosmos"}, + ProposalVotes: map[string]WalletVotes{ + "proposal": {Proposal: types.Proposal{ID: "1", Status: types.ProposalStatusVoting}}, + }, + }, + "sentinel": { + Chain: &types.Chain{Name: "sentinel"}, + ProposalVotes: map[string]WalletVotes{ + "proposal": {Proposal: types.Proposal{ID: "1", Status: types.ProposalStatusVoting}}, + }, + }, + "bitsong": { + Chain: &types.Chain{Name: "bitsong"}, + ProposalVotes: map[string]WalletVotes{ + "proposal": {Proposal: types.Proposal{ID: "1", Status: types.ProposalStatusVoting}}, + }, + }, + }, + } + + renderedState := state.ToRenderedState() + assert.Len(t, renderedState.ChainInfos, 3) + assert.Equal(t, "bitsong", renderedState.ChainInfos[0].Chain.Name) + assert.Equal(t, "cosmos", renderedState.ChainInfos[1].Chain.Name) + assert.Equal(t, "sentinel", renderedState.ChainInfos[2].Chain.Name) +} + +func TestToRenderedStateSortWallets(t *testing.T) { + t.Parallel() + + chain := &types.Chain{Name: "chain"} + proposal := types.Proposal{ID: "proposal", Status: types.ProposalStatusVoting} + state := NewState() + + wallets := []string{"21", "352", "2"} + + for _, addr := range wallets { + wallet := &types.Wallet{Address: addr} + state.SetVote(chain, proposal, wallet, ProposalVote{Wallet: wallet}) + } + + renderedState := state.ToRenderedState() + assert.Len(t, renderedState.ChainInfos, 1) + + renderedChain := renderedState.ChainInfos[0] + assert.Len(t, renderedChain.ProposalVotes, 1) + + renderedVotes := renderedChain.ProposalVotes[0] + assert.Len(t, renderedVotes.Votes, 3) + + assert.Equal(t, "2", renderedVotes.Votes[0].Wallet.Address) + assert.Equal(t, "21", renderedVotes.Votes[1].Wallet.Address) + assert.Equal(t, "352", renderedVotes.Votes[2].Wallet.Address) +} + +func TestRenderedWalletVoteHasVoted(t *testing.T) { + t.Parallel() + + assert.True(t, RenderedWalletVote{Vote: &types.Vote{}}.HasVoted()) + assert.False(t, RenderedWalletVote{Vote: &types.Vote{}, Error: &types.QueryError{}}.HasVoted()) + assert.False(t, RenderedWalletVote{Error: &types.QueryError{}}.HasVoted()) +} + +func TestRenderedWalletVoteIsError(t *testing.T) { + t.Parallel() + + assert.False(t, RenderedWalletVote{}.IsError()) + assert.True(t, RenderedWalletVote{Error: &types.QueryError{}}.IsError()) +} + +func TestRenderedChainInfoHasError(t *testing.T) { + t.Parallel() + + assert.False(t, RenderedChainInfo{}.HasProposalsError()) + assert.True(t, RenderedChainInfo{ProposalsError: &types.QueryError{}}.HasProposalsError()) +} diff --git a/pkg/state/state.go b/pkg/state/state.go index ce253fd..e50d341 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -2,6 +2,8 @@ package state import ( "main/pkg/types" + "sort" + "strconv" ) type ProposalVote struct { @@ -158,3 +160,86 @@ func (s *State) HasVoted(chain, proposal, wallet string) bool { return s.ChainInfos[chain].ProposalVotes[proposal].Votes[wallet].HasVoted() } + +func (s *State) ToRenderedState() RenderedState { + keys := make([]string, 0) + renderedChainInfos := map[string]RenderedChainInfo{} + + for chainName, chainInfo := range s.ChainInfos { + proposalsKeys := make([]string, 0) + renderedProposals := map[string]RenderedProposalVotes{} + + for proposalID, proposalVotes := range chainInfo.ProposalVotes { + if !proposalVotes.Proposal.IsInVoting() { + continue + } + + votesKeys := make([]string, 0) + renderedVotes := map[string]RenderedWalletVote{} + + for wallet, walletVote := range proposalVotes.Votes { + votesKeys = append(votesKeys, wallet) + renderedVotes[wallet] = RenderedWalletVote{ + Wallet: walletVote.Wallet, + Vote: walletVote.Vote, + Error: walletVote.Error, + } + } + + // sorting wallets votes by wallet name desc + sort.Strings(votesKeys) + + proposalsKeys = append(proposalsKeys, proposalID) + renderedProposals[proposalID] = RenderedProposalVotes{ + Proposal: proposalVotes.Proposal, + Votes: make([]RenderedWalletVote, len(votesKeys)), + } + + for index, key := range votesKeys { + renderedProposals[proposalID].Votes[index] = renderedVotes[key] + } + } + + // might be all the proposals are not in voting + if !chainInfo.HasProposalsError() && len(renderedProposals) == 0 { + continue + } + + keys = append(keys, chainName) + renderedChainInfos[chainName] = RenderedChainInfo{ + Chain: chainInfo.Chain, + ProposalsError: chainInfo.ProposalsError, + ProposalVotes: make([]RenderedProposalVotes, len(proposalsKeys)), + } + + // sorting proposals by ID desc + sort.Slice(proposalsKeys, func(i, j int) bool { + first, firstErr := strconv.Atoi(proposalsKeys[i]) + second, secondErr := strconv.Atoi(proposalsKeys[j]) + + // if it's faulty - doesn't matter how we sort it out + if firstErr != nil || secondErr != nil { + return true + } + + return first > second + }) + + for index, key := range proposalsKeys { + renderedChainInfos[chainName].ProposalVotes[index] = renderedProposals[key] + } + } + + // sorting chains by chain name desc + sort.Strings(keys) + + renderedState := RenderedState{ + ChainInfos: make([]RenderedChainInfo, len(keys)), + } + + for index, key := range keys { + renderedState.ChainInfos[index] = renderedChainInfos[key] + } + + return renderedState +}