From 80da12cfcf3fa4deaa7ec29e775ae1a3d8fdbc26 Mon Sep 17 00:00:00 2001 From: raul Date: Fri, 26 Apr 2024 08:48:48 +0200 Subject: [PATCH] Nuclear refactoring I've finally managed to properly rebuild the project, it's not extremely clean, but compared to before, it's infinitely more functional and expandable. --- cmd/client.go | 86 ++++++++------------ cmd/clientFunc.go | 195 ++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 15 ++-- cmd/server.go | 121 +++++----------------------- cmd/serverFunc.go | 150 +++++++++++++++++++++++++++++++++++ cmd/ui.go | 108 ------------------------- go.mod | 2 +- main.go | 2 +- 8 files changed, 404 insertions(+), 275 deletions(-) create mode 100644 cmd/clientFunc.go create mode 100644 cmd/serverFunc.go delete mode 100644 cmd/ui.go diff --git a/cmd/client.go b/cmd/client.go index 05cce97..0a52479 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -1,34 +1,38 @@ /* -Copyright © 2024 Raul +Copyright © 2024 Raul */ package cmd import ( - "bufio" "fmt" - "github.com/jroimartin/gocui" "github.com/spf13/cobra" - "log" - "net" "os" ) // clientCmd represents the client command var clientCmd = &cobra.Command{ Use: "client", - Short: "Connect to a mini-chat server", - Long: `Connect to a mini-chat server. - Example: - ./mini-chat client --ip 192.168.0.50 --port 1337`, + Short: "Client interface for mini-chat", + Long: `Refactored mini-chat client that properly interfaces +with its server. + +Example: + ./mini-chat client --ip 192.168.0.100 --port 1337`, Run: func(cmd *cobra.Command, args []string) { - client(cmd) + + if err := setClientParameters(cmd); err != nil { + cmd.Help() + fmt.Printf("\n-----------------------\n") + fmt.Println(err) + fmt.Printf("-----------------------\n") + os.Exit(0) + } + + Client() }, } -var err error -var conn net.Conn - func init() { rootCmd.AddCommand(clientCmd) @@ -37,58 +41,32 @@ func init() { // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // clientCmd.PersistentFlags().String("foo", "", "A help for foo") - clientCmd.Flags().String("ip", "", "Server to connect to") - clientCmd.Flags().String("port", "1302", "Port to connect to") + clientCmd.PersistentFlags().String("ip", "", "Server IP to connect to") + clientCmd.PersistentFlags().String("port", "1302", "Server port to connect to") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // clientCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") } -func client(cmd *cobra.Command) { - port, _ := cmd.Flags().GetString("port") - ip, _ := cmd.Flags().GetString("ip") - if ip == "" { - fmt.Println("Not enough arguments, run \"-h\" parameter to see all the parameters!") - os.Exit(0) - } - if port == "" { - port = "1302" - } - socket := ip + ":" + port - conn, err = net.Dial("tcp", socket) - cobra.CheckErr(err) - defer conn.Close() - - reply := make([]byte, 1024) - conn.Read(reply) - fmt.Print(string(reply)) - - msg, err := bufio.NewReader(os.Stdin).ReadString('\n') - cobra.CheckErr(err) - conn.Write([]byte(msg)) - - ui(conn) - -} - -func sendmsg(g *gocui.Gui, v *gocui.View) error { - textarea, err := g.View("textarea") +func setClientParameters(cmd *cobra.Command) error { + parameterPort, err := cmd.Flags().GetString("port") if err != nil { - log.Panicln(err) + return err } - message := textarea.Buffer() - msg := string(message) - if msg == "" { - return nil + if parameterPort != "" { + serverPort = parameterPort } - if msg == "" { - return nil + parameterIP, err := cmd.Flags().GetString("ip") + if err != nil { + return err } + if parameterIP == "" { + err := fmt.Errorf("IP cannot be empty") + return err + } + serverIP = parameterIP - fmt.Fprint(conn, msg) - textarea.Clear() - textarea.SetCursor(0, 0) return nil } diff --git a/cmd/clientFunc.go b/cmd/clientFunc.go new file mode 100644 index 0000000..cb98a58 --- /dev/null +++ b/cmd/clientFunc.go @@ -0,0 +1,195 @@ +/* +Copyright © 2024 Raul +*/ + +package cmd + +import ( + "bufio" + "fmt" + "github.com/jroimartin/gocui" + "github.com/nsf/termbox-go" + "log" + "net" + "os" + "strings" + "time" +) + +type Message struct { + Contents string + //Date time.Time + Server net.Conn +} + +func (m Message) toSend() { + m.Server.Write([]byte(m.Contents)) +} + +var ( + serverPort string = "1302" + serverIP string + data Message +) + +func Client() { + conn, err := net.Dial("tcp", serverIP+":"+serverPort) + if err != nil { + log.Fatalf("Error occurred trying to connect to server: %v\n", err) + } + defer conn.Close() + data.Server = conn + + nameRequest, err := receiveMessage(conn) + if err != nil { + log.Fatalf("Error occurred reading from server while requesting name: %v\n", err) + } + fmt.Print(nameRequest) + sendName(conn) + GUI() +} + +//////////////////////////////////////////////////// +// REFACTORING BELOW // +//////////////////////////////////////////////////// + +func listenMessages(g *gocui.Gui) { + time.Sleep(time.Millisecond * 250) + chatbox, err := g.View("chatbox") + if err != nil { + log.Panicln(err) + } + for { + messageFromServer, err := receiveMessage(data.Server) + if err != nil { + // Avoid triggering an error if client quits the client + if err == gocui.ErrQuit { + g.Close() + } else { + g.Close() + log.Fatalf("Error occurred reading from server: %v\n", err) + } + } + + formattedMessage := strings.TrimRight(messageFromServer, "\n") + fmt.Fprintln(chatbox, formattedMessage) + termbox.Interrupt() + } +} + +func receiveMessage(conn net.Conn) (s string, err error) { + serverMessage := make([]byte, 2048) + if _, err := conn.Read(serverMessage); err != nil { + return "", err + } + finalMessage := string(serverMessage) + return finalMessage, nil +} + +func sendName(conn net.Conn) { + message, err := bufio.NewReader(os.Stdin).ReadString('\n') + + if err != nil { + log.Fatalf("Error occurred sending message to server: %v\n", err) + } + + if _, err := conn.Write([]byte(message)); err != nil { + log.Fatalf("Error occurred writing to server: %v\n", err) + } +} + +func GUI() { + g, err := gocui.NewGui(gocui.OutputNormal) + if err != nil { + log.Panicln(err) + } + defer g.Close() + g.SetManagerFunc(layout) + g.Mouse = true + g.Cursor = true + initKeybindings(g) + + go listenMessages(g) + + if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { + log.Panicln(err) + } +} + +func quit(*gocui.Gui, *gocui.View) error { + return gocui.ErrQuit +} + +func sendToServer(g *gocui.Gui, v *gocui.View) error { + textarea, err := g.View("textarea") + if err != nil { + log.Panicln(err) + } + message := textarea.Buffer() + msg := string(message) + if msg == "" { + return nil + } + + if msg == "" { + return nil + } + + data.Contents = msg + data.toSend() + + textarea.Clear() + textarea.SetCursor(0, 0) + return nil +} + +func initKeybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding("button", gocui.MouseLeft, gocui.ModNone, sendToServer); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding("textarea", gocui.KeyEnter, gocui.ModNone, sendToServer); err != nil { + log.Panicln(err) + } + return nil +} + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + if chatbox, err := g.SetView("chatbox", 2, 1, maxX/2+40, maxY-5); err != nil { + if err != gocui.ErrUnknownView { + return err + } + chatbox.Autoscroll = true + chatbox.Title = "Chat Box" + } + + if button, err := g.SetView("button", maxX/2+32, maxY-4, maxX/2+40, maxY-2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + + button.Wrap = true + button.Frame = true + fmt.Fprintln(button, "Send it") + } + + if textarea, err := g.SetView("textarea", 2, maxY-4, maxX/2+28, maxY-2); err != nil { + if err != gocui.ErrUnknownView { + return err + } + if _, err := g.SetCurrentView("textarea"); err != nil { + log.Panicln(err) + } + textarea.Title = "Send message" + textarea.Wrap = true + textarea.Editable = true + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go index b501093..57577e0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,25 +1,20 @@ /* -Copyright © 2024 Raul +Copyright © 2024 Raul */ package cmd import ( - "os" - "github.com/spf13/cobra" + "os" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "mini-chat", - Short: "Minimalistic chat server built using Go", - Long: `mini-chat was originally designed to be used as a standalone server -users could connect to using netcat, but it can be also used with the binary -client + Short: "Application for hosting and joining a simple chat server", -Examples: - ./mini-chat server --port 8080`, + Long: `Application for hosting and joining a simple chat server`, // Uncomment the following line if your bare application // has an action associated with it: // Run: func(cmd *cobra.Command, args []string) { }, @@ -39,7 +34,7 @@ func init() { // Cobra supports persistent flags, which, if defined here, // will be global for your application. - // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.mini-chat.yaml)") + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.chat-tests.yaml)") // Cobra also supports local flags, which will only run // when this action is called directly. diff --git a/cmd/server.go b/cmd/server.go index 823b414..71f43cf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,31 +1,30 @@ /* -Copyright © 2024 Raul +Copyright © 2024 Raul */ package cmd import ( - "bufio" - "fmt" "github.com/spf13/cobra" - "net" - "strings" - "time" + "log" ) // serverCmd represents the server command var serverCmd = &cobra.Command{ Use: "server", - Short: "Main chat server", - Long: `You can connect to this server by running the following command from -a client: - nc $SERVER_IP $PORT + Short: "Tiny chat server", + Long: `Refactored mini-chat server designed to be connected to +using a proper interface this time, not using netcat. -Assuming your IP is 192.168.0.30 and the server port is left on default the -command would be as follows: - nc 192.168.0.30 1302`, +Example: + ./mini-chat server --port 1337`, Run: func(cmd *cobra.Command, args []string) { - server(cmd) + + if err := setServerParameters(cmd); err != nil { + log.Fatalf("Error happened trying to set parameters: %v\n", err) + } + + Server() }, } @@ -37,100 +36,20 @@ func init() { // Cobra supports Persistent Flags which will work for this command // and all subcommands, e.g.: // serverCmd.PersistentFlags().String("foo", "", "A help for foo") + serverCmd.PersistentFlags().String("port", "1302", "port to use for listening") // Cobra supports local flags which will only run when this command // is called directly, e.g.: // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - serverCmd.Flags().String("port", "1302", "Port to listen on") } -var receiverIsStarted bool = false - -type chatter struct { - Username string - IP string -} - -func server(cmd *cobra.Command) { - var lport string - port, _ := cmd.Flags().GetString("port") - if port != "" { - lport = port - } else { - lport = "1302" - } - - ln, err := net.Listen("tcp", ":"+lport) - cobra.CheckErr(err) - fmt.Printf("Listening on port %v...\n", lport) - masterChannel := make(chan string, 20) - - slaveChannel := make(chan string, 20) - go masterReceiver(slaveChannel, masterChannel) - // TODO: get channels properly working - // receivingChannel := make(chan string) - // sendingChannel := make(chan string) - for { - conn, err := ln.Accept() - cobra.CheckErr(err) - numOfClients++ - go handleConn(conn, slaveChannel, masterChannel) - } - -} - -var numOfClients int = 0 - -func masterReceiver(slaveChannel chan<- string, masterChannel <-chan string) { - for { - message := <-masterChannel - for i := 0; i < numOfClients; i++ { - slaveChannel <- message - } - } -} - -func handleConn(conn net.Conn, slaveChannel <-chan string, masterChannel chan<- string) { - defer conn.Close() - - go func() { - for { - message := <-slaveChannel - conn.Write([]byte(message)) - } - }() - - IP := getIP(conn) - t := time.Now() - date := fmt.Sprintf("%d-%02d-%02d | %02d:%02d:%02d", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - final_message_connect := fmt.Sprintf("[%v] Received connection from %v!\n", date, IP) - fmt.Print(final_message_connect) - conn.Write([]byte("What's your name?\nName: ")) - name, err := bufio.NewReader(conn).ReadString('\n') +func setServerParameters(cmd *cobra.Command) error { + parameterPort, err := cmd.Flags().GetString("port") if err != nil { - numOfClients-- - return + return err } - var NewUser = new(chatter) - NewUser.Username = strings.TrimRight(name, "\n") - NewUser.IP = IP - for { - message, err := bufio.NewReader(conn).ReadString('\n') - if err != nil { - fmt.Printf(NewUser.Username + " disconnected!\n") - numOfClients-- - return - } - finalMessage := fmt.Sprint("{" + NewUser.IP + "} " + NewUser.Username + ": " + message) - fmt.Print(finalMessage) - masterChannel <- finalMessage - //conn.Write([]byte(message)) + if parameterPort != "" { + listenPort = parameterPort } -} - -func getIP(conn net.Conn) (IP string) { - if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { - IP = fmt.Sprintf("%v", addr) - } - return IP + return nil } diff --git a/cmd/serverFunc.go b/cmd/serverFunc.go new file mode 100644 index 0000000..05049cd --- /dev/null +++ b/cmd/serverFunc.go @@ -0,0 +1,150 @@ +/* +Copyright © 2024 Raul +*/ + +package cmd + +import ( + "bufio" + "fmt" + "log" + "net" + "strings" +) + +var ( + listenPort string = "1302" +) + +type Creator interface { + CreateUser() +} + +type User struct { + Username string + IP string +} + +func (u User) CreateUser(usr string, ip string) User { + u.Username = usr + u.IP = ip + return u +} + +func Server() { + ln, err := net.Listen("tcp", ":"+listenPort) + if err != nil { + log.Fatalf("Error happened trying to listen on port: %v\n", err) + } + defer ln.Close() + fmt.Printf("Listening on port %v...\n", listenPort) + for { + conn, err := ln.Accept() + if err != nil { + log.Fatalf("Error happened trying to accept connection: %v\n", err) + } + chatChan := make(chan string, 10) + listenerList = append(listenerList, chatChan) + go handleConn(conn, chatChan) + } +} + +var listenerList []chan string + +func getUsername(conn net.Conn) (s string, err error) { + conn.Write([]byte("What's your name?\nChoice: ")) + name, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return "", err + } + trimmedName := strings.TrimRight(name, "\n") + return trimmedName, nil +} + +func getUserInput(conn net.Conn) (s string, err error) { + message, err := bufio.NewReader(conn).ReadString('\n') + if err != nil { + return "", err + } + + return message, nil +} + +func removeFromList(chatChan chan string) { + for i, v := range listenerList { + if v == chatChan { + listenerList = append(listenerList[:i], listenerList[:i+1]...) + } + } +} + +func handleConn(conn net.Conn, chatChan chan string) { + defer conn.Close() + + go receiveMessageServer(conn, chatChan) + + ////////////////////////////////// + // Get user information + ////////////////////////////////// + userName, err := getUsername(conn) + if err != nil { + log.Printf("Error occurred getting username: %v\n", err) + //removeFromList(chatChan) + return + } + userIP := getIP(conn) + ////////////////////////////////// + + newUserTemplate := new(User) + newUser := newUserTemplate.CreateUser(userName, userIP) + joinMessage := fmt.Sprintf("%v has joined the chat!", newUser.Username) + fmt.Println(joinMessage) + //conn.Write([]byte(joinMessage)) + sendMessage(joinMessage) + + ////////////////////////////////// + for { + message, err := getUserInput(conn) + if err != nil { + quitMessage := fmt.Sprintf("%v has disconnected!", newUser.Username) + fmt.Println(quitMessage) + sendMessage(quitMessage) + //removeFromList(chatChan) + // if _, err := conn.Write([]byte(quitMessage)); err != nil { + // log.Printf("Error happened sending disconnect message: %v", err) + // } + return + } + finalMessage := fmt.Sprintf("[%v] %v: %v", newUser.IP, newUser.Username, strings.TrimRight(message, "\n")) + fmt.Printf("%v\n", finalMessage) + sendMessage(finalMessage) + + //chatChan <- finalMessage + // if _, err := conn.Write([]byte(finalMessage)); err != nil { + // log.Printf("Error happened sending message: %v", err) + // } + } + ////////////////////////////////// +} + +func sendMessage(msg string) { + for _, ch := range listenerList { + ch <- msg + } +} + +func receiveMessageServer(conn net.Conn, chatChan chan string) { + for { + select { + case message := <-chatChan: + conn.Write([]byte(message)) + } + } +} + +func getIP(conn net.Conn) (IP string) { + if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok { + IP = fmt.Sprintf("%v", addr.IP) + } + return IP +} diff --git a/cmd/ui.go b/cmd/ui.go deleted file mode 100644 index 9f75b2f..0000000 --- a/cmd/ui.go +++ /dev/null @@ -1,108 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/jroimartin/gocui" - "github.com/nsf/termbox-go" - "log" - "net" - "time" -) - -func ui(conn net.Conn) { - g, err := gocui.NewGui(gocui.OutputNormal) - if err != nil { - log.Panicln(err) - } - defer g.Close() - g.SetManagerFunc(layout) - g.Mouse = true - g.Cursor = true - initKeybindings(g) - - go listener(g, conn) - - if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { - log.Panicln(err) - } -} - -func listener(g *gocui.Gui, conn net.Conn) { - time.Sleep(time.Second) - chatbox, err := g.View("chatbox") - if err != nil { - log.Panicln(err) - } - for { - reply := make([]byte, 2048) - _, err := conn.Read(reply) - if err != nil { - g.Close() - log.Fatalf("Server closed connection\n") - } - - fmt.Fprintln(chatbox, string(reply)) - termbox.Interrupt() - } -} - -func quit(*gocui.Gui, *gocui.View) error { - return gocui.ErrQuit -} - -func initKeybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - log.Panicln(err) - } - - if err := g.SetKeybinding("button", gocui.MouseLeft, gocui.ModNone, sendmsg); err != nil { - log.Panicln(err) - } - - if err := g.SetKeybinding("textarea", gocui.KeyEnter, gocui.ModNone, sendmsg); err != nil { - log.Panicln(err) - } - return nil -} - -func layout(g *gocui.Gui) error { - maxX, maxY := g.Size() - - if chatbox, err := g.SetView("chatbox", 2, 1, maxX/2+40, maxY-6); err != nil { - if err != gocui.ErrUnknownView { - return err - } - chatbox.Title = "Chat Box" - } - - if button, err := g.SetView("button", maxX/2+32, maxY-4, maxX/2+40, maxY-2); err != nil { - if err != gocui.ErrUnknownView { - return err - } - //button.BgColor = gocui.ColorRed - button.Wrap = true - button.Frame = true - fmt.Fprintln(button, "Send it") - } - - if textarea, err := g.SetView("textarea", 2, maxY-4, maxX/2+28, maxY-2); err != nil { - if err != gocui.ErrUnknownView { - return err - } - if _, err := g.SetCurrentView("textarea"); err != nil { - log.Panicln(err) - } - textarea.Title = "Send message" - textarea.Wrap = true - textarea.Editable = true - } - - // if currentUsers, err := g.SetView("currentUsers", maxX/2+42, 1, maxX-6, maxY-6); err != nil { - // if err != gocui.ErrUnknownView { - // return err - // } - // currentUsers.Title = "Connected users" - // } - - return nil -} diff --git a/go.mod b/go.mod index 815fe61..071c1d1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module mini-chat -go 1.22.1 +go 1.22.2 require ( github.com/jroimartin/gocui v0.5.0 diff --git a/main.go b/main.go index 8970d54..31cf998 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,5 @@ /* -Copyright © 2024 Raul +Copyright © 2024 Raul */ package main -- 2.30.2