Nuclear refactoring
Go audit / audit (pull_request) Successful in 1m48s Details

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.
This commit is contained in:
raul 2024-04-26 08:48:48 +02:00
parent cec33907b7
commit 80da12cfcf
8 changed files with 404 additions and 275 deletions

View File

@ -1,34 +1,38 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package cmd package cmd
import ( import (
"bufio"
"fmt" "fmt"
"github.com/jroimartin/gocui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"log"
"net"
"os" "os"
) )
// clientCmd represents the client command // clientCmd represents the client command
var clientCmd = &cobra.Command{ var clientCmd = &cobra.Command{
Use: "client", Use: "client",
Short: "Connect to a mini-chat server", Short: "Client interface for mini-chat",
Long: `Connect to a mini-chat server. Long: `Refactored mini-chat client that properly interfaces
Example: with its server.
./mini-chat client --ip 192.168.0.50 --port 1337`,
Example:
./mini-chat client --ip 192.168.0.100 --port 1337`,
Run: func(cmd *cobra.Command, args []string) { 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() { func init() {
rootCmd.AddCommand(clientCmd) rootCmd.AddCommand(clientCmd)
@ -37,58 +41,32 @@ func init() {
// Cobra supports Persistent Flags which will work for this command // Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.: // and all subcommands, e.g.:
// clientCmd.PersistentFlags().String("foo", "", "A help for foo") // clientCmd.PersistentFlags().String("foo", "", "A help for foo")
clientCmd.Flags().String("ip", "", "Server to connect to") clientCmd.PersistentFlags().String("ip", "", "Server IP to connect to")
clientCmd.Flags().String("port", "1302", "Port to connect to") clientCmd.PersistentFlags().String("port", "1302", "Server port to connect to")
// Cobra supports local flags which will only run when this command // Cobra supports local flags which will only run when this command
// is called directly, e.g.: // is called directly, e.g.:
// clientCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // clientCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
} }
func client(cmd *cobra.Command) { func setClientParameters(cmd *cobra.Command) error {
port, _ := cmd.Flags().GetString("port") parameterPort, err := 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")
if err != nil { if err != nil {
log.Panicln(err) return err
} }
message := textarea.Buffer() if parameterPort != "" {
msg := string(message) serverPort = parameterPort
if msg == "" {
return nil
} }
if msg == "" { parameterIP, err := cmd.Flags().GetString("ip")
return nil 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 return nil
} }

195
cmd/clientFunc.go Normal file
View File

@ -0,0 +1,195 @@
/*
Copyright © 2024 Raul <raul@bulgariu.xyz>
*/
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
}

View File

@ -1,25 +1,20 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package cmd package cmd
import ( import (
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"os"
) )
// rootCmd represents the base command when called without any subcommands // rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "mini-chat", Use: "mini-chat",
Short: "Minimalistic chat server built using Go", Short: "Application for hosting and joining a simple chat server",
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
Examples: Long: `Application for hosting and joining a simple chat server`,
./mini-chat server --port 8080`,
// Uncomment the following line if your bare application // Uncomment the following line if your bare application
// has an action associated with it: // has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { }, // Run: func(cmd *cobra.Command, args []string) { },
@ -39,7 +34,7 @@ func init() {
// Cobra supports persistent flags, which, if defined here, // Cobra supports persistent flags, which, if defined here,
// will be global for your application. // 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 // Cobra also supports local flags, which will only run
// when this action is called directly. // when this action is called directly.

View File

@ -1,31 +1,30 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package cmd package cmd
import ( import (
"bufio"
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"net" "log"
"strings"
"time"
) )
// serverCmd represents the server command // serverCmd represents the server command
var serverCmd = &cobra.Command{ var serverCmd = &cobra.Command{
Use: "server", Use: "server",
Short: "Main chat server", Short: "Tiny chat server",
Long: `You can connect to this server by running the following command from Long: `Refactored mini-chat server designed to be connected to
a client: using a proper interface this time, not using netcat.
nc $SERVER_IP $PORT
Assuming your IP is 192.168.0.30 and the server port is left on default the Example:
command would be as follows: ./mini-chat server --port 1337`,
nc 192.168.0.30 1302`,
Run: func(cmd *cobra.Command, args []string) { 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 // Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.: // and all subcommands, e.g.:
// serverCmd.PersistentFlags().String("foo", "", "A help for foo") // 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 // Cobra supports local flags which will only run when this command
// is called directly, e.g.: // is called directly, e.g.:
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
serverCmd.Flags().String("port", "1302", "Port to listen on")
} }
var receiverIsStarted bool = false func setServerParameters(cmd *cobra.Command) error {
parameterPort, err := cmd.Flags().GetString("port")
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')
if err != nil { if err != nil {
numOfClients-- return err
return
} }
var NewUser = new(chatter) if parameterPort != "" {
NewUser.Username = strings.TrimRight(name, "\n") listenPort = parameterPort
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))
} }
} return nil
func getIP(conn net.Conn) (IP string) {
if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
IP = fmt.Sprintf("%v", addr)
}
return IP
} }

150
cmd/serverFunc.go Normal file
View File

@ -0,0 +1,150 @@
/*
Copyright © 2024 Raul <raul@bulgariu.xyz>
*/
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
}

108
cmd/ui.go
View File

@ -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
}

2
go.mod
View File

@ -1,6 +1,6 @@
module mini-chat module mini-chat
go 1.22.1 go 1.22.2
require ( require (
github.com/jroimartin/gocui v0.5.0 github.com/jroimartin/gocui v0.5.0

View File

@ -1,5 +1,5 @@
/* /*
Copyright © 2024 Raul Copyright © 2024 Raul <raul@bulgariu.xyz>
*/ */
package main package main