diff --git a/cmd/ooni/main.go b/cmd/ooni/main.go index e48b9b3..df0473e 100644 --- a/cmd/ooni/main.go +++ b/cmd/ooni/main.go @@ -8,6 +8,7 @@ import ( _ "github.com/ooni/probe-cli/internal/cli/info" _ "github.com/ooni/probe-cli/internal/cli/list" _ "github.com/ooni/probe-cli/internal/cli/nettest" + _ "github.com/ooni/probe-cli/internal/cli/onboard" _ "github.com/ooni/probe-cli/internal/cli/run" _ "github.com/ooni/probe-cli/internal/cli/show" _ "github.com/ooni/probe-cli/internal/cli/upload" diff --git a/internal/cli/onboard/onboard.go b/internal/cli/onboard/onboard.go new file mode 100644 index 0000000..b130a89 --- /dev/null +++ b/internal/cli/onboard/onboard.go @@ -0,0 +1,134 @@ +package onboard + +import ( + "fmt" + + "github.com/alecthomas/kingpin" + "github.com/apex/log" + "github.com/ooni/probe-cli/internal/cli/root" + "github.com/ooni/probe-cli/internal/colors" + "github.com/ooni/probe-cli/internal/output" + "gopkg.in/AlecAivazis/survey.v1" +) + +func init() { + cmd := root.Command("onboard", "Starts the onboarding process") + + cmd.Action(func(_ *kingpin.ParseContext) error { + output.SectionTitle("What is OONI Probe?") + + fmt.Println() + output.Paragraph("Your tool for detecting internet censorship!") + fmt.Println() + output.Paragraph("OONI Probe checks whether your provider blocks access to sites and services. Run OONI Probe to collect evidence of internet censorship and to measure your network performance.") + fmt.Println() + output.PressEnterToContinue("Press 'Enter' to continue...") + + output.SectionTitle("Heads Up") + fmt.Println() + output.Bullet("Anyone monitoring your internet activity (such as your government or ISP) may be able to see that you are running OONI Probe.") + fmt.Println() + output.Bullet("The network data you will collect will automatically be published (unless you opt-out in the settings).") + fmt.Println() + output.Bullet("You may test objectionable sites.") + fmt.Println() + output.Bullet("Read the documentation to learn more.") + fmt.Println() + output.PressEnterToContinue("Press 'Enter' to continue...") + + output.SectionTitle("Pop Quiz!") + output.Paragraph("") + answer := "" + quiz1 := &survey.Select{ + Message: "Anyone monitoring my internet activity may be able to see that I am running OONI Probe.", + Options: []string{"true", "false"}, + Default: "true", + } + survey.AskOne(quiz1, &answer, nil) + if answer != "true" { + output.Paragraph(colors.Red("Actually...")) + output.Paragraph("OONI Probe is not a privacy tool. Therefore, anyone monitoring your internet activity may be able to see which software you are running.") + } else { + output.Paragraph(colors.Blue("Good job!")) + } + answer = "" + quiz2 := &survey.Select{ + Message: "The network data I will collect will automatically be published (unless I opt-out in the settings).", + Options: []string{"true", "false"}, + Default: "true", + } + survey.AskOne(quiz2, &answer, nil) + if answer != "true" { + output.Paragraph(colors.Red("Actually...")) + output.Paragraph("The network data you will collect will automatically be published to increase transparency of internet censorship (unless you opt-out in the settings).") + } else { + output.Paragraph(colors.Blue("Well done!")) + } + + changeDefaults := false + prompt := &survey.Confirm{ + Message: "Do you want to change the default settings?", + Default: false, + } + survey.AskOne(prompt, &changeDefaults, nil) + + settings := struct { + IncludeIP bool + IncludeNetwork bool + IncludeCountry bool + UploadResults bool + SendCrashReports bool + }{} + settings.IncludeIP = false + settings.IncludeNetwork = true + settings.IncludeCountry = true + settings.UploadResults = true + settings.SendCrashReports = true + + if changeDefaults == true { + var qs = []*survey.Question{ + { + Name: "IncludeIP", + Prompt: &survey.Confirm{Message: "Should we include your IP?"}, + }, + { + Name: "IncludeNetwork", + Prompt: &survey.Confirm{ + Message: "Can we include your network name?", + Default: true, + }, + }, + { + Name: "IncludeCountry", + Prompt: &survey.Confirm{ + Message: "Can we include your country name?", + Default: true, + }, + }, + { + Name: "UploadResults", + Prompt: &survey.Confirm{ + Message: "Can we upload your results?", + Default: true, + }, + }, + { + Name: "SendCrashReports", + Prompt: &survey.Confirm{ + Message: "Can we send crash reports to OONI?", + Default: true, + }, + }, + } + + err := survey.Ask(qs, &settings) + if err != nil { + log.WithError(err).Error("there was an error in parsing your responses") + return err + } + } + + log.Error("this function is not implemented") + return nil + }) +} diff --git a/internal/log/handlers/cli/cli.go b/internal/log/handlers/cli/cli.go index 07ca09b..d6a6b15 100644 --- a/internal/log/handlers/cli/cli.go +++ b/internal/log/handlers/cli/cli.go @@ -13,6 +13,7 @@ import ( "github.com/fatih/color" colorable "github.com/mattn/go-colorable" "github.com/ooni/probe-cli/internal/log/handlers/cli/progress" + "github.com/ooni/probe-cli/internal/util" ) // Default handler outputting to stderr. @@ -68,7 +69,7 @@ func logSectionTitle(w io.Writer, f log.Fields) error { title := f.Get("title").(string) fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth+2)+"┓\n") - fmt.Fprintf(w, "┃ %s ┃\n", RightPad(title, colWidth)) + fmt.Fprintf(w, "┃ %s ┃\n", util.RightPad(title, colWidth)) fmt.Fprintf(w, "┗"+strings.Repeat("━", colWidth+2)+"┛\n") return nil } @@ -85,7 +86,7 @@ func logTable(w io.Writer, f log.Fields) error { continue } line := fmt.Sprintf("%s: %s", color.Sprint(name), f.Get(name)) - lineLength := escapeAwareRuneCountInString(line) + lineLength := util.EscapeAwareRuneCountInString(line) lines = append(lines, line) if colWidth < lineLength { colWidth = lineLength @@ -95,7 +96,7 @@ func logTable(w io.Writer, f log.Fields) error { fmt.Fprintf(w, "┏"+strings.Repeat("━", colWidth+2)+"┓\n") for _, line := range lines { fmt.Fprintf(w, "┃ %s ┃\n", - RightPad(line, colWidth), + util.RightPad(line, colWidth), ) } fmt.Fprintf(w, "┗"+strings.Repeat("━", colWidth+2)+"┛\n") diff --git a/internal/log/handlers/cli/results.go b/internal/log/handlers/cli/results.go index 50ca767..5c11720 100644 --- a/internal/log/handlers/cli/results.go +++ b/internal/log/handlers/cli/results.go @@ -8,6 +8,7 @@ import ( "time" "github.com/apex/log" + "github.com/ooni/probe-cli/internal/util" ) // XXX Copy-pasta from nettest/groups @@ -118,21 +119,21 @@ func logResultItem(w io.Writer, f log.Fields) error { fmt.Fprintf(w, "┢"+strings.Repeat("━", colWidth*2+2)+"┪\n") } - firstRow := RightPad(fmt.Sprintf("#%d - %s", rID, startTime.Format(time.RFC822)), colWidth*2) + firstRow := util.RightPad(fmt.Sprintf("#%d - %s", rID, startTime.Format(time.RFC822)), colWidth*2) fmt.Fprintf(w, "┃ "+firstRow+" ┃\n") fmt.Fprintf(w, "┡"+strings.Repeat("━", colWidth*2+2)+"┩\n") summary := makeSummary(name, f.Get("summary").(string)) fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n", - RightPad(name, colWidth), - RightPad(summary[0], colWidth))) + util.RightPad(name, colWidth), + util.RightPad(summary[0], colWidth))) fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n", - RightPad(networkName, colWidth), - RightPad(summary[1], colWidth))) + util.RightPad(networkName, colWidth), + util.RightPad(summary[1], colWidth))) fmt.Fprintf(w, fmt.Sprintf("│ %s %s│\n", - RightPad(asn, colWidth), - RightPad(summary[2], colWidth))) + util.RightPad(asn, colWidth), + util.RightPad(summary[2], colWidth))) if index == totalCount-1 { fmt.Fprintf(w, "└┬──────────────┬──────────────┬──────────────┬") @@ -156,9 +157,9 @@ func logResultSummary(w io.Writer, f log.Fields) error { } // └┬──────────────┬──────────────┬──────────────┬ fmt.Fprintf(w, " │ %s │ %s │ %s │\n", - RightPad(fmt.Sprintf("%d tests", tests), 12), - RightPad(fmt.Sprintf("%d nets", networks), 12), - RightPad(fmt.Sprintf("%d ⬆ %d ⬇", dataUp, dataDown), 12)) + util.RightPad(fmt.Sprintf("%d tests", tests), 12), + util.RightPad(fmt.Sprintf("%d nets", networks), 12), + util.RightPad(fmt.Sprintf("%d ⬆ %d ⬇", dataUp, dataDown), 12)) fmt.Fprintf(w, " └──────────────┴──────────────┴──────────────┘\n") return nil diff --git a/internal/log/handlers/cli/util.go b/internal/log/handlers/cli/util.go deleted file mode 100644 index 07de510..0000000 --- a/internal/log/handlers/cli/util.go +++ /dev/null @@ -1,22 +0,0 @@ -package cli - -import ( - "regexp" - "strings" - "unicode/utf8" -) - -// Finds the control character sequences (like colors) -var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d") - -func escapeAwareRuneCountInString(s string) int { - n := utf8.RuneCountInString(s) - for _, sm := range ctrlFinder.FindAllString(s, -1) { - n -= utf8.RuneCountInString(sm) - } - return n -} - -func RightPad(str string, length int) string { - return str + strings.Repeat(" ", length-escapeAwareRuneCountInString(str)) -} diff --git a/internal/output/output.go b/internal/output/output.go index 8bb1cd2..2758fd8 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -1,9 +1,13 @@ package output import ( + "bufio" + "fmt" + "os" "time" "github.com/apex/log" + "github.com/ooni/probe-cli/internal/util" ) // Progress logs a progress type event @@ -76,3 +80,18 @@ func SectionTitle(text string) { "title": text, }).Info(text) } + +func Paragraph(text string) { + const width = 80 + fmt.Println(util.WrapString(text, width)) +} + +func Bullet(text string) { + const width = 80 + fmt.Printf("• %s\n", util.WrapString(text, width)) +} + +func PressEnterToContinue(text string) { + fmt.Print(text) + bufio.NewReader(os.Stdin).ReadBytes('\n') +} diff --git a/internal/util/util.go b/internal/util/util.go index 45358bb..5deb009 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,8 +1,13 @@ package util import ( + "bytes" "fmt" "os" + "regexp" + "strings" + "unicode" + "unicode/utf8" "github.com/ooni/probe-cli/internal/colors" ) @@ -17,3 +22,86 @@ func Fatal(err error) { fmt.Fprintf(os.Stderr, "\n %s %s\n\n", colors.Red("Error:"), err) os.Exit(1) } + +// Finds the control character sequences (like colors) +var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d") + +func EscapeAwareRuneCountInString(s string) int { + n := utf8.RuneCountInString(s) + for _, sm := range ctrlFinder.FindAllString(s, -1) { + n -= utf8.RuneCountInString(sm) + } + return n +} + +func RightPad(str string, length int) string { + return str + strings.Repeat(" ", length-EscapeAwareRuneCountInString(str)) +} + +// WrapString wraps the given string within lim width in characters. +// +// Wrapping is currently naive and only happens at white-space. A future +// version of the library will implement smarter wrapping. This means that +// pathological cases can dramatically reach past the limit, such as a very +// long word. +// This is taken from: https://github.com/mitchellh/go-wordwrap/tree/f253961a26562056904822f2a52d4692347db1bd +func WrapString(s string, lim uint) string { + // Initialize a buffer with a slightly larger size to account for breaks + init := make([]byte, 0, len(s)) + buf := bytes.NewBuffer(init) + + var current uint + var wordBuf, spaceBuf bytes.Buffer + + for _, char := range s { + if char == '\n' { + if wordBuf.Len() == 0 { + if current+uint(spaceBuf.Len()) > lim { + current = 0 + } else { + current += uint(spaceBuf.Len()) + spaceBuf.WriteTo(buf) + } + spaceBuf.Reset() + } else { + current += uint(spaceBuf.Len() + wordBuf.Len()) + spaceBuf.WriteTo(buf) + spaceBuf.Reset() + wordBuf.WriteTo(buf) + wordBuf.Reset() + } + buf.WriteRune(char) + current = 0 + } else if unicode.IsSpace(char) { + if spaceBuf.Len() == 0 || wordBuf.Len() > 0 { + current += uint(spaceBuf.Len() + wordBuf.Len()) + spaceBuf.WriteTo(buf) + spaceBuf.Reset() + wordBuf.WriteTo(buf) + wordBuf.Reset() + } + + spaceBuf.WriteRune(char) + } else { + + wordBuf.WriteRune(char) + + if current+uint(spaceBuf.Len()+wordBuf.Len()) > lim && uint(wordBuf.Len()) < lim { + buf.WriteRune('\n') + current = 0 + spaceBuf.Reset() + } + } + } + + if wordBuf.Len() == 0 { + if current+uint(spaceBuf.Len()) <= lim { + spaceBuf.WriteTo(buf) + } + } else { + spaceBuf.WriteTo(buf) + wordBuf.WriteTo(buf) + } + + return buf.String() +}