~edwargix/tallyard

3cdc3708b3d9481c79ce3e269bdb73f8d58e432f — David Florness 2 years ago 1ba0aef
Ensure elections have more than voter

because having one voter doesn't make any sense, and causes panics in the
cryptography code.

Some refactoring is also done in ElectionWaitTUI to improve readability.
2 files changed, 86 insertions(+), 48 deletions(-)

M election/msg.go
M ui/tui.go
M election/msg.go => election/msg.go +5 -0
@@ 379,6 379,11 @@ func (elections *ElectionsMap) onStartElectionMessage(evt *event.Event) (success
		return
	}

	if len(content.JoinIDs) == 1 {
		warnf("there must be more than one join")
		return
	}

	contents := make([]event.Content, 0, 1+len(content.JoinIDs))
	contents = append(contents, createEvt.Content)


M ui/tui.go => ui/tui.go +81 -48
@@ 410,25 410,7 @@ func ElectionWaitTUI(kill <-chan error, client *mautrix.Client, el *election.Ele
	}
	defer handlePanic()

	el.RLock()
	if el.LocalVoter != nil && el.CreateEvt.Sender == el.LocalVoter.JoinEvt.Sender {
		frame.AddText("Press enter to start the election", false, tview.AlignCenter, tcell.ColorWhite)
		app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
			if event.Key() == tcell.KeyEnter {
				frame.Clear()
				frame.AddText("Starting election...", false, tview.AlignCenter, tcell.ColorWhite)
				err = el.StartElection(client, eventStore)
			}
			return event
		})
	} else {
		frame.AddText("Waiting for election to start...", false, tview.AlignCenter, tcell.ColorWhite)
	}
	if err != nil {
		return
	}
	el.RUnlock()
	update := func() {
	updateVoters := func() {
		el.RLock()
		defer el.RUnlock()
		// TODO: handle users who joined after cutoff


@@ 451,53 433,98 @@ func ElectionWaitTUI(kill <-chan error, client *mautrix.Client, el *election.Ele
		text = "Joined voters:\n" + text
		votersTextView.SetText(text)
	}

	doStartElection := make(chan struct{}, 1)

	startElectionInputCapture := func(event *tcell.EventKey) *tcell.EventKey {
		if event.Key() != tcell.KeyEnter {
			return event
		}

		// so the election isn't started more than once
		app.SetInputCapture(nil)

		frame.Clear()
		frame.AddText("Starting election...", false, tview.AlignCenter, tcell.ColorWhite)

		// actually start election in loop goroutine so text gets
		// updated first
		doStartElection <- struct{}{}

		return event
	}

	queueUpdateText := func(txt string) {
		app.QueueUpdateDraw(func() {
			frame.Clear()
			frame.AddText(txt, false, tview.AlignCenter, tcell.ColorWhite)
			updateVoters()
		})
	}

	go func() {
		// exit screen on return
		defer app.Stop()
		defer handlePanic()
		var electionStarted bool
		for app.alive {
		var inputCaptureSet bool
		userIsCreator := userIsCreator(el)
		for {
			el.RLock()
			if electionStarted {
				// have we received everyone's keys?
				receivedAllKeys := true
			electionHasStarted := el.FinalJoinIDs != nil
			userKeysSent := el.LocalVoter.KeysID != nil
			enoughVoters := len(el.Joins) > 1

			var haveReceivedAllKeys bool
			if electionHasStarted && userKeysSent {
				haveReceivedAllKeys = true
				for _, joinID := range *el.FinalJoinIDs {
					if el.Joins[joinID].KeysID == nil {
						receivedAllKeys = false
						haveReceivedAllKeys = false
						break
					}
				}
				el.RUnlock()
				if receivedAllKeys {
					app.Stop()
			}
			el.RUnlock()

			if electionHasStarted {
				if haveReceivedAllKeys {
					return
				}
			} else if el.FinalJoinIDs != nil {
				electionStarted = true
				el.RUnlock()
				if el.LocalVoter.KeysID == nil {
					app.QueueUpdateDraw(func() {
						frame.Clear()
						frame.AddText("Sending keys...", false, tview.AlignCenter, tcell.ColorWhite)
						update()
					})
				} else if userKeysSent {
					queueUpdateText("Waiting for everyone's keys...")
				} else {
					queueUpdateText("Sending keys...")
					err = el.SendProvingKeys(client, eventStore)
					if err != nil {
						app.Stop()
						return
				}
			} else if userIsCreator {
				select {
				case <-doStartElection:
					err = el.StartElection(client, eventStore)
				default:
					if enoughVoters {
						if !inputCaptureSet {
							// only set once to prevent
							// starting the election more
							// than once
							inputCaptureSet = true
							app.SetInputCapture(startElectionInputCapture)
						}
						queueUpdateText("Press enter to start the election")
					} else {
						queueUpdateText("Waiting for more voters...")
					}
				}
				app.QueueUpdateDraw(func() {
					frame.Clear()
					frame.AddText("Waiting for everyone's keys...", false, tview.AlignCenter, tcell.ColorWhite)
					update()
				})
			} else {
				el.RUnlock()
				app.QueueUpdateDraw(update)
				queueUpdateText("Waiting for election to start...")
			}

			if err != nil {
				return
			}

			time.Sleep(1 * time.Second)
		}
	}()

	app.SetRoot(frame, true)
	err2 := app.Run()
	if err != nil {


@@ 509,6 536,12 @@ func ElectionWaitTUI(kill <-chan error, client *mautrix.Client, el *election.Ele
	return nil
}

func userIsCreator(el *election.Election) bool {
	el.RLock()
	defer el.RUnlock()
	return el.LocalVoter != nil && el.CreateEvt.Sender == el.LocalVoter.JoinEvt.Sender
}

// displays a voting UI to the user and returns the encoded ballot
func VoteTUI(kill <-chan error, candidates []election.Candidate) (ballot *[][]byte, err error) {
	app := newTallyardApplication(kill)