mirror of
https://git.mills.io/saltyim/saltyim.git
synced 2024-06-16 11:58:24 +00:00
179 lines
3.8 KiB
Go
179 lines
3.8 KiB
Go
package saltyim
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/dim13/crc24"
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/keys-pub/keys"
|
|
"github.com/rivo/tview"
|
|
"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
|
|
uri string
|
|
me Addr
|
|
inbox 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, uri string, me Addr, inbox, 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,
|
|
uri: uri,
|
|
me: me,
|
|
inbox: inbox,
|
|
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
|
|
}
|
|
|
|
cc.mu.RLock()
|
|
app.QueueUpdateDraw(func() {
|
|
fmt.Fprintf(chatBox,
|
|
"[#%s]%s\t[#%x]%s\t[#%s]%s\n",
|
|
cc.palette["date"],
|
|
s.Timestamp.DateTime().Local().Format(dateTimeFormat),
|
|
getUserColor(s.User.String()).Hex(),
|
|
s.User.String(),
|
|
cc.palette["text"],
|
|
s.LiteralText(),
|
|
)
|
|
})
|
|
cc.mu.RUnlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
// setScreen initializes the layout and UI components.
|
|
func (cc *chatClient) SetScreen(inCh <-chan string, outCh chan<- string) {
|
|
app := tview.NewApplication()
|
|
|
|
title := fmt.Sprintf(
|
|
"Chatting as %s via %s/%s with %s via %s",
|
|
cc.me, cc.uri, cc.inbox, 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.uri, cc.inbox, "", "") {
|
|
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)
|
|
}
|
|
}
|