Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: connection engine #1

Merged
merged 8 commits into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Lint
on: [push, pull_request]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.18
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.48
14 changes: 14 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
on: [push, pull_request]
name: Tests
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Install Go.
uses: actions/setup-go@v2
with:
go-version: 1.18
- name: Checkout code.
uses: actions/checkout@v2
- name: Run tests.
run: go test -v ./...
124 changes: 124 additions & 0 deletions client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package main

import (
"bufio"
"flag"
"fmt"
"log"
"net/url"
"os"

"github.com/fatih/color"
"github.com/gorilla/websocket"
)

type ConnReader interface {
ReadJSON(v interface{}) error
}

type ConnWriter interface {
WriteJSON(v interface{}) error
Close() error
}

type Scanner interface {
Scan() bool
Text() string
}

type message struct {
Username string `json:"username"`
Text string `json:"text"`
Type string `json:"type"`
}

func main() {
var name string
var s *bufio.Scanner

// Parse flags.
server := flag.String("server", "localhost:9000", "Server network address")
path := flag.String("path", "/", "Server path")
flag.Parse()

// Construct WebSocket URL.
u := url.URL{Scheme: "ws", Host: *server, Path: *path}

// Read username.
fmt.Printf("%s", color.YellowString("Enter your Name: "))
s = bufio.NewScanner(os.Stdin)
s.Scan()
name = s.Text()

// Display welcome message.
color.Green("\nWelcome %s!\n", name)
color.Green("Connecting to server @ %s\n", *server)
color.Yellow("Send a message, or type !q to exit.\n")

// Get WebSocket connection.
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
color.Red("Connection error, exiting: %s", err)
os.Exit(0)
}
defer conn.Close()

// Send joining message.
msg := message{Username: name, Text: "has joined the chat.", Type: "info"}
_ = conn.WriteJSON(msg)

go readMessages(conn) // Handle incoming messages concurrently.
writeMessages(conn, s, name) // Handle outgoing messages concurrently.
}

// readMessages handles incoming messages on the WebSocket connection.
func readMessages(conn ConnReader) {
for {
var msg message

// Read message.
err := conn.ReadJSON(&msg)
if err != nil {
color.Red("Server closed. Exiting...")
os.Exit(0)
}

// Display message.
color.Magenta("%s: %s\n", msg.Username, msg.Text)
}
}

// writeMessages scans stdin and sends each scanned line to the server as JSON.
func writeMessages(conn ConnWriter, s Scanner, name string) {
var msg message
msg.Username = name

for {
fmt.Print("> ")
if s.Scan() {
fmt.Printf("\033[A")
msg.Text = s.Text()

// Handle quit event.
if msg.Text == "!q" {
fmt.Println("Goodbye!")
_ = conn.WriteJSON(message{Username: name, Text: "has disconnected.", Type: "info"})
conn.Close()
os.Exit(0)
}

// Display message.
if msg.Type != "" {
color.Cyan("%s %s\n", msg.Username, msg.Text)
} else {
color.Cyan("%s: %s\n", msg.Username, msg.Text)
}

// Write message to connection.
err := conn.WriteJSON(msg)
if err != nil {
log.Fatal("Error sending message, exiting")
}
}
}
}
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ go 1.18
require (
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.22.1
github.com/fatih/color v1.13.0
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/lipgloss v0.5.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,18 @@ github.com/charmbracelet/lipgloss v0.5.0 h1:lulQHuVeodSgDez+3rGiuxlPVXSnhth442DA
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
Expand All @@ -37,6 +46,8 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
108 changes: 108 additions & 0 deletions server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"flag"
"log"
"net/http"
"time"

"github.com/fatih/color"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)

type message struct {
Username string `json:"username"`
Text string `json:"text"`
Type string `json:"type"`
ID uuid.UUID
}

// Upgrader instance to upgrade all HTTP connections to a WebSocket.
var upgrader = websocket.Upgrader{}

// Map to store currently active client connections.
var activeClients = make(map[*websocket.Conn]uuid.UUID)

// Channel for client messages.
var messageChan = make(chan message)

func main() {
// Parse flags.
addr := flag.String("addr", ":9000", "Server's network address")
flag.Parse()

mux := http.NewServeMux()
mux.HandleFunc("/", handleConn)

// Handle incoming messages.
go handleMsg()

// Start the server.
log.Printf("Starting server on %s", *addr)
err := http.ListenAndServe(*addr, mux)
if err != nil {
log.Fatal("Error starting server, exiting.", err)
}
}

// handleConn handles incoming HTTP connections by adding the connection to activeClients and reads messages from the connection.
func handleConn(w http.ResponseWriter, r *http.Request) {
// Upgrade incoming HTTP connections to WebSocket connections
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Error upgrading connection to websocket: %v", err)
}
defer conn.Close()

// Generate a UUID for the client.
activeClients[conn] = uuid.New()

for {
var msg message

// Read message from the connection.
err := conn.ReadJSON(&msg)
if err != nil {
log.Printf("Closing connection with ID: %v", activeClients[conn])
delete(activeClients, conn)
break
}

// Set message ID
msg.ID = activeClients[conn]

// Send message to messageChan.
messageChan <- msg
}
}

// handleMsg listens to the messageChan channel and broadcasts messages to other clients.
func handleMsg() {
for {
// Get message from messageChan.
msg := <-messageChan

// Log each message to stdout.
t := time.Now().Format(time.ANSIC)
if msg.Type != "" {
color.Green("%s >> %s %s\n", t, msg.Username, msg.Text)
} else {
color.Green("%s >> %s: %s\n", t, msg.Username, msg.Text)
}

// Broadcast to all active clients.
for client, UUID := range activeClients {
// Check the UUID to prevent sending messages to their origin.
if msg.ID != UUID {
// Write JSON message.
err := client.WriteJSON(msg)
if err != nil {
log.Printf("Error sending message to client: %v", err)
client.Close()
delete(activeClients, client)
}
}
}
}
}
2 changes: 1 addition & 1 deletion tui/main.go → tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
)

func main() {
func UI() { //nolint:deadcode
p := tea.NewProgram(initialModel())
if err := p.Start(); err != nil {
log.Fatal(err)
Expand Down