2024-03-22 18:38:19 +00:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
2024-04-29 00:02:51 +00:00
|
|
|
"bufio"
|
|
|
|
"context"
|
|
|
|
"errors"
|
2024-03-29 15:53:10 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2024-03-24 13:09:36 +00:00
|
|
|
"net/url"
|
2024-03-24 16:10:02 +00:00
|
|
|
"os"
|
2024-04-29 11:46:26 +00:00
|
|
|
"slices"
|
2024-04-29 00:02:51 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2024-04-29 11:46:26 +00:00
|
|
|
"sync"
|
2024-03-22 18:38:19 +00:00
|
|
|
"time"
|
|
|
|
|
2024-03-24 16:10:02 +00:00
|
|
|
"github.com/charmbracelet/log"
|
2024-03-24 13:09:36 +00:00
|
|
|
"github.com/gorilla/websocket"
|
2024-04-29 00:02:51 +00:00
|
|
|
cm "krzyzanowski.dev/archat/common"
|
2024-03-22 18:38:19 +00:00
|
|
|
)
|
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
type Context struct {
|
|
|
|
conn *websocket.Conn
|
|
|
|
// Assumption: size of 1 is enough, because first response read will be response for the last request
|
|
|
|
// no need to buffer
|
2024-04-29 11:46:26 +00:00
|
|
|
resFromServer chan cm.RFrame
|
|
|
|
reqFromServer chan cm.RFrame
|
|
|
|
rToServer chan cm.RFrame
|
|
|
|
initiations []*cm.Initiation
|
|
|
|
initiationsLock sync.RWMutex
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-24 16:10:02 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
func NewClientContext(conn *websocket.Conn) *Context {
|
|
|
|
return &Context{
|
|
|
|
conn: conn,
|
|
|
|
resFromServer: make(chan cm.RFrame),
|
|
|
|
reqFromServer: make(chan cm.RFrame),
|
|
|
|
rToServer: make(chan cm.RFrame),
|
2024-03-24 16:10:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func (cliCtx *Context) serverHandler(syncCtx context.Context) error {
|
|
|
|
defer logger.Debug("server handler last line...")
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
handleNext:
|
2024-04-29 00:02:51 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-syncCtx.Done():
|
|
|
|
return nil
|
|
|
|
case reqFrame := <-cliCtx.reqFromServer:
|
|
|
|
logger.Debug("got request from server", "id", reqFrame.ID)
|
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
var res cm.Response
|
|
|
|
var err error
|
2024-04-29 00:02:51 +00:00
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
if reqFrame.ID == cm.EchoReqID {
|
|
|
|
res, err = cliCtx.handleEcho(reqFrame)
|
|
|
|
} else if reqFrame.ID == cm.StartChatBReqID {
|
|
|
|
res, err = cliCtx.handleStartChatB(reqFrame)
|
|
|
|
} else if reqFrame.ID == cm.StartChatDReqID {
|
|
|
|
res, err = cliCtx.handleStartChatD(reqFrame)
|
2024-04-29 00:02:51 +00:00
|
|
|
} else {
|
|
|
|
logger.Warn("can't handle it!")
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-04-29 11:46:26 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("could not handle request ID=%d", reqFrame.ID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if res == nil {
|
|
|
|
logger.Debugf("request without response ID=%d", reqFrame.ID)
|
|
|
|
continue handleNext
|
|
|
|
}
|
|
|
|
|
|
|
|
resFrame, err := cm.ResponseFrameFrom(res)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("could not create frame from response")
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
cliCtx.rToServer <- resFrame
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
func (cliCtx *Context) handleEcho(reqFrame cm.RFrame) (res cm.Response, err error) {
|
|
|
|
echoReq, err := cm.RequestFromFrame[cm.EchoRequest](reqFrame)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return cm.EchoResponse(echoReq), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cliCtx *Context) handleStartChatB(reqFrame cm.RFrame) (res cm.Response, err error) {
|
|
|
|
startChatBReq, err := cm.RequestFromFrame[cm.StartChatBRequest](reqFrame)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Infof("got start chat, %s wants to contact. use startchatc command to "+
|
|
|
|
"decide if you want to accept the chat", startChatBReq.Nickname)
|
|
|
|
|
|
|
|
cliCtx.initiationsLock.Lock()
|
|
|
|
cliCtx.initiations = append(cliCtx.initiations, &cm.Initiation{
|
|
|
|
AbANick: startChatBReq.Nickname,
|
|
|
|
AbBNick: "",
|
|
|
|
Stage: cm.InitiationStageB,
|
|
|
|
})
|
|
|
|
cliCtx.initiationsLock.Unlock()
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cliCtx *Context) handleStartChatD(reqFrame cm.RFrame) (res cm.Response, err error) {
|
|
|
|
startChatDReq, err := cm.RequestFromFrame[cm.StartChatDRequest](reqFrame)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Infof("servers wants to be punched, got start chat d request for %s with code %s",
|
|
|
|
startChatDReq.Nickname, startChatDReq.PunchCode)
|
|
|
|
logger.Warn("handleStartChatD not implemented yet")
|
|
|
|
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func (cliCtx *Context) serverWriter(syncCtx context.Context) error {
|
|
|
|
defer logger.Debug("server writer last line...")
|
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
for {
|
|
|
|
logger.Debug("waiting for a frame to write")
|
2024-04-29 00:02:51 +00:00
|
|
|
select {
|
|
|
|
case <-syncCtx.Done():
|
|
|
|
return nil
|
|
|
|
case frameToWrite := <-cliCtx.rToServer:
|
|
|
|
err := cliCtx.conn.WriteJSON(frameToWrite)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
logger.Debug("frame written", "id", frameToWrite.ID)
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func (cliCtx *Context) serverReader(syncCtx context.Context) error {
|
|
|
|
defer logger.Debug("server reader last line...")
|
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
for {
|
|
|
|
logger.Debug("waiting for a frame to read")
|
|
|
|
var rFrame cm.RFrame
|
|
|
|
err := cliCtx.conn.ReadJSON(&rFrame)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Debug("frame read", "id", rFrame.ID)
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 15:53:10 +00:00
|
|
|
if rFrame.IsResponse() {
|
2024-03-29 14:05:42 +00:00
|
|
|
cliCtx.resFromServer <- rFrame
|
|
|
|
} else {
|
|
|
|
cliCtx.reqFromServer <- rFrame
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Debug("frame pushed", "id", rFrame.ID)
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
func (cliCtx *Context) sendRequest(req cm.Request) error {
|
|
|
|
rf, err := cm.RequestFrameFrom(req)
|
2024-03-24 13:09:36 +00:00
|
|
|
if err != nil {
|
2024-03-29 14:05:42 +00:00
|
|
|
return err
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
cliCtx.rToServer <- rf
|
|
|
|
return nil
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
func (cliCtx *Context) getResponseFrame() cm.RFrame {
|
|
|
|
return <-cliCtx.resFromServer
|
|
|
|
}
|
|
|
|
|
|
|
|
var logger = log.NewWithOptions(os.Stdout, log.Options{
|
|
|
|
ReportTimestamp: true,
|
|
|
|
TimeFormat: time.TimeOnly,
|
|
|
|
Prefix: "👤 Client",
|
|
|
|
})
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
if cm.IsProd {
|
|
|
|
logger.SetLevel(log.InfoLevel)
|
|
|
|
} else {
|
|
|
|
logger.SetLevel(log.DebugLevel)
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func sendAuth(ctx *Context, nick, pass string) {
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Info("trying to authenticate as krzmaciek...")
|
2024-04-29 00:02:51 +00:00
|
|
|
err := ctx.sendRequest(cm.AuthRequest{Nickname: nick, Password: pass})
|
2024-03-29 15:53:10 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Debug("request sent, waiting for response...")
|
2024-03-29 14:05:42 +00:00
|
|
|
arf := ctx.getResponseFrame()
|
|
|
|
ar, err := cm.ResponseFromFrame[cm.AuthResponse](arf)
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-03-22 18:38:19 +00:00
|
|
|
if err != nil {
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Error(err)
|
2024-03-29 15:53:10 +00:00
|
|
|
return
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Infof("Authenticated?: %t", ar.IsSuccess)
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func sendEcho(ctx *Context, echoByte byte) {
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Info("testing echo...", "echoByte", echoByte)
|
2024-04-29 00:02:51 +00:00
|
|
|
err := ctx.sendRequest(cm.EchoRequest{EchoByte: echoByte})
|
2024-03-29 15:53:10 +00:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Debug("request sent, waiting for response...")
|
2024-03-29 14:05:42 +00:00
|
|
|
ereqf := ctx.getResponseFrame()
|
|
|
|
ereq, err := cm.ResponseFromFrame[cm.EchoResponse](ereqf)
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
if err != nil {
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Error(err)
|
2024-03-29 15:53:10 +00:00
|
|
|
return
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Info("got response", "echoByte", ereq.EchoByte)
|
2024-03-29 14:05:42 +00:00
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func sendListPeers(ctx *Context) {
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Info("trying to get list of peers...")
|
2024-03-29 15:53:10 +00:00
|
|
|
err := ctx.sendRequest(cm.ListPeersRequest{})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Debug("request sent, waiting for response...")
|
2024-03-29 14:05:42 +00:00
|
|
|
lpreqf := ctx.getResponseFrame()
|
|
|
|
lpreq, err := cm.ResponseFromFrame[cm.ListPeersResponse](lpreqf)
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-03-24 13:09:36 +00:00
|
|
|
if err != nil {
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Error(err)
|
2024-03-29 15:53:10 +00:00
|
|
|
return
|
2024-03-24 13:09:36 +00:00
|
|
|
}
|
2024-03-29 15:53:10 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
logger.Info("Got that list", "peersList", lpreq.PeersInfo)
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
func sendStartChatA(ctx *Context, nick string) {
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Info("doing chat start A...")
|
2024-04-29 00:02:51 +00:00
|
|
|
err := ctx.sendRequest(cm.StartChatARequest{Nickname: nick})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-04-29 11:46:26 +00:00
|
|
|
logger.Debug("request sent, no wait for response")
|
|
|
|
}
|
|
|
|
|
|
|
|
func sendStartChatC(ctx *Context, nick string) {
|
|
|
|
idx := slices.IndexFunc(ctx.initiations, func(i *cm.Initiation) bool {
|
|
|
|
return i.AbANick == nick
|
|
|
|
})
|
|
|
|
|
|
|
|
if idx == -1 {
|
|
|
|
logger.Warn("user of that nick did not initiate connection, ignoring")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err := ctx.sendRequest(cm.StartChatCRequest{Nickname: nick})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.Debug("request sent, no wait for response")
|
2024-04-29 00:02:51 +00:00
|
|
|
}
|
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
func RunClient() {
|
|
|
|
u := url.URL{Scheme: "ws", Host: ":8080", Path: "/wsapi"}
|
|
|
|
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
2024-03-22 18:38:19 +00:00
|
|
|
|
2024-03-29 14:05:42 +00:00
|
|
|
if err != nil {
|
|
|
|
logger.Error("could not connect to websocket")
|
|
|
|
return
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|
|
|
|
|
2024-04-29 00:02:51 +00:00
|
|
|
cliCtx := NewClientContext(c)
|
|
|
|
errGroup, syncCtx := errgroup.WithContext(context.Background())
|
|
|
|
|
|
|
|
errGroup.Go(func() error {
|
|
|
|
return cliCtx.serverHandler(syncCtx)
|
|
|
|
})
|
|
|
|
|
|
|
|
errGroup.Go(func() error {
|
|
|
|
return cliCtx.serverReader(syncCtx)
|
|
|
|
})
|
|
|
|
|
|
|
|
errGroup.Go(func() error {
|
|
|
|
return cliCtx.serverWriter(syncCtx)
|
|
|
|
})
|
|
|
|
|
|
|
|
errGroup.Go(func() error {
|
|
|
|
<-syncCtx.Done()
|
|
|
|
logger.Info("closing client...")
|
|
|
|
time.Sleep(time.Second * 3)
|
|
|
|
close(cliCtx.rToServer)
|
|
|
|
close(cliCtx.resFromServer)
|
|
|
|
close(cliCtx.reqFromServer)
|
|
|
|
_ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
|
|
|
return c.Close()
|
|
|
|
})
|
|
|
|
|
|
|
|
closer := make(chan int)
|
|
|
|
errGroup.Go(func() error {
|
|
|
|
select {
|
|
|
|
case <-closer:
|
|
|
|
return errors.New("close")
|
|
|
|
case <-syncCtx.Done():
|
|
|
|
return nil
|
2024-03-29 15:53:10 +00:00
|
|
|
}
|
2024-04-29 00:02:51 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
|
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
cmd := strings.TrimRight(scanner.Text(), " \n\t")
|
|
|
|
cmdElements := strings.Split(cmd, " ")
|
|
|
|
cmdName := cmdElements[0]
|
|
|
|
cmdArgs := cmdElements[1:]
|
|
|
|
|
|
|
|
if cmdName == "exit" {
|
|
|
|
logger.Info("closing...")
|
|
|
|
closer <- 1
|
|
|
|
} else if cmdName == "echo" {
|
|
|
|
if len(cmdArgs) != 1 {
|
|
|
|
logger.Errorf("echo command requires 1 argument, but %d was provided", len(cmdArgs))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
num, err := strconv.Atoi(cmdArgs[0])
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Errorf("%s is not a number", cmdArgs[0])
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
sendEcho(cliCtx, byte(num))
|
|
|
|
} else if cmdName == "list" {
|
|
|
|
sendListPeers(cliCtx)
|
|
|
|
} else if cmdName == "auth" {
|
|
|
|
if len(cmdArgs) != 2 {
|
|
|
|
logger.Errorf("auth command requires 2 argument, but %d was provided", len(cmdArgs))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
nick := cmdArgs[0]
|
|
|
|
pass := cmdArgs[1]
|
|
|
|
|
|
|
|
sendAuth(cliCtx, nick, pass)
|
|
|
|
} else if cmdName == "startchata" {
|
|
|
|
if len(cmdArgs) != 1 {
|
|
|
|
logger.Errorf("startchata command requires 1 argument, but %d was provided", len(cmdArgs))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
sendStartChatA(cliCtx, cmdArgs[0])
|
2024-04-29 11:46:26 +00:00
|
|
|
} else if cmdName == "initations" {
|
|
|
|
logger.Info("displaying all initations...")
|
|
|
|
|
|
|
|
cliCtx.initiationsLock.RLock()
|
|
|
|
for _, i := range cliCtx.initiations {
|
|
|
|
logger.Debugf("from %s, stage: %d", i.AbANick, i.Stage)
|
|
|
|
}
|
|
|
|
cliCtx.initiationsLock.RUnlock()
|
|
|
|
} else if cmdName == "startchatc" {
|
|
|
|
if len(cmdArgs) != 1 {
|
|
|
|
logger.Errorf("startchatc command requires 1 argument, but %d was provided", len(cmdArgs))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
sendStartChatC(cliCtx, cmdArgs[0])
|
2024-04-29 00:02:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2024-03-29 14:05:42 +00:00
|
|
|
|
2024-03-29 15:53:10 +00:00
|
|
|
err = errGroup.Wait()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
logger.Error(err)
|
|
|
|
}
|
2024-03-22 18:38:19 +00:00
|
|
|
}
|