// ============================================================================= // Auth: alex // File: client.go // Revn: 09-15-2026 3.0.3 // Func: ask server for a message of the day quote // // TODO: remove ioutil import // expand on /ba/ failures // add more flags? // log errors in logfile // ============================================================================= // CHANGE LOG // ----------------------------------------------------------------------------- // 05-11-2022: init // 05-17-2022: changed an entry in the response table // 08-16-2023: copied from speak.go // gutted, rewrote // 09-12-2023: added size conversation // added -d, -l, and -dest flags // commented // 09-21-2023: began complete-ass overhaul of handle() system // wrote draft of draw() // 09-26-2023: made draw return size and reference globals // imported check() from soary // 09-29-2023: commented handle() //*10-02-2023: commented main(), and friends //#10-05-2023: /remove/ works // removed -d flag // commented // 06-12-2024: began added PWFT to /la/ // 06-13-2024: finished adding PWFT to /la/ // commented PWFT stuff // STARTED BYTE-BASED PWFT REWRITE //#06-14-2024: finished byte-based PWFT rewrite // commented //*01-09-2025: basically just copy/pasted from /la/ into /r1/, /r2/ // 06-21-2026: copied v and vv flags from server // copied /edit/ switch case from /remove/ in main() // pulled /sa/ and /qa/ into separate functions // 09-03-2026: renamed in() -> inputNumber() // copied in() and removed all logic // added firstEdit() and secondEdit() and /e1/ and /e2/ // 09-04-2026: rewrote in() with bufio instead of scanln //#09-06-2026: fixed blank message error in secondEdit() // comments // 09-07-2026: added fatih/color import // rewrote quoteAnswer() to handle coloring escape // sequences //*09-12-2026: commented quoteAnswer() //*09-15-2026: added -c flag and color code example printing // changed edit command to be add and then remove instead // of remove and then add, fixing server OOB error when // editting only quote in list // // ============================================================================= package main import ( "bufio" // NewReader, ReadString "io" // io.EOF "io/ioutil" // ReadFile "flag" // BoolVar, StringVar, Parse "fmt" // Println "math/rand" // NewSource, New, Intn, *rand.Rand "net" // ResolveTCPAddr, DialTCP, conn.Write,Read "os" // Exit, Create, Stdin "strconv" // Atoi, Itoa "strings" // Split "time" // Now, UnixNano "github.com/fatih/color" // color.New, Printf, Println ) // print errors if they occur, quit func check( err error ) { var errep string // declare error report string switch err { // switch case over error input case nil: // if there was no error // then there was no error errep = "err is nil" case io.EOF: // if there was an End Of File error errep = "Error: " + err.Error() // document error conn.Close() // close connection default: // anything besides those are a big deal errep = "Fatal: " + err.Error() } var critical bool // declare critical error flag // must be neither nil nor EOF critical = err != nil && err != io.EOF if critical { // XXX critical print // TODO at some point, critical should be logging instead of // printing, only verbose should print fmt.Println( "check() -> ", errep ) } if critical { // if there was a critical error os.Exit( 1 ) // cut and run } } // get user input func in( prompt string ) string { // instantiate reader reader := bufio.NewReader( os.Stdin ) fmt.Print( prompt ) // print user prompt // get user input, read until newline input, _ := reader.ReadString( '\n' ) // strip trailing newline before returning return input[:len( input ) - 1] } // get user input, must be a number func inputNumber( prompt string ) string { // get user input //input := in( prompt ) fmt.Print( prompt ) var input string fmt.Scanln( &input ) // try to convert to an int, discard results _, err := strconv.Atoi( input ) // if there was an error, user didn't input an int if err != nil { // warn user against this fmt.Println( "Input must be a number" ) // try to get user input again //XXX input = in( prompt ) fmt.Println( "err: ", err.Error() ) // this, like handle() calling handle(), seems like a super // bad idea, stack-wise... } // once user has properly inputted a number, return string of num return input } // seed and get a new rng func seed() *rand.Rand { // use current time to seed random number s := rand.NewSource( time.Now().UnixNano() ) r := rand.New( s ) // create new random number from seed return r // return random number } // get a random number func draw( qsize int ) string { rn := seed() // get a random seed r := rn.Intn( qsize ) // get a random number using seed // convert random number to ascii, because converting // ints to bytes ( for networking ) and back is obnoxious rsz := strconv.Itoa( r ) return rsz // return rand size } // respond to size answer func sizeAnswer( subcom []string ) string { // subcom should contain only a size // convert to int, catch error size, err := strconv.Atoi( subcom[0] ) if err != nil { // if int conversion failed ( for some reason ) os.Exit( 1 ) // just quit // TODO make exit codes unique, document them } // draw a random number, add to quote request return "qr@" + draw( size ) } // receive quote, print func quoteAnswer( subcom []string ) string { // split subcommand over spaces words := strings.Split( subcom[0], " " ) // default text is white c := color.New( color.FgWhite ) for _, w := range words { // if first char is normal color escape character if string( w[0] ) == "$" { // see which color is requested switch string( w[1] ) { case "1": // 1 -> red c = color.New( color.FgRed ) case "2": // 2 -> yellow c = color.New( color.FgYellow ) case "3": // 3 -> green c = color.New( color.FgGreen ) case "4": // 4 -> blue c = color.New( color.FgBlue ) case "5": // 5 -> cyan c = color.New( color.FgCyan ) case "6": // 6 -> magenta c = color.New( color.FgMagenta ) case "7": // 7 -> black c = color.New( color.FgBlack ) case "0": // 0 -> white again c = color.New( color.FgWhite ) default: // none of these, continue with the last color } // color chosen, print string with trailing space, minus // first two characters c.Printf( "%s ", w[2:] ) // if first char is bold color escape character } else if string( w[0] ) == "#" { // see which color is requested switch string( w[1] ) { case "1": // 1 -> red c = color.New( color.FgRed, color.Bold ) case "2": // 2 -> yellow c = color.New( color.FgYellow, color.Bold ) case "3": // 3 -> green c = color.New( color.FgGreen, color.Bold ) case "4": // 4 -> blue c = color.New( color.FgBlue, color.Bold ) case "5": // 5 -> cyan c = color.New( color.FgCyan, color.Bold ) case "6": // 6 -> magenta c = color.New( color.FgMagenta, color.Bold ) case "7": // 7 -> black c = color.New( color.FgBlack, color.Bold ) case "0": // 0 -> white again c = color.New( color.FgWhite, color.Bold ) default: // none of these, continue with the last color } // color chosen, print string with trailing space, minus // first two characters c.Printf( "%s ", w[2:] ) } else { // TODO add if for if w[0] is "\" // using the currently selected color, print the next work // and a trailing space c.Printf( "%s ", w ) } } c.Println() // print final newline //fmt.Println( subcom[0] ) // whole-ass just print quote return "te@success" // set transaction end message } // add answer func addAnswer( subcom []string ) string { var resp string = "te@" // if server says add succeeded if subcom[0] == "success" { fmt.Println( "Add successful" ) // print success resp = resp + "success" // set transaction end message // if server says add failed } else if subcom[0] == "error" { fmt.Println( "Add failed" ) // print fail resp = resp + "error" // set transaction end message // unknown status message } else { // print unknown status fmt.Println( "Unknown status code: " + subcom[0] ) // TODO should this be unknown instead of error? resp = resp + "error" // set transaction end message } return resp } // receive answer to list func listAnswer( subcom []string ) string { var resp string if len( subcom ) > 1 { // multistage send condition if subcom[0] == "X" { // final message // TODO update // read entire temp storage file as byte array tfile, err := ioutil.ReadFile( "temp.q" ) check( err ) // check read for errors // cast file to string, concat with recv'd string tstr := string( tfile ) + subcom[1] // split concat over newlines, separating quotes tlist := strings.Split( tstr, "\n" ) // iterate over array, printing each quote for p, v := range tlist { fmt.Println( p + 1, "\t", v ) } // delete temp storage file err = os.Remove( "temp.q" ) check( err ) // check rm for errors resp = "te@X" // successful response message } else { // first or intermediate message // open temp quote storage file, creating if // necessary, with write-only permissions, in // append mode, wr u perm, r g/w perm f, err := os.OpenFile( "temp.q", os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0644 ) check( err ) // check file open for errors // write received message to file, keeping track // of bytes written n, err := f.WriteString( subcom[1] ) check( err ) // check file write for errors // cast bytes already written field to int b, err := strconv.Atoi( subcom[0] ) check( err ) // check cast for errors // add bytes just written to bytes already written, cast // back to string, and append to response variable resp = "lr@" + strconv.Itoa( b + n ) } } else { // split recv'd message over \n, create quote list quotes := strings.Split( subcom[0], "\n" ) for p, v := range quotes { // iterate over quotes // print num of quote, followed by that quote fmt.Println( p + 1, "\t", v ) } resp = "te@success" // set transaction end message } return resp } // first round edit request func firstEdit( subcom []string ) string { var resp string if len( subcom ) > 1 { // multistage send condition if subcom[0] == "X" { // final message // TODO update // read entire temp storage file as byte array tfile, err := ioutil.ReadFile( "temp.q" ) check( err ) // check read for errors // cast file to string, concat with recv'd string tstr := string( tfile ) + subcom[1] // split concat over newlines, separating quotes tlist := strings.Split( tstr, "\n" ) // iterate over array, printing each quote for p, v := range tlist { fmt.Println( p + 1, "\t", v ) } // delete temp storage file err = os.Remove( "temp.q" ) check( err ) // check rm for errors // ask user which quote to remove resp = "e2@" + inputNumber( "\n-> " ) } else { // first or intermediate message // open temp quote storage file, creating if // necessary, with write-only permissions, in // append mode, wr u perm, r g/w perm f, err := os.OpenFile( "temp.q", os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0644 ) check( err ) // check file open for errors // write received message to file, keeping track // of bytes written n, err := f.WriteString( subcom[1] ) check( err ) // check file write for errors // cast bytes already written field to int b, err := strconv.Atoi( subcom[0] ) check( err ) // check cast for errors // add bytes just written to bytes already written, cast // back to string, and append to response variable resp = "e1@" + strconv.Itoa( b + n ) } } else { // split recv'd message over \n, create quote list quotes := strings.Split( subcom[0], "\n" ) for p, v := range quotes { // iterate over quotes // print num of quote, followed by that quote fmt.Println( p + 1, "\t", v ) } // ask user which quote to edit resp = "e2@" + inputNumber( "\n-> " ) } return resp } // second round edit request ( and also third, technically ) func secondEdit( subcom []string ) string { // success message and edit request pt2 come to the same place if subcom[0] == "success" { // print success and end transaction fmt.Println( "Edit successful" ) return "te@success" } // edit command is an add and then a removal var resp string resp = "cr@ar@" fmt.Println( "as is: " + subcom[1] ) fmt.Print( "preview: " ) // show raw of current quote _ = quoteAnswer( subcom[1:] ) // display quote properly // server response is user input for new quote resp = resp + in( "\nupdate -> " ) // and position of old quote to be removed resp = resp + "@r2@" + subcom[0] return resp } // list of all quotes, pick which to remove func firstRemove( subcom []string ) string { // the idea is that if the user doesn't specify which quote to // remove, list all quotes and let the user choose the quote // to remove. There was probably a way to handle this a little // more simply, but this, I feel, has some nuance to it that I // actually like var resp string if len( subcom ) > 1 { // multistage send condition if subcom[0] == "X" { // final message // TODO update // read entire temp storage file as byte array tfile, err := ioutil.ReadFile( "temp.q" ) check( err ) // check read for errors // cast file to string, concat with recv'd string tstr := string( tfile ) + subcom[1] // split concat over newlines, separating quotes tlist := strings.Split( tstr, "\n" ) // iterate over array, printing each quote for p, v := range tlist { fmt.Println( p + 1, "\t", v ) } // delete temp storage file err = os.Remove( "temp.q" ) check( err ) // check rm for errors // ask user which quote to remove resp = "r2@" + inputNumber( "\n-> " ) } else { // first or intermediate message // open temp quote storage file, creating if // necessary, with write-only permissions, in // append mode, wr u perm, r g/w perm f, err := os.OpenFile( "temp.q", os.O_WRONLY | os.O_APPEND | os.O_CREATE, 0644 ) check( err ) // check file open for errors // write received message to file, keeping track // of bytes written n, err := f.WriteString( subcom[1] ) check( err ) // check file write for errors // cast bytes already written field to int b, err := strconv.Atoi( subcom[0] ) check( err ) // check cast for errors // add bytes just written to bytes already written, cast // back to string, and append to response variable resp = "r1@" + strconv.Itoa( b + n ) } } else { // split recv'd message over \n, create quote list quotes := strings.Split( subcom[0], "\n" ) for p, v := range quotes { // iterate over quotes // print num of quote, followed by that quote fmt.Println( p + 1, "\t", v ) } // ask user which quote to remove resp = "r2@" + inputNumber( "\n-> " ) } return resp } // second remove, determine if success or failure func secondRemove( subcom []string ) string { var resp string // if server says remove succeeded if subcom[0] == "success" { fmt.Println( "Remove successful\n" ) // success // list all quotes to validate remove succeeded // TODO should /aa/ also make /lr/ before /te/? resp = "lr@init" // set transaction end message } else if subcom[0] == "error" { // if server says remove failed fmt.Println( "Remove failed" ) // print failure resp = "te@error" // set transaction end message } else { // if unknown status message // TODO there's a known issue where if the buffer // size ( -s ) is less than 10, on the server side, // 'success' will get cut off ( r2@success ) // and /r2/ will end up down here despite having // actually worked correctly, and will not display the // list, as it should on success // print unknown status fmt.Println( "Unknown error code: " + subcom[0] ) // TODO should this be unknown instead of error? resp = "te@error" // set transaction end message } return resp } // get server response and print func handle( conn net.Conn ) { // create buffer to hold response buffer := make( []byte, tsize ) //var buffer [32]byte // create buffer to hold response // read n bytes from server into buffer ( byte slice ) n, err := conn.Read( buffer[:] ) check( err ) // check for errors // split recv'd message over @, separate header from message message := strings.Split( string( buffer[:n] ), "@" ) command := message[0] var resp string // declare response variable switch command { // switch/case over the header // if message is... case "sa": // /sa/, size answer resp = sizeAnswer( message[1:] ) case "qa": // /qa/, quote answer resp = quoteAnswer( message[1:] ) case "aa": // /aa/, add answer resp = addAnswer( message[1:] ) case "la": // /la/, list answer resp = listAnswer( message[1:] ) case "e1": // /e1/, edit quote ( first transaction ) resp = firstEdit( message[1:] ) case "e2": // /e2/, edit quote ( second transaction ) resp = secondEdit( message[1:] ) case "r1": // /r1/, remove ( first transaction ) resp = firstRemove( message[1:] ) case "r2": // /r2/, remove ( second transaction ) resp = secondRemove( message[1:] ) case "br": // /br/, bad request // client has asked for something nonsensical // TODO write out to file fmt.Println( "Client sent a bad request" ) resp = "te@error" // set transaction end message case "ba": // /ba/, bad answer // TODO create server-side, and handle client-side fmt.Println( "error in " + message[1] + ": " + message[2] ) resp = "te@error" // set transaction end message default: // something undefined entirely // TODO write out to file //fmt.Println( "Message header not recognized" ) //fmt.Println( msg ) conn.Close() // TODO how important is this line return // dead return, essentially lets program quit } // send formulated response back to server _, err = conn.Write( []byte( resp ) ) check( err ) // make sure write worked handle( conn ) // call handle again } // globals //var qsize int // keep track of num of quotes var conn net.Conn // keep track of connection // global flag variables var add bool // add new quotes to quote file ( on server ) var remove bool // remove quotes from quote file ( on server ) var list bool // request entire quote file from server var edit bool // edit a pre-existing quote from quote file ( on server ) var co bool // list color codes var dest string // provide a new destination ip and port var tsize int // size of transmission buffer var VERS string // version func main() { var v bool // version flag var vv bool // big version flag // define flags flag.BoolVar( &add, "a", false, "add quote to list" ) flag.BoolVar( &remove, "r", false, "remove quote from list" ) flag.BoolVar( &list, "l", false, "print quote list" ) flag.BoolVar( &edit, "e", false, "edit quote from list" ) flag.BoolVar( &co, "c", false, "list color codes with examples" ) flag.StringVar( &dest, "ip", "127.0.0.1:1300", "ip:port of server" ) flag.IntVar( &tsize, "s", 256, "transmission size ( bytes )" ) flag.BoolVar( &v, "v", false, "display version and exit" ) flag.BoolVar( &vv, "vv", false, "display version and about info and exit" ) flag.Parse() // FIXME get better versioning method VERS = "3.0.3" if vv { // display info and exit fmt.Println( "ekho client v" + VERS ) fmt.Println( "ekho is named after Echo the Oread" ) fmt.Println( "in Greek mythology, Echo was cursed by Hera to only be able to say the last words spoken to her" ) fmt.Println( "i found this to be quite fitting for a MOTD server" ) return } else if v { // display info and exit fmt.Println( VERS ) return } if co { // display color codes and examples and quit // print color code and color example on same line // color.XYZ print inserts newline fmt.Print( "$1: " ) color.Red( "red" ) fmt.Print( "$2: " ) color.Yellow( "yellow" ) fmt.Print( "$3: " ) color.Green( "green" ) fmt.Print( "$4: " ) color.Blue( "blue" ) fmt.Print( "$5: " ) color.Cyan( "cyan" ) fmt.Print( "$6: " ) color.Magenta( "magenta" ) fmt.Print( "$7: " ) color.Black( "black" ) fmt.Print( "$0: " ) color.White( "white (reset)" ) // print bold codes fmt.Print( "\n#1: " ) // create bold color object, change out actual color and print bold := color.New( color.Bold, color.FgRed ) bold.Println( "bold red" ) fmt.Print( "#2: " ) bold.Add( color.FgYellow ) bold.Println( "bold yellow" ) fmt.Print( "#3: " ) bold.Add( color.FgGreen ) bold.Println( "bold green" ) fmt.Print( "#4: " ) bold.Add( color.FgBlue ) bold.Println( "bold blue" ) fmt.Print( "#5: " ) bold.Add( color.FgCyan ) bold.Println( "bold Cyan" ) fmt.Print( "#6: " ) bold.Add( color.FgMagenta ) bold.Println( "bold magenta" ) fmt.Print( "#7: " ) bold.Add( color.FgBlack ) bold.Println( "bold black" ) fmt.Print( "#0: " ) bold.Add( color.FgWhite ) bold.Println( "bold white" ) return // bail } service := dest // declare server ip:port // create address object addr, err := net.ResolveTCPAddr( "tcp", service ) check( err ) // check for errors // make connection with object conn, err = net.DialTCP( "tcp", nil, addr ) check( err ) // check for errors // block for all flags var command string switch { // empty switch statement is basically an if case list: // if list flag is active command = "lr@init" // list request // TODO come up with a useful argument, instead of dumvar case add: // if add flag is active // if arguments were specified if len( flag.Args() ) != 0 { command = "ar@" // preface command w/ add request // need to concat flag.args for _, v := range flag.Args() { // add the string and a space to command command = command + v + " " } // remove the trailing space command = command[:len( command ) - 1] } else { // if no arguments were specified // print that you need args and bail fmt.Println( "/a/ flag must be used with an arugment" ) os.Exit( 1 ) } case edit: // if edit flag is active // if arguments were specified if len( flag.Args() ) > 0 { // argument is supposed to the number of the quote to // be edited, get first arg // cast to int just to make sure it's a valid number // assuming the user knows what they're doing _, rerr := strconv.Atoi( flag.Args()[0] ) // if there was an error, just pretend there's no args if rerr != nil { command = "e1@init" // remove request 1 } else { // if user knows the number, take care of it // arg[0] is a string, so concat is cool // jump right into edit request 2 command = "e2@" + flag.Args()[0] } } else { // if no argument was specified for edit // jump right into edit request 1 command = "e1@init" } case remove: // if remove flag is active // if arguments were specified if len( flag.Args() ) > 0 { // argument is supposed to the number of the quote to // be removed, get first arg // cast to int just to make sure it's a valid number // assuming the user knows what they're doing _, rerr := strconv.Atoi( flag.Args()[0] ) // if there was an error, just pretend there's no args if rerr != nil { command = "r1@init" // remove request 1 } else { // if user knows the number, take care of it // arg[0] is a string, so concat is cool // jump right into remove request 2 command = "r2@" + flag.Args()[0] } } else { // if no argument was specified for remove // jump right into remove request 1 command = "r1@init" } default: // if no flag was specified // begin transaction for quote request command = "sr@please" // size request } // write command to server, beginning transaction _, err = conn.Write( []byte( command ) ) check( err ) // make sure write worked handle( conn ) // handle response // TODO stack trace when handle returns, should probably be // updated conn.Close() // close connection when finished os.Exit( 0 ) // exit }