6
1
mirror of https://git.mills.io/saltyim/saltyim.git synced 2024-06-16 03:48:24 +00:00
prologic-saltyim/client.go
2022-03-21 16:14:38 +10:00

195 lines
4.2 KiB
Go

package saltyim
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/dim13/crc24"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/keys-pub/keys"
"github.com/posener/formatter"
"github.com/rivo/tview"
log "github.com/sirupsen/logrus"
"go.mills.io/salty"
"go.yarn.social/lextwt"
)
const (
dateTimeFormat = "2006-01-02 15:04:05"
)
func getUserColor(s string) tcell.Color {
c := crc24.Sum([]byte(s))
return tcell.NewRGBColor(
int32(c>>0x10),
int32(c>>0x08),
int32(c),
)
}
type chatClient struct {
mu sync.RWMutex
key *keys.EdX25519Key
me Addr
endpoint string
user string
config Config
// Configurations.
palette map[string]string
}
// NewChatClient initializes a new chatClient.
// Sets up connection with broker, and initializes UI.
func NewChatClient(key *keys.EdX25519Key, me Addr, endpoint, user string) (*chatClient, error) {
config, err := Lookup(user)
if err != nil {
return nil, fmt.Errorf("error: failed to lookup user %q: %w", user, err)
}
return &chatClient{
key: key,
me: me,
endpoint: endpoint,
user: user,
config: config,
palette: map[string]string{
"background": "000000",
"border": "4C5C68",
"title": "46494C",
"date": "E76F51",
"text": "577399",
},
}, nil
}
// newMessageHandler returns an InputHandler that handles outgoing messages.
func (cc *chatClient) newMessageHandler(outCh chan<- string) InputHandler {
handler := func(message string) {
cc.mu.RLock()
outCh <- message
cc.mu.RUnlock()
}
return handler
}
// updateChatBox updates chatBox component with incoming messages.
func (cc *chatClient) updateChatBox(inCh <-chan string, app *tview.Application,
chatBox *tview.TextView) {
for in := range inCh {
s, err := lextwt.ParseSalty(in)
if err != nil {
continue
}
switch s := s.(type) {
case *lextwt.SaltyEvent:
// Ignored for now
case *lextwt.SaltyText:
if s.User.String() != cc.me.String() && s.User.String() != cc.user {
continue
}
buf := &bytes.Buffer{}
_, _, width, _ := chatBox.GetInnerRect()
f := formatter.Formatter{
Writer: buf,
Indent: []byte("> "),
Width: width - 1,
}
if _, err := f.Write([]byte(s.LiteralText())); err != nil {
log.WithError(err).Error("error formatting message")
continue
}
cc.mu.RLock()
app.QueueUpdateDraw(func() {
fmt.Fprintf(chatBox,
"[#%s]%s [#%x]%s\n[#%s]%s\n\n",
cc.palette["date"],
s.Timestamp.DateTime().Local().Format(dateTimeFormat),
getUserColor(s.User.String()).Hex(),
s.User.String(),
cc.palette["text"],
buf.String(),
)
})
cc.mu.RUnlock()
}
}
}
// setScreen initializes the layout and UI components.
func (cc *chatClient) SetScreen(inCh <-chan string, outCh chan<- string) {
encoding.Register()
app := tview.NewApplication()
title := fmt.Sprintf(
"Chatting as %s via %s with %s via %s",
cc.me, cc.endpoint, cc.user, cc.config.Endpoint,
)
// Generate UI components.
cc.mu.RLock()
chatBox := NewChatBox(cc.palette, title)
inputField := NewChatInput(cc.palette, cc.newMessageHandler(outCh))
cc.mu.RUnlock()
// Layout the widgets in flex view.
flex := tview.NewFlex().SetDirection(tview.FlexRow).
AddItem(chatBox, 0, 1, false).
AddItem(inputField, 3, 0, true)
go cc.updateChatBox(inCh, app, chatBox)
err := app.SetRoot(flex, true).SetFocus(inputField).EnableMouse(true).Run()
if err != nil {
log.Fatal(err)
}
}
// Open bi-directional stream between client and server.
func (cc *chatClient) RunChat(inCh chan<- string, outCh <-chan string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Receives incoming messages on a separate goroutine to be non-blocking.
go func() {
for msg := range Read(ctx, cc.key, cc.endpoint, "", "") {
inCh <- msg
}
}()
// Forward/send outgoing messages.
for msg := range outCh {
if strings.TrimSpace(msg) == "" {
continue
}
packedMsg := PackMessage(cc.me, msg)
b, err := salty.Encrypt(cc.key, packedMsg, []string{cc.config.Key})
if err != nil {
log.Fatalf("error encrypting message: %v", err)
}
err = Send(cc.config.Endpoint, string(b))
if err != nil {
log.Fatalf("error sending message: %v", err)
}
inCh <- string(packedMsg)
}
}