Configure golangci-lint and apply suggestions

main
Jan Dittberner 4 years ago
parent 594df29dc1
commit 03827874cf

@ -0,0 +1,63 @@
---
run:
skip-files:
- boardvoting/assets.go
output:
sort-results: true
linter-settings:
goheader:
values:
const:
ORGANIZATION: CAcert Inc.
template: |
Copyright {{ YEAR-RANGE }} {{ ORGANIZATION }}
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
goimports:
local-prefixes: git.cacert.org/cacert-boardvoting
misspell:
locale: US
ignore-words:
- CAcert
linters:
disable-all: false
enable:
- bodyclose
- errorlint
- gocognit
- goconst
- gocritic
- gofmt
- goheader
- goimports
- golint
- gomnd
- gosec
- interfacer
- lll
- makezero
- misspell
- nakedret
- nestif
- nlreturn
- nolintlint
- predeclared
- rowserrcheck
- scopelint
- sqlclosecheck
- wrapcheck
- wsl

@ -1,18 +1,20 @@
/* /*
Copyright 2017-2020 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// The CAcert board voting software.
package main package main
import ( import (
@ -51,12 +53,21 @@ var csrfKey []byte
var version = "undefined" var version = "undefined"
var build = "undefined" var build = "undefined"
const sessionCookieName = "votesession" const (
cookieSecretMinLen = 32
csrfKeyLength = 32
httpIdleTimeout = 5
httpReadHeaderTimeout = 10
httpReadTimeout = 10
httpWriteTimeout = 60
sessionCookieName = "votesession"
)
func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) { func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
funcMaps := sprig.FuncMap() funcMaps := sprig.FuncMap()
funcMaps["nl2br"] = func(text string) template.HTML { funcMaps["nl2br"] = func(text string) template.HTML {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1)) // #nosec G203 input is sanitized
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
} }
funcMaps[csrf.TemplateTag] = func() template.HTML { funcMaps[csrf.TemplateTag] = func() template.HTML {
return csrf.TemplateField(r) return csrf.TemplateField(r)
@ -65,20 +76,24 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string,
var baseTemplate *template.Template var baseTemplate *template.Template
for count, t := range templates { for count, t := range templates {
var err error var (
var assetBytes []byte err error
if //noinspection GoUnresolvedReference assetBytes []byte
assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil { )
if assetBytes, err = boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if count == 0 {
if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else { } else {
if count == 0 { if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} }
} }
} }
@ -100,27 +115,35 @@ const (
func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) { func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
emailsTried := make(map[string]bool) emailsTried := make(map[string]bool)
for _, cert := range r.TLS.PeerCertificates { for _, cert := range r.TLS.PeerCertificates {
for _, extKeyUsage := range cert.ExtKeyUsage { for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageClientAuth { if extKeyUsage != x509.ExtKeyUsageClientAuth {
for _, emailAddress := range cert.EmailAddresses { continue
emailLower := strings.ToLower(emailAddress) }
emailsTried[emailLower] = true
voter, err := FindVoterByAddress(emailLower) for _, emailAddress := range cert.EmailAddresses {
if err != nil { emailLower := strings.ToLower(emailAddress)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) emailsTried[emailLower] = true
return
} voter, err := FindVoterByAddress(emailLower)
if voter != nil { if err != nil {
requestContext := context.WithValue(r.Context(), ctxVoter, voter) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
handler(w, r.WithContext(requestContext)) return
return }
}
if voter != nil {
requestContext := context.WithValue(r.Context(), ctxVoter, voter)
requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
handler(w, r.WithContext(requestContext))
return
} }
} }
} }
} }
needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool) needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
if ok && needsAuth { if ok && needsAuth {
var templateContext struct { var templateContext struct {
@ -129,14 +152,18 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(ht
Flashes interface{} Flashes interface{}
Emails []string Emails []string
} }
for k := range emailsTried { for k := range emailsTried {
templateContext.Emails = append(templateContext.Emails, k) templateContext.Emails = append(templateContext.Emails, k)
} }
sort.Strings(templateContext.Emails) sort.Strings(templateContext.Emails)
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext) renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
return return
} }
handler(w, r) handler(w, r)
} }
@ -154,6 +181,7 @@ type motionListParameters struct {
func parseMotionParameters(r *http.Request) motionParameters { func parseMotionParameters(r *http.Request) motionParameters {
var m = motionParameters{} var m = motionParameters{}
m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes")) m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
return m return m
} }
@ -164,20 +192,24 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
} else { } else {
m.Page = page m.Page = page
} }
m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw")) m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted")) m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm")) m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
} }
return m return m
} }
func motionListHandler(w http.ResponseWriter, r *http.Request) { func motionListHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionListParameters(r) params := parseMotionListParameters(r)
session, err := store.Get(r, sessionCookieName) session, err := store.Get(r, sessionCookieName)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -189,30 +221,42 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string PageTitle string
Flashes interface{} Flashes interface{}
} }
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok { if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter templateContext.Voter = voter
} }
if flashes := session.Flashes(); len(flashes) > 0 { if flashes := session.Flashes(); len(flashes) > 0 {
templateContext.Flashes = flashes templateContext.Flashes = flashes
} }
err = session.Save(r, w) err = session.Save(r, w)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
templateContext.Params = &params templateContext.Params = &params
if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil { if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(
params.Page, params.Flags.Unvoted, templateContext.Voter,
); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if len(templateContext.Decisions) > 0 { if len(templateContext.Decisions) > 0 {
olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter) olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(
params.Flags.Unvoted, templateContext.Voter,
)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if olderExists { if olderExists {
templateContext.NextPage = params.Page + 1 templateContext.NextPage = params.Page + 1
} }
@ -233,6 +277,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r) decision, ok := getDecisionFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return return
} }
@ -244,32 +289,53 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
PageTitle string PageTitle string
Flashes interface{} Flashes interface{}
} }
voter, ok := getVoterFromRequest(r) voter, ok := getVoterFromRequest(r)
if ok { if ok {
templateContext.Voter = voter templateContext.Voter = voter
} }
templateContext.Params = &params templateContext.Params = &params
if params.ShowVotes { if params.ShowVotes {
if err := decision.LoadVotes(); err != nil { if err := decision.LoadVotes(); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
} }
templateContext.Decision = decision templateContext.Decision = decision
templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html"}, templateContext)
renderTemplate(w, r, []string{
"motion.html",
"motion_fragments.html",
"page_fragments.html",
"header.html",
"footer.html",
}, templateContext)
} }
func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) { func singleDecisionHandler(
w http.ResponseWriter,
r *http.Request,
tag string,
handler func(http.ResponseWriter, *http.Request),
) {
decision, err := FindDecisionForDisplayByTag(tag) decision, err := FindDecisionForDisplayByTag(tag)
if err != nil { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
if decision == nil { if decision == nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision))) handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
} }
@ -286,16 +352,19 @@ func (authenticationRequiredHandler) NeedsAuth() bool {
func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) { func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
voter, ok = r.Context().Value(ctxVoter).(*Voter) voter, ok = r.Context().Value(ctxVoter).(*Voter)
return return
} }
func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) { func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay) decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
return return
} }
func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) { func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
vote, ok = r.Context().Value(ctxVote).(VoteChoice) vote, ok = r.Context().Value(ctxVote).(VoteChoice)
return return
} }
@ -305,12 +374,16 @@ func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, me
session, err := store.Get(r, sessionCookieName) session, err := store.Get(r, sessionCookieName)
if err != nil { if err != nil {
log.Warnf("could not get session cookie: %v", err) log.Warnf("could not get session cookie: %v", err)
return return
} }
session.AddFlash(message, tags...) session.AddFlash(message, tags...)
err = session.Save(r, w) err = session.Save(r, w)
if err != nil { if err != nil {
log.Warnf("could not save flash message: %v", err) log.Warnf("could not save flash message: %v", err)
return return
} }
} }
@ -324,14 +397,25 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r) decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending { if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return return
} }
voter, ok := getVoterFromRequest(r) voter, ok := getVoterFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return return
} }
templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
templates := []string{
"withdraw_motion_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct { var templateContext struct {
PageTitle string PageTitle string
Decision *DecisionForDisplay Decision *DecisionForDisplay
@ -343,9 +427,11 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
case http.MethodPost: case http.MethodPost:
decision.Status = voteStatusWithdrawn decision.Status = voteStatusWithdrawn
decision.Modified = time.Now().UTC() decision.Modified = time.Now().UTC()
if err := decision.UpdateStatus(); err != nil { if err := decision.UpdateStatus(); err != nil {
log.Errorf("withdrawing motion failed: %v", err) log.Errorf("withdrawing motion failed: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -372,12 +458,14 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
} }
templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"} templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
var templateContext struct { var templateContext struct {
Form NewDecisionForm Form NewDecisionForm
PageTitle string PageTitle string
Voter *Voter Voter *Voter
Flashes interface{} Flashes interface{}
} }
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
form := NewDecisionForm{ form := NewDecisionForm{
@ -393,10 +481,11 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, r, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} else { } else {
data.Proposed = time.Now().UTC() data.Proposed = time.Now().UTC()
data.ProponentId = voter.Id data.ProponentID = voter.ID
if err := data.Create(); err != nil { if err := data.Create(); err != nil {
log.Errorf("saving motion failed: %v", err) log.Errorf("saving motion failed: %v", err)
http.Error(w, "Saving motion failed", http.StatusInternalServerError) http.Error(w, "Saving motion failed", http.StatusInternalServerError)
return return
} }
@ -426,20 +515,26 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r) decision, ok := getDecisionFromRequest(r)
if !ok || decision.Status != voteStatusPending { if !ok || decision.Status != voteStatusPending {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return return
} }
voter, ok := getVoterFromRequest(r) voter, ok := getVoterFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
return return
} }
templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"} templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
var templateContext struct { var templateContext struct {
Form EditDecisionForm Form EditDecisionForm
PageTitle string PageTitle string
Voter *Voter Voter *Voter
Flashes interface{} Flashes interface{}
} }
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
form := EditDecisionForm{ form := EditDecisionForm{
@ -459,6 +554,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Update(); err != nil { if err := data.Update(); err != nil {
log.Errorf("updating motion failed: %v", err) log.Errorf("updating motion failed: %v", err)
http.Error(w, "Updating the motion failed.", http.StatusInternalServerError) http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
return return
} }
@ -468,6 +564,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
} }
return return
default: default:
templateContext.Voter = voter templateContext.Voter = voter
@ -494,35 +591,43 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch { switch {
case subURL == "": case subURL == "":
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler) authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
return return
case subURL == "/newmotion/": case subURL == "/newmotion/":
handler := &newMotionHandler{} handler := &newMotionHandler{}
authenticateRequest( authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
handler.Handle) handler.Handle)
return return
case strings.Count(subURL, "/") == 1: case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/") parts := strings.Split(subURL, "/")
motionTag := parts[0] motionTag := parts[0]
action, ok := motionActionMap[parts[1]] action, ok := motionActionMap[parts[1]]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
authenticateRequest( authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())), w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, action.Handle) singleDecisionHandler(w, r, motionTag, action.Handle)
}) })
return return
case strings.Count(subURL, "/") == 0: case strings.Count(subURL, "/") == 0:
authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, subURL, motionHandler) singleDecisionHandler(w, r, subURL, motionHandler)
}) })
return return
default: default:
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
} }
@ -536,26 +641,33 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r) decision, ok := getDecisionFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
voter, ok := getVoterFromRequest(r) voter, ok := getVoterFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
vote, ok := getVoteFromRequest(r) vote, ok := getVoteFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
voteResult := &Vote{ voteResult := &Vote{
VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(), VoterID: voter.ID, Vote: vote, DecisionID: decision.ID, Voted: time.Now().UTC(),
Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))} Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
if err := voteResult.Save(); err != nil { if err := voteResult.Save(); err != nil {
log.Errorf("Problem saving vote: %v", err) log.Errorf("Problem saving vote: %v", err)
http.Error(w, "Problem saving vote", http.StatusInternalServerError) http.Error(w, "Problem saving vote", http.StatusInternalServerError)
return return
} }
@ -565,7 +677,14 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
default: default:
templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"} templates := []string{
"direct_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct { var templateContext struct {
Decision *DecisionForDisplay Decision *DecisionForDisplay
VoteChoice VoteChoice VoteChoice VoteChoice
@ -573,9 +692,11 @@ func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{} Flashes interface{}
Voter *Voter Voter *Voter
} }
templateContext.Decision = decision templateContext.Decision = decision
templateContext.VoteChoice = vote templateContext.VoteChoice = vote
templateContext.Voter = voter templateContext.Voter = voter
renderTemplate(w, r, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} }
} }
@ -588,10 +709,12 @@ type proxyVoteHandler struct {
func getPEMClientCert(r *http.Request) string { func getPEMClientCert(r *http.Request) string {
clientCertPEM := bytes.NewBufferString("") clientCertPEM := bytes.NewBufferString("")
authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate) authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw}) err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
if err != nil { if err != nil {
log.Errorf("error encoding client certificate: %v", err) log.Errorf("error encoding client certificate: %v", err)
} }
return clientCertPEM.String() return clientCertPEM.String()
} }
@ -599,14 +722,25 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
decision, ok := getDecisionFromRequest(r) decision, ok := getDecisionFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
proxy, ok := getVoterFromRequest(r) proxy, ok := getVoterFromRequest(r)
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
templates := []string{
"proxy_vote_form.html",
"header.html",
"footer.html",
"motion_fragments.html",
"page_fragments.html",
}
var templateContext struct { var templateContext struct {
Form ProxyVoteForm Form ProxyVoteForm
Decision *DecisionForDisplay Decision *DecisionForDisplay
@ -615,7 +749,9 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
Flashes interface{} Flashes interface{}
Voter *Voter Voter *Voter
} }
templateContext.Voter = proxy templateContext.Voter = proxy
switch r.Method { switch r.Method {
case http.MethodPost: case http.MethodPost:
form := ProxyVoteForm{ form := ProxyVoteForm{
@ -627,15 +763,19 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if valid, voter, data, justification := form.Validate(); !valid { if valid, voter, data, justification := form.Validate(); !valid {
templateContext.Form = form templateContext.Form = form
templateContext.Decision = decision templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
voters, err := GetVotersForProxy(proxy)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} else {
templateContext.Voters = voters
} }
templateContext.Voters = voters
renderTemplate(w, r, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} else { } else {
data.DecisionId = decision.Id data.DecisionID = decision.ID
data.Voted = time.Now().UTC() data.Voted = time.Now().UTC()
data.Notes = fmt.Sprintf( data.Notes = fmt.Sprintf(
"Proxy-Vote by %s\n\n%s\n\n%s", "Proxy-Vote by %s\n\n%s\n\n%s",
@ -644,6 +784,7 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
if err := data.Save(); err != nil { if err := data.Save(); err != nil {
log.Errorf("Error saving vote: %s", err) log.Errorf("Error saving vote: %s", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} }
@ -653,16 +794,21 @@ func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently) http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
} }
return return
default: default:
templateContext.Form = ProxyVoteForm{} templateContext.Form = ProxyVoteForm{}
templateContext.Decision = decision templateContext.Decision = decision
if voters, err := GetVotersForProxy(proxy); err != nil {
voters, err := GetVotersForProxy(proxy)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
} else {
templateContext.Voters = voters
} }
templateContext.Voters = voters
renderTemplate(w, r, templates, templateContext) renderTemplate(w, r, templates, templateContext)
} }
} }
@ -674,24 +820,33 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
case strings.HasPrefix(r.URL.Path, "/proxy/"): case strings.HasPrefix(r.URL.Path, "/proxy/"):
motionTag := r.URL.Path[len("/proxy/"):] motionTag := r.URL.Path[len("/proxy/"):]
handler := &proxyVoteHandler{} handler := &proxyVoteHandler{}
authenticateRequest( authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
singleDecisionHandler(w, r, motionTag, handler.Handle) singleDecisionHandler(w, r, motionTag, handler.Handle)
}) })
case strings.HasPrefix(r.URL.Path, "/vote/"): case strings.HasPrefix(r.URL.Path, "/vote/"):
const expectedParts = 2
parts := strings.Split(r.URL.Path[len("/vote/"):], "/") parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
if len(parts) != 2 { if len(parts) != expectedParts {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
motionTag := parts[0] motionTag := parts[0]
voteValue, ok := VoteValues[parts[1]] voteValue, ok := VoteValues[parts[1]]
if !ok { if !ok {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return return
} }
handler := &directVoteHandler{} handler := &directVoteHandler{}
authenticateRequest( authenticateRequest(
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
func(w http.ResponseWriter, r *http.Request) { func(w http.ResponseWriter, r *http.Request) {
@ -699,6 +854,7 @@ func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)), w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
motionTag, handler.Handle) motionTag, handler.Handle)
}) })
return return
} }
} }
@ -714,8 +870,8 @@ type Config struct {
CookieSecret string `yaml:"cookie_secret"` CookieSecret string `yaml:"cookie_secret"`
CsrfKey string `yaml:"csrf_key"` CsrfKey string `yaml:"csrf_key"`
BaseURL string `yaml:"base_url"` BaseURL string `yaml:"base_url"`
HttpAddress string `yaml:"http_address"` HTTPAddress string `yaml:"http_address"`
HttpsAddress string `yaml:"https_address"` HTTPSAddress string `yaml:"https_address"`
MailServer struct { MailServer struct {
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
@ -727,15 +883,17 @@ func readConfig() {
if err != nil { if err != nil {
log.Panicf("Opening configuration file failed: %v", err) log.Panicf("Opening configuration file failed: %v", err)
} }
if err := yaml.Unmarshal(source, &config); err != nil { if err := yaml.Unmarshal(source, &config); err != nil {
log.Panicf("Loading configuration failed: %v", err) log.Panicf("Loading configuration failed: %v", err)
} }
if config.HttpsAddress == "" { if config.HTTPSAddress == "" {
config.HttpsAddress = "127.0.0.1:8443" config.HTTPSAddress = "127.0.0.1:8443"
} }
if config.HttpAddress == "" {
config.HttpAddress = "127.0.0.1:8080" if config.HTTPAddress == "" {
config.HTTPAddress = "127.0.0.1:8080"
} }
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret) cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
@ -743,17 +901,26 @@ func readConfig() {
log.Panicf("Decoding cookie secret failed: %v", err) log.Panicf("Decoding cookie secret failed: %v", err)
panic(err) panic(err)
} }
if len(cookieSecret) < 32 {
log.Panic("Cookie secret is less than 32 bytes long") if len(cookieSecret) < cookieSecretMinLen {
log.Panicf("Cookie secret is less than %d bytes long", cookieSecretMinLen)
} }
csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey) csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
if err != nil { if err != nil {
log.Panicf("Decoding csrf key failed: %v", err) log.Panicf("Decoding csrf key failed: %v", err)
} }
if len(csrfKey) != 32 {
log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey)) if len(csrfKey) != csrfKeyLength {
log.Panicf(
"CSRF key must be exactly %d bytes long but is %d bytes long",
csrfKeyLength,
len(csrfKey),
)
} }
store = sessions.NewCookieStore(cookieSecret) store = sessions.NewCookieStore(cookieSecret)
log.Info("Read configuration") log.Info("Read configuration")
} }
@ -762,6 +929,7 @@ func setupDbConfig(ctx context.Context) {
if err != nil { if err != nil {
log.Panicf("Opening database failed: %v", err) log.Panicf("Opening database failed: %v", err)
} }
db = NewDB(database) db = NewDB(database)
go func() { go func() {
@ -812,6 +980,7 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
if err != nil { if err != nil {
log.Panicf("Error reading client certificate CAs %v", err) log.Panicf("Error reading client certificate CAs %v", err)
} }
caCertPool := x509.NewCertPool() caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) { if !caCertPool.AppendCertsFromPEM(caCert) {
log.Panic("could not initialize client CA certificate pool") log.Panic("could not initialize client CA certificate pool")
@ -819,9 +988,11 @@ func setupTLSConfig() (tlsConfig *tls.Config) {
// setup HTTPS server // setup HTTPS server
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool, ClientCAs: caCertPool,
ClientAuth: tls.VerifyClientCertIfGiven, ClientAuth: tls.VerifyClientCertIfGiven,
} }
return return
} }
@ -835,23 +1006,26 @@ func main() {
flag.Parse() flag.Parse()
var stopAll func() var stopAll func()
executionContext, stopAll := context.WithCancel(context.Background()) executionContext, stopAll := context.WithCancel(context.Background())
readConfig() readConfig()
setupDbConfig(executionContext) setupDbConfig(executionContext)
setupNotifications(executionContext) setupNotifications(executionContext)
setupJobs(executionContext) setupJobs(executionContext)
setupHandlers() setupHandlers()
tlsConfig := setupTLSConfig() tlsConfig := setupTLSConfig()
defer stopAll() defer stopAll()
server := &http.Server{ server := &http.Server{
Addr: config.HttpsAddress, Addr: config.HTTPSAddress,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
IdleTimeout: time.Second * 120, IdleTimeout: time.Second * httpIdleTimeout,
ReadHeaderTimeout: time.Second * 10, ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
ReadTimeout: time.Second * 20, ReadTimeout: time.Second * httpReadTimeout,
WriteTimeout: time.Second * 60, WriteTimeout: time.Second * httpWriteTimeout,
} }
server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux) server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)
@ -859,24 +1033,27 @@ func main() {
log.Infof("Launching application on https://%s/", server.Addr) log.Infof("Launching application on https://%s/", server.Addr)
errs := make(chan error, 1) errs := make(chan error, 1)
go func() { go func() {
httpRedirector := &http.Server{ httpRedirector := &http.Server{
Addr: config.HttpAddress, Addr: config.HTTPAddress,
Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently), Handler: http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently),
IdleTimeout: time.Second * 5, IdleTimeout: time.Second * httpIdleTimeout,
ReadHeaderTimeout: time.Second * 10, ReadHeaderTimeout: time.Second * httpReadHeaderTimeout,
ReadTimeout: time.Second * 10, ReadTimeout: time.Second * httpReadTimeout,
WriteTimeout: time.Second * 60, WriteTimeout: time.Second * httpWriteTimeout,
} }
if err := httpRedirector.ListenAndServe(); err != nil { if err := httpRedirector.ListenAndServe(); err != nil {
errs <- err errs <- err
} }
close(errs) close(errs)
}() }()
if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil { if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
log.Panicf("ListenAndServerTLS failed: %v", err) log.Panicf("ListenAndServerTLS failed: %v", err)
} }
if err := <-errs; err != nil { if err := <-errs; err != nil {
log.Panicf("ListenAndServe failed: %v", err) log.Panicf("ListenAndServe failed: %v", err)
} }

@ -1,18 +1,20 @@
/* /*
Copyright 2017-2019 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Assets for the CAcert board voting software.
package boardvoting package boardvoting
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/... //go:generate go-bindata -pkg $GOPACKAGE -o assets.go ./migrations/... ./static/... ./templates/...
@ -29,8 +31,11 @@ import (
"time" "time"
) )
const defaultFileMode = 0400
var defaultFileTimestamp = time.Now() var defaultFileTimestamp = time.Now()
// Simulated asset file system.
type AssetFS struct { type AssetFS struct {
Asset func(path string) ([]byte, error) Asset func(path string) ([]byte, error)
AssetDir func(path string) ([]string, error) AssetDir func(path string) ([]string, error)
@ -46,16 +51,19 @@ type FakeFile struct {
func (f *FakeFile) Name() string { func (f *FakeFile) Name() string {
_, name := filepath.Split(f.Path) _, name := filepath.Split(f.Path)
return name return name
} }
func (f *FakeFile) Size() int64 { return f.Len } func (f *FakeFile) Size() int64 { return f.Len }
func (f *FakeFile) Mode() os.FileMode { func (f *FakeFile) Mode() os.FileMode {
mode := os.FileMode(0644) mode := os.FileMode(defaultFileMode)
if f.Dir { if f.Dir {
return mode | os.ModeDir return mode | os.ModeDir
} }
return mode return mode
} }
@ -73,6 +81,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
if timestamp.IsZero() { if timestamp.IsZero() {
timestamp = defaultFileTimestamp timestamp = defaultFileTimestamp
} }
return &AssetFile{ return &AssetFile{
bytes.NewReader(content), bytes.NewReader(content),
ioutil.NopCloser(nil), ioutil.NopCloser(nil),
@ -80,7 +89,7 @@ func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile {
} }
} }
func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) { func (f *AssetFile) Readdir(int) ([]os.FileInfo, error) {
return nil, errors.New("not a directory") return nil, errors.New("not a directory")
} }
@ -102,24 +111,31 @@ func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) {
if count <= 0 { if count <= 0 {
return f.Children, nil return f.Children, nil
} }
if f.ChildrenRead+count > len(f.Children) { if f.ChildrenRead+count > len(f.Children) {
count = len(f.Children) - f.ChildrenRead count = len(f.Children) - f.ChildrenRead
} }
rv := f.Children[f.ChildrenRead : f.ChildrenRead+count] rv := f.Children[f.ChildrenRead : f.ChildrenRead+count]
f.ChildrenRead += count f.ChildrenRead += count
return rv, nil return rv, nil
} }
// Simulate os.Stat for a simulated asset directory.
func (f *AssetDirectory) Stat() (os.FileInfo, error) { func (f *AssetDirectory) Stat() (os.FileInfo, error) {
return f, nil return f, nil
} }
// Return a new asset directory.
func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory { func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory {
fileInfos := make([]os.FileInfo, 0, len(children)) fileInfos := make([]os.FileInfo, 0, len(children))
for _, child := range children { for _, child := range children {
_, err := fs.AssetDir(filepath.Join(name, child)) _, err := fs.AssetDir(filepath.Join(name, child))
fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}}) fileInfos = append(fileInfos, &FakeFile{child, err == nil, 0, time.Time{}})
} }
return &AssetDirectory{ return &AssetDirectory{
AssetFile{ AssetFile{
bytes.NewReader(nil), bytes.NewReader(nil),
@ -130,29 +146,37 @@ func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirect
fileInfos} fileInfos}
} }
// Open named simulated file.
func (f *AssetFS) Open(name string) (http.File, error) { func (f *AssetFS) Open(name string) (http.File, error) {
if len(name) > 0 && name[0] == '/' { if len(name) > 0 && name[0] == '/' {
name = name[1:] name = name[1:]
} }
if b, err := f.Asset(name); err == nil { if b, err := f.Asset(name); err == nil {
timestamp := defaultFileTimestamp timestamp := defaultFileTimestamp
if f.AssetInfo != nil { if f.AssetInfo != nil {
if info, err := f.AssetInfo(name); err == nil { if info, err := f.AssetInfo(name); err == nil {
timestamp = info.ModTime() timestamp = info.ModTime()
} }
} }
return NewAssetFile(name, b, timestamp), nil return NewAssetFile(name, b, timestamp), nil
} }
if children, err := f.AssetDir(name); err == nil {
children, err := f.AssetDir(name)
if err == nil {
return NewAssetDirectory(name, children, f), nil return NewAssetDirectory(name, children, f), nil
} else {
if strings.Contains(err.Error(), "not found") {
return nil, os.ErrNotExist
}
return nil, err
} }
if strings.Contains(err.Error(), "not found") {
return nil, os.ErrNotExist
}
return nil, err
} }
// Get the simulated asset filesystem initialized with generated values.
func GetAssetFS() *AssetFS { func GetAssetFS() *AssetFS {
return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo} return &AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo}
} }

@ -1,23 +1,24 @@
/* /*
Copyright 2017-2019 Jan Dittberner Copyright 2017-2020 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package db package db
import ( import (
migrate "github.com/rubenv/sql-migrate"
"git.cacert.org/cacert-boardvoting/boardvoting" "git.cacert.org/cacert-boardvoting/boardvoting"
"github.com/rubenv/sql-migrate"
) )
func Migrations() migrate.MigrationSource { func Migrations() migrate.MigrationSource {

@ -1,18 +1,19 @@
/* /*
Copyright 2017-2019 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main
import ( import (
@ -22,11 +23,24 @@ import (
"time" "time"
) )
const (
minimumContentLen = 3
minimumTitleLen = 3
)
const (
hoursInADay = 24
dueThreeDays = 3
dueOneWeek = 7
dueTwoWeeks = 14
dueFourWeeks = 28
)
var validDueDurations = map[string]time.Duration{ var validDueDurations = map[string]time.Duration{
"+3 days": time.Hour * 24 * 3, "+3 days": time.Hour * hoursInADay * dueThreeDays,
"+7 days": time.Hour * 24 * 7, "+7 days": time.Hour * hoursInADay * dueOneWeek,
"+14 days": time.Hour * 24 * 14, "+14 days": time.Hour * hoursInADay * dueTwoWeeks,
"+28 days": time.Hour * 24 * 28, "+28 days": time.Hour * hoursInADay * dueFourWeeks,
} }
type NewDecisionForm struct { type NewDecisionForm struct {
@ -43,13 +57,13 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
data := &Decision{} data := &Decision{}
data.Title = strings.TrimSpace(f.Title) data.Title = strings.TrimSpace(f.Title)
if len(data.Title) < 3 { if len(data.Title) < minimumTitleLen {
f.Errors["Title"] = "Please enter at least 3 characters for Title." f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
} }
data.Content = strings.TrimSpace(f.Content) data.Content = strings.TrimSpace(f.Content)
if len(strings.Fields(data.Content)) < 3 { if len(strings.Fields(data.Content)) < minimumContentLen {
f.Errors["Content"] = "Please enter at least 3 words as Text." f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
} }
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) { if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@ -83,13 +97,13 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
data := f.Decision data := f.Decision
data.Title = strings.TrimSpace(f.Title) data.Title = strings.TrimSpace(f.Title)
if len(data.Title) < 3 { if len(data.Title) < minimumTitleLen {
f.Errors["Title"] = "Please enter at least 3 characters for Title." f.Errors["Title"] = fmt.Sprintf("Please enter at least %d characters for Title.", minimumTitleLen)
} }
data.Content = strings.TrimSpace(f.Content) data.Content = strings.TrimSpace(f.Content)
if len(strings.Fields(data.Content)) < 3 { if len(strings.Fields(data.Content)) < minimumContentLen {
f.Errors["Content"] = "Please enter at least 3 words as Text." f.Errors["Content"] = fmt.Sprintf("Please enter at least %d words as Text.", minimumContentLen)
} }
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) { if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
@ -118,27 +132,34 @@ type ProxyVoteForm struct {
func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) { func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) {
f.Errors = make(map[string]string) f.Errors = make(map[string]string)
const minimumJustificationLen = 3
var (
voter *Voter
err error
voterID, vote int64
)
data := &Vote{} data := &Vote{}
var voter *Voter if voterID, err = strconv.ParseInt(f.Voter, 10, 64); err != nil {
if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil { f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err) } else if voter, err = GetVoterByID(voterID); err != nil {
} else if voter, err = GetVoterById(voterId); err != nil { f.Errors["Voter"] = fmt.Sprintf("Please choose a valid voter: %v.", err)
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter.", err)
} else { } else {
data.VoterId = voter.Id data.VoterID = voter.ID
} }
if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil { if vote, err = strconv.ParseInt(f.Vote, 10, 8); err != nil {
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.", err) f.Errors["Vote"] = fmt.Sprintf("Please choose a valid vote: %v.", err)
} else if voteChoice, ok := VoteChoices[vote]; !ok { } else if voteChoice, ok := VoteChoices[vote]; !ok {
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote.") f.Errors["Vote"] = "Please choose a valid vote."
} else { } else {
data.Vote = voteChoice data.Vote = voteChoice
} }
justification := strings.TrimSpace(f.Justification) justification := strings.TrimSpace(f.Justification)
if len(justification) < 3 { if len(justification) < minimumJustificationLen {
f.Errors["Justification"] = "Please enter at least 3 characters for justification." f.Errors["Justification"] = "Please enter at least 3 characters for justification."
} }

@ -4,23 +4,26 @@ go 1.14
require ( require (
github.com/Masterminds/goutils v1.1.0 // indirect github.com/Masterminds/goutils v1.1.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.20.0+incompatible github.com/Masterminds/sprig v2.22.0+incompatible
github.com/gobuffalo/packr v1.30.1 // indirect github.com/gobuffalo/packr v1.30.1 // indirect
github.com/google/uuid v1.1.1 // indirect github.com/google/uuid v1.1.4 // indirect
github.com/gorilla/csrf v1.6.0 github.com/gorilla/csrf v1.7.0
github.com/gorilla/sessions v1.2.0 github.com/gorilla/sessions v1.2.1
github.com/huandu/xstrings v1.2.0 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.7 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/jmoiron/sqlx v1.2.0 github.com/jmoiron/sqlx v1.2.0
github.com/mattn/go-sqlite3 v1.11.0 github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 github.com/mattn/go-sqlite3 v1.14.6
github.com/sirupsen/logrus v1.4.2 github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351
github.com/sirupsen/logrus v1.7.0
github.com/ziutek/mymysql v1.5.4 // indirect github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
google.golang.org/appengine v1.6.1 // indirect golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/gorp.v1 v1.7.2 // indirect gopkg.in/gorp.v1 v1.7.2 // indirect
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.4.0
) )

369
go.sum

@ -1,52 +1,191 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8= github.com/Masterminds/sprig v2.20.0+incompatible h1:dJTKKuUkYW3RMFdQFXPU/s6hg10RgctmTjRcbZ98Ap8=
github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.20.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU= github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.4 h1:0ecGp3skIrHWPNGPJDaBIghfA6Sp7Ruo2Io8eLKzWm0=
github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.6.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0= github.com/gorilla/csrf v1.6.0 h1:60oN1cFdncCE8tjwQ3QEkFND5k37lQPcRjnlvm7CIJ0=
github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI=
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -54,75 +193,305 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE=
github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 h1:xPeaaIHjF9j8jbYQ5xdvLnFp+lpmGYFG1uBPtXNBHno= github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079 h1:xPeaaIHjF9j8jbYQ5xdvLnFp+lpmGYFG1uBPtXNBHno=
github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= github.com/rubenv/sql-migrate v0.0.0-20190717103323-87ce952f7079/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY=
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk=
github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 h1:ydJNl0ENAG67pFbB+9tfhiL2pYqLhfoaZFw/cjLhY4A=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c h1:+EXw7AwNOKzPFXMZ1yNjO40aWCh3PIquJB2fYlv9wcs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw=
gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

@ -1,23 +1,25 @@
/* /*
Copyright 2017-2019 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main
import ( import (
log "github.com/sirupsen/logrus"
"time" "time"
log "github.com/sirupsen/logrus"
) )
type Job interface { type Job interface {
@ -29,30 +31,34 @@ type Job interface {
type jobIdentifier int type jobIdentifier int
const ( const (
JobIdCloseDecisions jobIdentifier = iota JobIDCloseDecisions jobIdentifier = iota
JobIdRemindVotersJob JobIDRemindVotersJob
) )
var rescheduleChannel = make(chan jobIdentifier, 1) var rescheduleChannel = make(chan jobIdentifier, 1)
func JobScheduler(quitChannel chan int) { func JobScheduler(quitChannel chan int) {
var jobs = map[jobIdentifier]Job{ var jobs = map[jobIdentifier]Job{
JobIdCloseDecisions: NewCloseDecisionsJob(), JobIDCloseDecisions: NewCloseDecisionsJob(),
JobIdRemindVotersJob: NewRemindVotersJob(), JobIDRemindVotersJob: NewRemindVotersJob(),
} }
log.Info("started job scheduler") log.Info("started job scheduler")
for { for {
select { select {
case jobId := <-rescheduleChannel: case jobID := <-rescheduleChannel:
job := jobs[jobId] job := jobs[jobID]
log.Infof("reschedule job %s", job) log.Infof("reschedule job %s", job)
job.Schedule() job.Schedule()
case <-quitChannel: case <-quitChannel:
for _, job := range jobs { for _, job := range jobs {
job.Stop() job.Stop()
} }
log.Info("stop job scheduler") log.Info("stop job scheduler")
return return
} }
} }
@ -65,27 +71,35 @@ type CloseDecisionsJob struct {
func NewCloseDecisionsJob() *CloseDecisionsJob { func NewCloseDecisionsJob() *CloseDecisionsJob {
job := &CloseDecisionsJob{} job := &CloseDecisionsJob{}
job.Schedule() job.Schedule()
return job return job
} }
func (j *CloseDecisionsJob) Schedule() { func (j *CloseDecisionsJob) Schedule() {
var nextDue *time.Time var (
nextDue, err := GetNextPendingDecisionDue() nextDue *time.Time
err error
)
nextDue, err = GetNextPendingDecisionDue()
if err != nil { if err != nil {
log.Error("Could not get next pending due date") log.Error("Could not get next pending due date")
if j.timer != nil { if j.timer != nil {
j.timer.Stop() j.timer.Stop()
j.timer = nil j.timer = nil
} }
return return
} }
if nextDue == nil { if nextDue == nil {
log.Info("no next planned execution of CloseDecisionsJob") log.Info("no next planned execution of CloseDecisionsJob")
j.Stop() j.Stop()
} else { } else {
nextDue := nextDue.Add(time.Second) nextDue := nextDue.Add(time.Second)
log.Infof("scheduling CloseDecisionsJob for %s", nextDue) log.Infof("scheduling CloseDecisionsJob for %s", nextDue)
when := nextDue.Sub(time.Now()) when := time.Until(nextDue)
if j.timer != nil { if j.timer != nil {
j.timer.Reset(when) j.timer.Reset(when)
} else { } else {
@ -103,11 +117,13 @@ func (j *CloseDecisionsJob) Stop() {
func (j *CloseDecisionsJob) Run() { func (j *CloseDecisionsJob) Run() {
log.Debug("running CloseDecisionsJob") log.Debug("running CloseDecisionsJob")
err := CloseDecisions() err := CloseDecisions()
if err != nil { if err != nil {
log.Errorf("closing decisions %v", err) log.Errorf("closing decisions %v", err)
} }
rescheduleChannel <- JobIdCloseDecisions
rescheduleChannel <- JobIDCloseDecisions
} }
func (j *CloseDecisionsJob) String() string { func (j *CloseDecisionsJob) String() string {
@ -121,14 +137,18 @@ type RemindVotersJob struct {
func NewRemindVotersJob() *RemindVotersJob { func NewRemindVotersJob() *RemindVotersJob {
job := &RemindVotersJob{} job := &RemindVotersJob{}
job.Schedule() job.Schedule()
return job return job
} }
func (j *RemindVotersJob) Schedule() { func (j *RemindVotersJob) Schedule() {
year, month, day := time.Now().UTC().Date() year, month, day := time.Now().UTC().Date()
nextExecution := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 3) nextExecution := time.Date(year, month, day, 0, 0, 0, 0, time.UTC).AddDate(0, 0, 3)
log.Infof("scheduling RemindVotersJob for %s", nextExecution) log.Infof("scheduling RemindVotersJob for %s", nextExecution)
when := nextExecution.Sub(time.Now())
when := time.Until(nextExecution)
if j.timer != nil { if j.timer != nil {
j.timer.Reset(when) j.timer.Reset(when)
} else { } else {
@ -145,22 +165,33 @@ func (j *RemindVotersJob) Stop() {
func (j *RemindVotersJob) Run() { func (j *RemindVotersJob) Run() {
log.Info("running RemindVotersJob") log.Info("running RemindVotersJob")
defer func() { rescheduleChannel <- JobIdRemindVotersJob }()
voters, err := GetReminderVoters() defer func() { rescheduleChannel <- JobIDRemindVotersJob }()
var (
voters []Voter
err error
)
voters, err = GetReminderVoters()
if err != nil { if err != nil {
log.Errorf("problem getting voters %v", err) log.Errorf("problem getting voters %v", err)
return return
} }
for _, voter := range *voters { var decisions []Decision
decisions, err := FindUnvotedDecisionsForVoter(&voter)
for i := range voters {
decisions, err = FindUnVotedDecisionsForVoter(&voters[i])
if err != nil { if err != nil {
log.Errorf("problem getting unvoted decisions: %v", err) log.Errorf("problem getting unvoted decisions: %v", err)
return return
} }
if len(*decisions) > 0 {
NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions} if len(decisions) > 0 {
NotifyMailChannel <- &RemindVoterNotification{voter: voters[i], decisions: decisions}
} }
} }
} }

@ -1,51 +1,54 @@
/* /*
Copyright 2017-2020 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main
import ( import (
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"github.com/jmoiron/sqlx"
"github.com/rubenv/sql-migrate"
"time" "time"
migrations "git.cacert.org/cacert-boardvoting/db" "github.com/jmoiron/sqlx"
migrate "github.com/rubenv/sql-migrate"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
migrations "git.cacert.org/cacert-boardvoting/db"
) )
type sqlKey int type sqlKey int
const ( const (
sqlLoadDecisions sqlKey = iota sqlLoadDecisions sqlKey = iota
sqlLoadUnvotedDecisions sqlLoadUnVotedDecisions
sqlLoadDecisionByTag sqlLoadDecisionByTag
sqlLoadDecisionById sqlLoadDecisionByID
sqlLoadVoteCountsForDecision sqlLoadVoteCountsForDecision
sqlLoadVotesForDecision sqlLoadVotesForDecision
sqlLoadEnabledVoterByEmail sqlLoadEnabledVoterByEmail
sqlCountOlderThanDecision sqlCountOlderThanDecision
sqlCountOlderThanUnvotedDecision sqlCountOlderThanUnVotedDecision
sqlCreateDecision sqlCreateDecision
sqlUpdateDecision sqlUpdateDecision
sqlUpdateDecisionStatus sqlUpdateDecisionStatus
sqlSelectClosableDecisions sqlSelectClosableDecisions
sqlGetNextPendingDecisionDue sqlGetNextPendingDecisionDue
sqlGetReminderVoters sqlGetReminderVoters
sqlFindUnvotedDecisionsForVoter sqlFindUnVotedDecisionsForVoter
sqlGetEnabledVoterById sqlGetEnabledVoterByID
sqlCreateVote sqlCreateVote
sqlLoadVote sqlLoadVote
sqlGetVotersForProxy sqlGetVotersForProxy
@ -59,7 +62,7 @@ FROM decisions
JOIN voters ON decisions.proponent=voters.id JOIN voters ON decisions.proponent=voters.id
ORDER BY proposed DESC ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $1`, LIMIT 10 OFFSET 10 * $1`,
sqlLoadUnvotedDecisions: ` sqlLoadUnVotedDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title, SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM decisions FROM decisions
@ -73,7 +76,7 @@ SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer
FROM decisions FROM decisions
JOIN voters ON decisions.proponent=voters.id JOIN voters ON decisions.proponent=voters.id
WHERE decisions.tag=$1;`, WHERE decisions.tag=$1;`,
sqlLoadDecisionById: ` sqlLoadDecisionByID: `
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content, SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
decisions.votetype, decisions.status, decisions.due, decisions.modified decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM decisions FROM decisions
@ -91,14 +94,14 @@ FROM voters
JOIN emails ON voters.id=emails.voter JOIN emails ON voters.id=emails.voter
JOIN user_roles ON user_roles.voter_id=voters.id JOIN user_roles ON user_roles.voter_id=voters.id
WHERE emails.address=$1 AND user_roles.role='VOTER'`, WHERE emails.address=$1 AND user_roles.role='VOTER'`,
sqlGetEnabledVoterById: ` sqlGetEnabledVoterByID: `
SELECT voters.id, voters.name, voters.reminder SELECT voters.id, voters.name, voters.reminder
FROM voters FROM voters
JOIN user_roles ON user_roles.voter_id=voters.id JOIN user_roles ON user_roles.voter_id=voters.id
WHERE user_roles.role='VOTER' AND voters.id=$1`, WHERE user_roles.role='VOTER' AND voters.id=$1`,
sqlCountOlderThanDecision: ` sqlCountOlderThanDecision: `
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`, SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
sqlCountOlderThanUnvotedDecision: ` sqlCountOlderThanUnVotedDecision: `
SELECT COUNT(*) > 0 FROM decisions SELECT COUNT(*) > 0 FROM decisions
WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`, WHERE proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
sqlCreateDecision: ` sqlCreateDecision: `
@ -134,7 +137,7 @@ SELECT voters.id, voters.name, voters.reminder
FROM voters FROM voters
JOIN user_roles ON user_roles.voter_id=voters.id JOIN user_roles ON user_roles.voter_id=voters.id
WHERE user_roles.role='VOTER' AND reminder!='' AND reminder IS NOT NULL`, WHERE user_roles.role='VOTER' AND reminder!='' AND reminder IS NOT NULL`,
sqlFindUnvotedDecisionsForVoter: ` sqlFindUnVotedDecisionsForVoter: `
SELECT tag, title, votetype, due SELECT tag, title, votetype, due
FROM decisions FROM decisions
WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1) WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
@ -152,9 +155,9 @@ type VoteType uint8
type VoteStatus int8 type VoteStatus int8
type Decision struct { type Decision struct {
Id int64 ID int64 `db:"id"`
Proposed time.Time Proposed time.Time
ProponentId int64 `db:"proponent"` ProponentID int64 `db:"proponent"`
Title string Title string
Content string Content string
Quorum int Quorum int
@ -167,7 +170,7 @@ type Decision struct {
} }
type Voter struct { type Voter struct {
Id int64 ID int64 `db:"id"`
Name string Name string
Reminder string // reminder email address Reminder string // reminder email address
} }
@ -185,23 +188,36 @@ const (
voteTypeVeto = 1 voteTypeVeto = 1
) )
const (
voteTypeLabelMotion = "motion"
voteTypeLabelUnknown = "unknown"
voteTypeLabelVeto = "veto"
)
func (v VoteType) String() string { func (v VoteType) String() string {
switch v { switch v {
case voteTypeMotion: case voteTypeMotion:
return "motion" return voteTypeLabelMotion
case voteTypeVeto: case voteTypeVeto:
return "veto" return voteTypeLabelVeto
default: default:
return "unknown" return voteTypeLabelUnknown
} }
} }
func (v VoteType) QuorumAndMajority() (int, int) { func (v VoteType) QuorumAndMajority() (int, float32) {
const (
majorityDefault = 0.99
majorityMotion = 0.50
quorumDefault = 1
quorumMotion = 3
)
switch v { switch v {
case voteTypeMotion: case voteTypeMotion:
return 3, 50 return quorumMotion, majorityMotion
default: default:
return 1, 99 return quorumDefault, majorityDefault
} }
} }
@ -253,36 +269,42 @@ func (v VoteStatus) String() string {
} }
type Vote struct { type Vote struct {
DecisionId int64 `db:"decision"` DecisionID int64 `db:"decision"`
VoterId int64 `db:"voter"` VoterID int64 `db:"voter"`
Vote VoteChoice Vote VoteChoice
Voted time.Time Voted time.Time
Notes string Notes string
} }
type dbHandler struct { type DbHandler struct {
db *sqlx.DB db *sqlx.DB
} }
var db *dbHandler var db *DbHandler
func NewDB(database *sql.DB) *DbHandler {
handler := &DbHandler{db: sqlx.NewDb(database, "sqlite3")}
func NewDB(database *sql.DB) *dbHandler {
handler := &dbHandler{db: sqlx.NewDb(database, "sqlite3")}
_, err := migrate.Exec(database, "sqlite3", migrations.Migrations(), migrate.Up) _, err := migrate.Exec(database, "sqlite3", migrations.Migrations(), migrate.Up)
if err != nil { if err != nil {
log.Panicf("running database migration failed: %v", err) log.Panicf("running database migration failed: %v", err)
} }
failedStatements := make([]string, 0) failedStatements := make([]string, 0)
for _, sqlStatement := range sqlStatements { for _, sqlStatement := range sqlStatements {
var stmt *sqlx.Stmt var stmt *sqlx.Stmt
stmt, err := handler.db.Preparex(sqlStatement) stmt, err := handler.db.Preparex(sqlStatement)
if err != nil { if err != nil {
log.Errorf("error parsing statement %s: %s", sqlStatement, err) log.Errorf("error parsing statement %s: %s", sqlStatement, err)
failedStatements = append(failedStatements, sqlStatement) failedStatements = append(failedStatements, sqlStatement)
} }
// nolint:sqlclosecheck
_ = stmt.Close() _ = stmt.Close()
} }
if len(failedStatements) > 0 { if len(failedStatements) > 0 {
log.Panicf("%d statements failed to prepare", len(failedStatements)) log.Panicf("%d statements failed to prepare", len(failedStatements))
} }
@ -290,44 +312,48 @@ func NewDB(database *sql.DB) *dbHandler {
return handler return handler
} }
func (d *dbHandler) Close() error { func (d *DbHandler) Close() error {
return d.db.Close() return d.db.Close()
} }
func (d *dbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt { func (d *DbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
statement, err := d.db.PrepareNamed(sqlStatements[statementKey]) statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
if err != nil { if err != nil {
log.Panicf("Preparing statement failed: %v", err) log.Panicf("Preparing statement failed: %v", err)
} }
return statement return statement
} }
func (d *dbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt { func (d *DbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
statement, err := d.db.Preparex(sqlStatements[statementKey]) statement, err := d.db.Preparex(sqlStatements[statementKey])
if err != nil { if err != nil {
log.Panicf("Preparing statement failed: %v", err) log.Panicf("Preparing statement failed: %v", err)
} }
return statement return statement
} }
func (v *Vote) Save() (err error) { func (v *Vote) Save() error {
insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote) insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)
defer func() { _ = insertVoteStmt.Close() }() defer func() { _ = insertVoteStmt.Close() }()
var err error
if _, err = insertVoteStmt.Exec(v); err != nil { if _, err = insertVoteStmt.Exec(v); err != nil {
log.Errorf("saving vote failed: %v", err) return fmt.Errorf("saving vote failed: %w", err)
return
} }
getVoteStmt := db.getPreparedStatement(sqlLoadVote) getVoteStmt := db.getPreparedStatement(sqlLoadVote)
defer func() { _ = getVoteStmt.Close() }() defer func() { _ = getVoteStmt.Close() }()
if err = getVoteStmt.Get(v, v.DecisionId, v.VoterId); err != nil { if err = getVoteStmt.Get(v, v.DecisionID, v.VoterID); err != nil {
log.Errorf("getting inserted vote failed: %v", err) return fmt.Errorf("getting inserted vote failed: %w", err)
return
} }
return return nil
} }
type VoteSums struct { type VoteSums struct {
@ -349,18 +375,20 @@ func (v *VoteSums) Percent() int {
if totalVotes == 0 { if totalVotes == 0 {
return 0 return 0
} }
return v.Ayes * 100 / totalVotes return v.Ayes * 100 / totalVotes
} }
func (v *VoteSums) CalculateResult(quorum int, majority int) (status VoteStatus, reasoning string) { func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, string) {
if v.VoteCount() < quorum { if v.VoteCount() < quorum {
status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum) return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
} else if (v.Ayes / v.TotalVotes()) < (majority / 100) {
status, reasoning = voteStatusDeclined, fmt.Sprintf("Needed majority of %d%% has not been reached.", majority)
} else {
status, reasoning = voteStatusApproved, "Quorum and majority have been reached"
} }
return
if (float32(v.Ayes) / float32(v.TotalVotes())) < majority {
return voteStatusDeclined, fmt.Sprintf("Needed majority of %0.2f%% has not been reached.", majority)
}
return voteStatusApproved, "Quorum and majority have been reached"
} }
type VoteForDisplay struct { type VoteForDisplay struct {
@ -375,23 +403,26 @@ type DecisionForDisplay struct {
Votes []VoteForDisplay Votes []VoteForDisplay
} }
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) { func FindDecisionForDisplayByTag(tag string) (*DecisionForDisplay, error) {
decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag) decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)
defer func() { _ = decisionStmt.Close() }() defer func() { _ = decisionStmt.Close() }()
decision = &DecisionForDisplay{} decision := &DecisionForDisplay{}
var err error
if err = decisionStmt.Get(decision, tag); err != nil { if err = decisionStmt.Get(decision, tag); err != nil {
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
decision = nil return nil, nil
err = nil
return
} else {
log.Errorf("getting motion %s failed: %v", tag, err)
return
} }
return nil, fmt.Errorf("getting motion %s failed: %w", tag, err)
} }
decision.VoteSums, err = decision.Decision.VoteSums() decision.VoteSums, err = decision.Decision.VoteSums()
return
return decision, err
} }
// FindDecisionsForDisplayOnPage loads a set of decisions from the database. // FindDecisionsForDisplayOnPage loads a set of decisions from the database.
@ -399,61 +430,78 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
// This function uses OFFSET for pagination which is not a good idea for larger data sets. // This function uses OFFSET for pagination which is not a good idea for larger data sets.
// //
// TODO: migrate to timestamp base pagination // TODO: migrate to timestamp base pagination
func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) { func FindDecisionsForDisplayOnPage(page int64, unVoted bool, voter *Voter) ([]*DecisionForDisplay, error) {
var decisionsStmt *sqlx.Stmt var decisionsStmt *sqlx.Stmt
if unvoted && voter != nil {
decisionsStmt = db.getPreparedStatement(sqlLoadUnvotedDecisions) if unVoted && voter != nil {
decisionsStmt = db.getPreparedStatement(sqlLoadUnVotedDecisions)
} else { } else {
decisionsStmt = db.getPreparedStatement(sqlLoadDecisions) decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
} }
defer func() { _ = decisionsStmt.Close() }() defer func() { _ = decisionsStmt.Close() }()
var rows *sqlx.Rows var (
if unvoted && voter != nil { rows *sqlx.Rows
rows, err = decisionsStmt.Queryx(voter.Id, page-1) err error
decisions []*DecisionForDisplay
)
if unVoted && voter != nil {
rows, err = decisionsStmt.Queryx(voter.ID, page-1)
} else { } else {
rows, err = decisionsStmt.Queryx(page - 1) rows, err = decisionsStmt.Queryx(page - 1)
} }
if err != nil { if err != nil {
log.Errorf("loading motions for page %d failed: %v", page, err) return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
return
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
for rows.Next() { for rows.Next() {
var d DecisionForDisplay var d DecisionForDisplay
if err = rows.StructScan(&d); err != nil { if err = rows.StructScan(&d); err != nil {
log.Errorf("loading motions for page %d failed: %v", page, err) return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
return
} }
d.VoteSums, err = d.Decision.VoteSums() d.VoteSums, err = d.Decision.VoteSums()
if err != nil { if err != nil {
return return nil, err
} }
decisions = append(decisions, &d) decisions = append(decisions, &d)
} }
return
return decisions, nil
} }
func (d *Decision) VoteSums() (sums *VoteSums, err error) { func (d *Decision) VoteSums() (*VoteSums, error) {
votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision) votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)
defer func() { _ = votesStmt.Close() }() defer func() { _ = votesStmt.Close() }()
voteRows, err := votesStmt.Queryx(d.Id) voteRows, err := votesStmt.Queryx(d.ID)
if err != nil { if err != nil {
log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
return
} }
defer func() { _ = voteRows.Close() }() defer func() { _ = voteRows.Close() }()
sums = &VoteSums{} sums := &VoteSums{}
for voteRows.Next() { for voteRows.Next() {
var vote VoteChoice var (
var count int vote VoteChoice
count int
)
if err = voteRows.Scan(&vote, &count); err != nil { if err = voteRows.Scan(&vote, &count); err != nil {
log.Errorf("fetching vote sums for motion %s failed: %v", d.Tag, err) return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
return
} }
switch vote { switch vote {
case voteAye: case voteAye:
sums.Ayes = count sums.Ayes = count
@ -463,79 +511,86 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
sums.Abstains = count sums.Abstains = count
} }
} }
return
return sums, nil
} }
func (d *DecisionForDisplay) LoadVotes() (err error) { func (d *DecisionForDisplay) LoadVotes() (err error) {
votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision) votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)
defer func() { _ = votesStmt.Close() }() defer func() { _ = votesStmt.Close() }()
err = votesStmt.Select(&d.Votes, d.Id) err = votesStmt.Select(&d.Votes, d.ID)
if err != nil { if err != nil {
log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err) log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)
return return
} }
return return
} }
func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) { func (d *Decision) OlderExists(unvoted bool, voter *Voter) (bool, error) {
var result bool
if unvoted && voter != nil { if unvoted && voter != nil {
olderStmt := db.getPreparedStatement(sqlCountOlderThanUnvotedDecision) olderStmt := db.getPreparedStatement(sqlCountOlderThanUnVotedDecision)
defer func() { _ = olderStmt.Close() }() defer func() { _ = olderStmt.Close() }()
if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil { if err := olderStmt.Get(&result, d.Proposed, voter.ID); err != nil {
log.Errorf("finding older motions than %s failed: %v", d.Tag, err) return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
return
} }
} else { } else {
olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision) olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)
defer func() { _ = olderStmt.Close() }() defer func() { _ = olderStmt.Close() }()
if err = olderStmt.Get(&result, d.Proposed); err != nil { if err := olderStmt.Get(&result, d.Proposed); err != nil {
log.Errorf("finding older motions than %s failed: %v", d.Tag, err) return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
return
} }
} }
return return result, nil
} }
func (d *Decision) Create() (err error) { func (d *Decision) Create() error {
insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision) insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)
defer func() { _ = insertDecisionStmt.Close() }() defer func() { _ = insertDecisionStmt.Close() }()
result, err := insertDecisionStmt.Exec(d) result, err := insertDecisionStmt.Exec(d)
if err != nil { if err != nil {
log.Errorf("creating motion failed: %v", err) return fmt.Errorf("creating motion failed: %w", err)
return
} }
lastInsertId, err := result.LastInsertId() decisionID, err := result.LastInsertId()
if err != nil { if err != nil {
log.Errorf("getting id of inserted motion failed: %v", err) return fmt.Errorf("getting id of inserted motion failed: %w", err)
return
} }
rescheduleChannel <- JobIdCloseDecisions rescheduleChannel <- JobIDCloseDecisions
getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)
getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById)
defer func() { _ = getDecisionStmt.Close() }() defer func() { _ = getDecisionStmt.Close() }()
err = getDecisionStmt.Get(d, lastInsertId) err = getDecisionStmt.Get(d, decisionID)
if err != nil { if err != nil {
log.Errorf("getting inserted motion failed: %v", err) return fmt.Errorf("getting inserted motion failed: %w", err)
return
} }
return return nil
} }
func (d *Decision) LoadWithId() (err error) { func (d *Decision) LoadWithID() (err error) {
getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionById) getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)
defer func() { _ = getDecisionStmt.Close() }() defer func() { _ = getDecisionStmt.Close() }()
err = getDecisionStmt.Get(d, d.Id) err = getDecisionStmt.Get(d, d.ID)
if err != nil { if err != nil {
log.Errorf("loading updated motion failed: %v", err) log.Errorf("loading updated motion failed: %v", err)
return return
} }
@ -544,93 +599,110 @@ func (d *Decision) LoadWithId() (err error) {
func (d *Decision) Update() (err error) { func (d *Decision) Update() (err error) {
updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision) updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)
defer func() { _ = updateDecisionStmt.Close() }() defer func() { _ = updateDecisionStmt.Close() }()
result, err := updateDecisionStmt.Exec(d) result, err := updateDecisionStmt.Exec(d)
if err != nil { if err != nil {
log.Errorf("updating motion failed: %v", err) log.Errorf("updating motion failed: %v", err)
return return
} }
affectedRows, err := result.RowsAffected() affectedRows, err := result.RowsAffected()
if err != nil { if err != nil {
log.Error("Problem determining the affected rows") log.Error("Problem determining the affected rows")
return return
} else if affectedRows != 1 { } else if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
} }
rescheduleChannel <- JobIdCloseDecisions
err = d.LoadWithId() rescheduleChannel <- JobIDCloseDecisions
err = d.LoadWithID()
return return
} }
func (d *Decision) UpdateStatus() (err error) { func (d *Decision) UpdateStatus() error {
updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
defer func() { _ = updateStatusStmt.Close() }() defer func() { _ = updateStatusStmt.Close() }()
result, err := updateStatusStmt.Exec(d) result, err := updateStatusStmt.Exec(d)
if err != nil { if err != nil {
log.Errorf("setting motion status failed: %v", err) return fmt.Errorf("setting motion status failed: %w", err)
return
} }
affectedRows, err := result.RowsAffected() affectedRows, err := result.RowsAffected()
if err != nil { if err != nil {
log.Errorf("determining the affected rows failed: %v", err) return fmt.Errorf("determining the affected rows failed: %w", err)
return
} else if affectedRows != 1 { } else if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
} }
rescheduleChannel <- JobIdCloseDecisions
err = d.LoadWithId() rescheduleChannel <- JobIDCloseDecisions
return
err = d.LoadWithID()
return err
} }
func (d *Decision) String() string { func (d *Decision) String() string {
return fmt.Sprintf("%s %s (Id %d)", d.Tag, d.Title, d.Id) return fmt.Sprintf("%s %s (ID %d)", d.Tag, d.Title, d.ID)
} }
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { func FindVoterByAddress(emailAddress string) (*Voter, error) {
findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail) findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)
defer func() { _ = findVoterStmt.Close() }() defer func() { _ = findVoterStmt.Close() }()
voter = &Voter{} voter := &Voter{}
if err = findVoterStmt.Get(voter, emailAddress); err != nil { if err := findVoterStmt.Get(voter, emailAddress); err != nil {
if err != sql.ErrNoRows { if !errors.Is(err, sql.ErrNoRows) {
log.Errorf("getting voter for address %s failed: %v", emailAddress, err) return nil, fmt.Errorf("getting voter for address %s failed: %w", emailAddress, err)
} else {
err = nil
voter = nil
} }
voter = nil
} }
return
return voter, nil
} }
func (d *Decision) Close() error { func (d *Decision) Close() error {
quorum, majority := d.VoteType.QuorumAndMajority() quorum, majority := d.VoteType.QuorumAndMajority()
var voteSums *VoteSums var (
var err error voteSums *VoteSums
err error
)
if voteSums, err = d.VoteSums(); err != nil { if voteSums, err = d.VoteSums(); err != nil {
log.Errorf("getting vote sums failed: %v", err) log.Errorf("getting vote sums failed: %v", err)
return err return err
} }
var reasoning string var reasoning string
d.Status, reasoning = voteSums.CalculateResult(quorum, majority) d.Status, reasoning = voteSums.CalculateResult(quorum, majority)
closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus) closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)
defer func() { _ = closeDecisionStmt.Close() }() defer func() { _ = closeDecisionStmt.Close() }()
result, err := closeDecisionStmt.Exec(d) result, err := closeDecisionStmt.Exec(d)
if err != nil { if err != nil {
log.Errorf("closing vote failed: %v", err) return fmt.Errorf("closing vote failed: %w", err)
return err
} }
if affectedRows, err := result.RowsAffected(); err != nil {
log.Errorf("getting affected rows failed: %v", err) affectedRows, err := result.RowsAffected()
return err if err != nil {
} else if affectedRows != 1 { return fmt.Errorf("getting affected rows failed: %w", err)
}
if affectedRows != 1 {
log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows) log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
} }
@ -641,110 +713,117 @@ func (d *Decision) Close() error {
return nil return nil
} }
func CloseDecisions() (err error) { func CloseDecisions() error {
getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions) getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)
defer func() { _ = getClosableDecisionsStmt.Close() }() defer func() { _ = getClosableDecisionsStmt.Close() }()
decisions := make([]*Decision, 0) decisions := make([]*Decision, 0)
rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()}) rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
if err != nil { if err != nil {
log.Errorf("fetching closable decisions failed: %v", err) return fmt.Errorf("fetching closable decisions failed: %w", err)
return
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
for rows.Next() { for rows.Next() {
decision := &Decision{} decision := &Decision{}
if err = rows.StructScan(decision); err != nil { if err = rows.StructScan(decision); err != nil {
log.Errorf("scanning row failed: %v", err) return fmt.Errorf("scanning row failed: %w", err)
return
} }
decisions = append(decisions, decision) decisions = append(decisions, decision)
} }
defer func() { _ = rows.Close() }() defer func() { _ = rows.Close() }()
for _, decision := range decisions { for _, decision := range decisions {
log.Infof("found closable decision %s", decision.Tag) log.Infof("found closable decision %s", decision.Tag)
if err = decision.Close(); err != nil { if err = decision.Close(); err != nil {
log.Errorf("closing decision %s failed: %s", decision.Tag, err) return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
return
} }
} }
return return nil
} }
func GetNextPendingDecisionDue() (due *time.Time, err error) { func GetNextPendingDecisionDue() (*time.Time, error) {
getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue) getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)
defer func() { _ = getNextPendingDecisionDueStmt.Close() }() defer func() { _ = getNextPendingDecisionDueStmt.Close() }()
row := getNextPendingDecisionDueStmt.QueryRow() row := getNextPendingDecisionDueStmt.QueryRow()
due = &time.Time{} due := &time.Time{}
if err = row.Scan(due); err != nil { if err := row.Scan(due); err != nil {
if err == sql.ErrNoRows { if errors.Is(err, sql.ErrNoRows) {
log.Debug("No pending decisions") log.Debug("No pending decisions")
return nil, nil return nil, nil
} }
log.Errorf("parsing result failed: %v", err)
return nil, err return nil, fmt.Errorf("parsing result failed: %w", err)
} }
return return due, nil
} }
func GetReminderVoters() (voters *[]Voter, err error) { func GetReminderVoters() ([]Voter, error) {
getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters) getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)
defer func() { _ = getReminderVotersStmt.Close() }() defer func() { _ = getReminderVotersStmt.Close() }()
voterSlice := make([]Voter, 0) var voters []Voter
if err = getReminderVotersStmt.Select(&voterSlice); err != nil { if err := getReminderVotersStmt.Select(&voters); err != nil {
log.Errorf("getting voters failed: %v", err) return nil, fmt.Errorf("getting voters failed: %w", err)
return
} }
voters = &voterSlice
return return voters, nil
} }
func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err error) { func FindUnVotedDecisionsForVoter(voter *Voter) ([]Decision, error) {
findUnvotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnvotedDecisionsForVoter) findUnVotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnVotedDecisionsForVoter)
defer func() { _ = findUnvotedDecisionsForVoterStmt.Close() }()
decisionsSlice := make([]Decision, 0) defer func() { _ = findUnVotedDecisionsForVoterStmt.Close() }()
if err = findUnvotedDecisionsForVoterStmt.Select(&decisionsSlice, voter.Id); err != nil { var decisions []Decision
log.Errorf("getting unvoted decisions failed: %v", err)
return if err := findUnVotedDecisionsForVoterStmt.Select(&decisions, voter.ID); err != nil {
return nil, fmt.Errorf("getting unvoted decisions failed: %w", err)
} }
decisions = &decisionsSlice
return return decisions, nil
} }
func GetVoterById(id int64) (voter *Voter, err error) { func GetVoterByID(id int64) (*Voter, error) {
getVoterByIdStmt := db.getPreparedStatement(sqlGetEnabledVoterById) getVoterByIDStmt := db.getPreparedStatement(sqlGetEnabledVoterByID)
defer func() { _ = getVoterByIdStmt.Close() }()
voter = &Voter{} defer func() { _ = getVoterByIDStmt.Close() }()
if err = getVoterByIdStmt.Get(voter, id); err != nil {
log.Errorf("getting voter failed: %v", err) voter := &Voter{}
return if err := getVoterByIDStmt.Get(voter, id); err != nil {
return nil, fmt.Errorf("getting voter failed: %w", err)
} }
return return voter, nil
} }
func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) { func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy) getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)
defer func() { _ = getVotersForProxyStmt.Close() }() defer func() { _ = getVotersForProxyStmt.Close() }()
votersSlice := make([]Voter, 0) votersSlice := make([]Voter, 0)
if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id); err != nil { if err = getVotersForProxyStmt.Select(&votersSlice, proxy.ID); err != nil {
log.Errorf("Error getting voters for proxy failed: %v", err) log.Errorf("Error getting voters for proxy failed: %v", err)
return return
} }
voters = &votersSlice voters = &votersSlice
return return

@ -1,27 +1,30 @@
/* /*
Copyright 2017-2019 Jan Dittberner Copyright 2017-2021 Jan Dittberner
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this program except in compliance with the License. you may not use this program except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"git.cacert.org/cacert-boardvoting/boardvoting" "text/template"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
"text/template"
"git.cacert.org/cacert-boardvoting/boardvoting"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -37,7 +40,7 @@ type recipientData struct {
field, address, name string field, address, name string
} }
type notificationContent struct { type NotificationContent struct {
template string template string
data interface{} data interface{}
subject string subject string
@ -46,32 +49,39 @@ type notificationContent struct {
} }
type NotificationMail interface { type NotificationMail interface {
GetNotificationContent() *notificationContent GetNotificationContent() *NotificationContent
} }
var NotifyMailChannel = make(chan NotificationMail, 1) var NotifyMailChannel = make(chan NotificationMail, 1)
func MailNotifier(quitMailNotifier chan int) { func MailNotifier(quitMailNotifier chan int) {
log.Info("Launched mail notifier") log.Info("Launched mail notifier")
for { for {
select { select {
case notification := <-NotifyMailChannel: case notification := <-NotifyMailChannel:
content := notification.GetNotificationContent() content := notification.GetNotificationContent()
mailText, err := buildMail(content.template, content.data) mailText, err := buildMail(content.template, content.data)
if err != nil { if err != nil {
log.Errorf("building mail failed: %v", err) log.Errorf("building mail failed: %v", err)
continue continue
} }
m := gomail.NewMessage() m := gomail.NewMessage()
m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system") m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system")
for _, recipient := range content.recipients { for _, recipient := range content.recipients {
m.SetAddressHeader(recipient.field, recipient.address, recipient.name) m.SetAddressHeader(recipient.field, recipient.address, recipient.name)
} }
m.SetHeader("Subject", content.subject) m.SetHeader("Subject", content.subject)
for _, header := range content.headers { for _, header := range content.headers {
m.SetHeader(header.name, header.value...) m.SetHeader(header.name, header.value...)
} }
m.SetBody("text/plain", mailText.String()) m.SetBody("text/plain", mailText.String())
d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "") d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
@ -80,6 +90,7 @@ func MailNotifier(quitMailNotifier chan int) {
} }
case <-quitMailNotifier: case <-quitMailNotifier:
log.Info("Ending mail notifier") log.Info("Ending mail notifier")
return return
} }
} }
@ -90,6 +101,7 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
if err != nil { if err != nil {
return return
} }
t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b)) t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b))
if err != nil { if err != nil {
return return
@ -97,8 +109,10 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
mailText = bytes.NewBufferString("") mailText = bytes.NewBufferString("")
if err := t.Execute(mailText, context); err != nil { if err := t.Execute(mailText, context); err != nil {
log.Errorf("Failed to execute template %s with context %+v: %v", templateName, context, err) return nil, fmt.Errorf(
return nil, err "failed to execute template %s with context %+v: %w",
templateName, context, err,
)
} }
return return
@ -122,6 +136,7 @@ func (n *decisionReplyBase) getHeaders() headerList {
headers = append(headers, headerData{ headers = append(headers, headerData{
name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)}, name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
}) })
return headers return headers
} }
@ -139,11 +154,12 @@ type notificationClosedDecision struct {
func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail { func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail {
notification := &notificationClosedDecision{voteSums: *voteSums, reasoning: reasoning} notification := &notificationClosedDecision{voteSums: *voteSums, reasoning: reasoning}
notification.decision = *decision notification.decision = *decision
return notification return notification
} }
func (n *notificationClosedDecision) GetNotificationContent() *notificationContent { func (n *notificationClosedDecision) GetNotificationContent() *NotificationContent {
return &notificationContent{ return &NotificationContent{
template: "closed_motion_mail.txt", template: "closed_motion_mail.txt",
data: struct { data: struct {
*Decision *Decision
@ -162,10 +178,11 @@ type NotificationCreateMotion struct {
voter Voter voter Voter
} }
func (n *NotificationCreateMotion) GetNotificationContent() *notificationContent { func (n *NotificationCreateMotion) GetNotificationContent() *NotificationContent {
voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag) voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL) unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
return &notificationContent{
return &NotificationContent{
template: "create_motion_mail.txt", template: "create_motion_mail.txt",
data: struct { data: struct {
*Decision *Decision
@ -188,13 +205,15 @@ type notificationUpdateMotion struct {
func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail { func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail {
notification := notificationUpdateMotion{voter: voter} notification := notificationUpdateMotion{voter: voter}
notification.decision = decision notification.decision = decision
return &notification return &notification
} }
func (n *notificationUpdateMotion) GetNotificationContent() *notificationContent { func (n *notificationUpdateMotion) GetNotificationContent() *NotificationContent {
voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag) voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL) unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
return &notificationContent{
return &NotificationContent{
template: "update_motion_mail.txt", template: "update_motion_mail.txt",
data: struct { data: struct {
*Decision *Decision
@ -217,11 +236,12 @@ type notificationWithDrawMotion struct {
func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail { func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail {
notification := &notificationWithDrawMotion{voter: *voter} notification := &notificationWithDrawMotion{voter: *voter}
notification.decision = *decision notification.decision = *decision
return notification return notification
} }
func (n *notificationWithDrawMotion) GetNotificationContent() *notificationContent { func (n *notificationWithDrawMotion) GetNotificationContent() *NotificationContent {
return &notificationContent{ return &NotificationContent{
template: "withdraw_motion_mail.txt", template: "withdraw_motion_mail.txt",
data: struct { data: struct {
*Decision *Decision
@ -238,8 +258,8 @@ type RemindVoterNotification struct {
decisions []Decision decisions []Decision
} }
func (n *RemindVoterNotification) GetNotificationContent() *notificationContent { func (n *RemindVoterNotification) GetNotificationContent() *NotificationContent {
return &notificationContent{ return &NotificationContent{
template: "remind_voter_mail.txt", template: "remind_voter_mail.txt",
data: struct { data: struct {
Decisions []Decision Decisions []Decision
@ -266,14 +286,21 @@ type notificationProxyVote struct {
justification string justification string
} }
func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) NotificationMail { func NewNotificationProxyVote(
decision *Decision,
proxy *Voter,
voter *Voter,
vote *Vote,
justification string,
) NotificationMail {
notification := &notificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification} notification := &notificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
notification.decision = *decision notification.decision = *decision
return notification return notification
} }
func (n *notificationProxyVote) GetNotificationContent() *notificationContent { func (n *notificationProxyVote) GetNotificationContent() *NotificationContent {
return &notificationContent{ return &NotificationContent{
template: "proxy_vote_mail.txt", template: "proxy_vote_mail.txt",
data: struct { data: struct {
Proxy string Proxy string
@ -298,11 +325,12 @@ type notificationDirectVote struct {
func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail { func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail {
notification := &notificationDirectVote{voter: *voter, vote: *vote} notification := &notificationDirectVote{voter: *voter, vote: *vote}
notification.decision = *decision notification.decision = *decision
return notification return notification
} }
func (n *notificationDirectVote) GetNotificationContent() *notificationContent { func (n *notificationDirectVote) GetNotificationContent() *NotificationContent {
return &notificationContent{ return &NotificationContent{
template: "direct_vote_mail.txt", template: "direct_vote_mail.txt",
data: struct { data: struct {
Vote VoteChoice Vote VoteChoice

Loading…
Cancel
Save