// promptForForm runs an XEP-0004 form and collects responses from the user. func promptForForm(term *terminal.Terminal, user, password, title, instructions string, fields []interface{}) error { info(term, "The server has requested the following information. Text that has come from the server will be shown in red.") // formStringForPrinting takes a string form the form and returns an // escaped version with codes to make it show as red. formStringForPrinting := func(s string) string { var line []byte line = append(line, term.Escape.Red...) line = appendTerminalEscaped(line, []byte(s)) line = append(line, term.Escape.Reset...) return string(line) } write := func(s string) { term.Write([]byte(s)) } var tmpDir string showMediaEntries := func(questionNumber int, medias [][]xmpp.Media) { if len(medias) == 0 { return } write("The following media blobs have been provided by the server with this question:\n") for i, media := range medias { for j, rep := range media { if j == 0 { write(fmt.Sprintf(" %d. ", i+1)) } else { write(" ") } write(fmt.Sprintf("Data of type %s", formStringForPrinting(rep.MIMEType))) if len(rep.URI) > 0 { write(fmt.Sprintf(" at %s\n", formStringForPrinting(rep.URI))) continue } var fileExt string switch rep.MIMEType { case "image/png": fileExt = "png" case "image/jpeg": fileExt = "jpeg" } if len(tmpDir) == 0 { var err error if tmpDir, err = ioutil.TempDir("", "xmppclient"); err != nil { write(", but failed to create temporary directory in which to save it: " + err.Error() + "\n") continue } } filename := filepath.Join(tmpDir, fmt.Sprintf("%d-%d-%d", questionNumber, i, j)) if len(fileExt) > 0 { filename = filename + "." + fileExt } out, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { write(", but failed to create file in which to save it: " + err.Error() + "\n") continue } out.Write(rep.Data) out.Close() write(", saved in " + filename + "\n") } } write("\n") } var err error if len(title) > 0 { write(fmt.Sprintf("Title: %s\n", formStringForPrinting(title))) } if len(instructions) > 0 { write(fmt.Sprintf("Instructions: %s\n", formStringForPrinting(instructions))) } questionNumber := 0 for _, field := range fields { questionNumber++ write("\n") switch field := field.(type) { case *xmpp.FixedFormField: write(formStringForPrinting(field.Text)) write("\n") questionNumber-- case *xmpp.BooleanFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) term.SetPrompt("Please enter yes, y, no or n: ") TryAgain: for { answer, err := term.ReadLine() if err != nil { return err } switch answer { case "yes", "y": field.Result = true case "no", "n": field.Result = false default: continue TryAgain } break } case *xmpp.TextFormField: switch field.Label { case "CAPTCHA web page": if strings.HasPrefix(field.Default, "http") { // This is a oddity of jabber.ccc.de and maybe // others. The URL for the capture is provided // as the default answer to a question. Perhaps // that was needed with some clients. However, // we support embedded media and it's confusing // to ask the question, so we just print the // URL. write(fmt.Sprintf("CAPTCHA web page (only if not provided below): %s\n", formStringForPrinting(field.Default))) questionNumber-- continue } case "User": field.Result = user questionNumber-- continue case "Password": field.Result = password questionNumber-- continue } write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) if len(field.Default) > 0 { write(fmt.Sprintf("Please enter response or leave blank for the default, which is '%s'\n", formStringForPrinting(field.Default))) } else { write("Please enter response") } term.SetPrompt("> ") if field.Private { field.Result, err = term.ReadPassword("> ") } else { field.Result, err = term.ReadLine() } if err != nil { return err } if len(field.Result) == 0 { field.Result = field.Default } case *xmpp.MultiTextFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) write("Please enter one or more responses, terminated by an empty line\n") term.SetPrompt("> ") for { line, err := term.ReadLine() if err != nil { return err } if len(line) == 0 { break } field.Results = append(field.Results, line) } case *xmpp.SelectionFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) for i, opt := range field.Values { write(fmt.Sprintf(" %d. %s\n\n", i+1, formStringForPrinting(opt))) } term.SetPrompt("Please enter the number of your selection: ") TryAgain2: for { answer, err := term.ReadLine() if err != nil { return err } answerNum, err := strconv.Atoi(answer) answerNum-- if err != nil || answerNum < 0 || answerNum >= len(field.Values) { write("Cannot parse that reply. Try again.") continue TryAgain2 } field.Result = answerNum break } case *xmpp.MultiSelectionFormField: write(fmt.Sprintf("%d. %s\n\n", questionNumber, formStringForPrinting(field.Label))) showMediaEntries(questionNumber, field.Media) for i, opt := range field.Values { write(fmt.Sprintf(" %d. %s\n\n", i+1, formStringForPrinting(opt))) } term.SetPrompt("Please enter the numbers of zero or more of the above, separated by spaces: ") TryAgain3: for { answer, err := term.ReadLine() if err != nil { return err } var candidateResults []int answers := strings.Fields(answer) for _, answerStr := range answers { answerNum, err := strconv.Atoi(answerStr) answerNum-- if err != nil || answerNum < 0 || answerNum >= len(field.Values) { write("Cannot parse that reply. Please try again.") continue TryAgain3 } for _, other := range candidateResults { if answerNum == other { write("Cannot have duplicates. Please try again.") continue TryAgain3 } } candidateResults = append(candidateResults, answerNum) } field.Results = candidateResults break } } } if len(tmpDir) > 0 { os.RemoveAll(tmpDir) } return nil }
func main() { var ( filename string site string username string password string line string buf []byte db *keyrack.Database oldState *terminal.State term *terminal.Terminal termio TermIO group *keyrack.Group //trail []*keyrack.Group groupView GroupView matched bool quit bool err error ) if len(os.Args) != 2 { fmt.Printf("Syntax: %s <filename>\n", os.Args[0]) os.Exit(1) } filename = os.Args[1] // setup terminal oldState, err = terminal.MakeRaw(0) if err != nil { panic(err) } defer terminal.Restore(0, oldState) termio.Input = os.Stdin termio.Output = os.Stdout term = terminal.NewTerminal(termio, "> ") if _, err = os.Stat(filename); os.IsNotExist(err) { db, err = keyrack.NewDatabase() } else { password, err = term.ReadPassword("Password: "******"" } group = db.Top() for err == nil && !quit { groupView.Group = group buf, err = groupView.Render() if err != nil { break } _, err = term.Write(buf) if err != nil { break } line, err = term.ReadLine() if err != nil { break } switch line { case "q": password, err = term.ReadPassword("Password: "******"" quit = true case "ng", "new group": term.SetPrompt("Group name: ") line, err = term.ReadLine() if err != nil { break } if line == "" { term.Write([]byte("Group creation cancelled.\n")) } else { err = group.AddGroup(line) if err != nil { if err == keyrack.ErrGroupExists { term.Write([]byte("Group already exists.\n")) } else { break } } } term.SetPrompt("> ") case "nl", "new login": term.SetPrompt("Site name: ") site, err = term.ReadLine() if err != nil { break } if site == "" { term.Write([]byte("Login creation cancelled.\n")) } else { term.SetPrompt("Username: "******"" { term.Write([]byte("Login creation cancelled.\n")) } else { password, err = term.ReadPassword("Password: "******"" { term.Write([]byte("Login creation cancelled.\n")) } else { err = group.AddLogin(site, username, password) password = "" if err != nil { if err == keyrack.ErrLoginExists { term.Write([]byte("Login already exists.\n")) } else { break } } } } } term.SetPrompt("> ") default: matched, err = regexp.MatchString("^G(\\d+)$", line) if err != nil { break } if matched { term.Write([]byte("Group!\n")) continue } matched, err = regexp.MatchString("^L(\\d+)$", line) if err != nil { break } if matched { term.Write([]byte("Group!\n")) } } } if err != nil { fmt.Println(err) } if quit { fmt.Println("Quitting...") } }