summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKyren223 <Kyren223@proton.me>2025-07-08 17:53:36 +0300
committerKyren223 <Kyren223@proton.me>2025-07-08 17:53:36 +0300
commita34919da2f3c3102434635276250cc4d398c8492 (patch)
tree827268e8f90be84cd8b38d6aa07682477f65cc91
parent8362d835eed8f7f552a3aa433ea4f02dd1dbde88 (diff)
Refactored client side to comply with protocol changes, implemnted tos
screen model, still missing accept/decline and authentication
-rw-r--r--internal/client/gateway/gateway.go33
-rw-r--r--internal/client/ui/core/core.go187
-rw-r--r--internal/client/ui/tosscreen/tosscreen.go332
3 files changed, 489 insertions, 63 deletions
diff --git a/internal/client/gateway/gateway.go b/internal/client/gateway/gateway.go
index e5fd45e..f98a3fb 100644
--- a/internal/client/gateway/gateway.go
+++ b/internal/client/gateway/gateway.go
@@ -14,7 +14,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
- "github.com/kyren223/eko/certs"
+ "github.com/kyren223/eko/embeds"
"github.com/kyren223/eko/internal/client/config"
"github.com/kyren223/eko/internal/client/ui"
"github.com/kyren223/eko/internal/packet"
@@ -30,36 +30,36 @@ var (
)
type (
- ConnectionEstablished snowflake.ID
- ConnectionFailed error
- ConnectionLost error
- ConnectionClosed struct{}
+ AuthenticationEstablished snowflake.ID
+ ConnectionEstablished struct{}
+ ConnectionFailed error
+ ConnectionLost error
+ ConnectionClosed struct{}
)
-func Connect(privKey ed25519.PrivateKey, timeout time.Duration) tea.Cmd {
+func Connect(timeout time.Duration) tea.Cmd {
return func() tea.Msg {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
- id, err := connect(ctx, privKey)
+ err := connect(ctx)
if err != nil {
return ConnectionFailed(err)
}
- return ConnectionEstablished(id)
+ return ConnectionEstablished{}
}
}
-func connect(ctx context.Context, privKey ed25519.PrivateKey) (snowflake.ID, error) {
+func connect(ctx context.Context) error {
assert.Assert(conn == nil, "cannot connect, connection is active")
closed = false
- var id snowflake.ID
connChan := make(chan net.Conn, 1)
errChan := make(chan error, 1)
go func() {
framer = packet.NewFramer()
certPool := x509.NewCertPool()
- if !certPool.AppendCertsFromPEM(certs.CertPEM) {
+ if !certPool.AppendCertsFromPEM(embeds.ServerCertificate) {
log.Fatalln("failed to append server certificate")
}
@@ -85,11 +85,6 @@ func connect(ctx context.Context, privKey ed25519.PrivateKey) (snowflake.ID, err
}
log.Println("established connection with the server")
- if id, err = handleAuth(ctx, connection, privKey); err != nil {
- errChan <- err
- return
- }
- log.Println("successfully authenticated with the server")
connChan <- connection
}()
@@ -97,15 +92,15 @@ func connect(ctx context.Context, privKey ed25519.PrivateKey) (snowflake.ID, err
case connection := <-connChan:
conn = connection
case err := <-errChan:
- return 0, err
+ return err
case <-ctx.Done():
- return 0, ctx.Err()
+ return ctx.Err()
}
go readForever(conn)
go handlePacketStream()
- return id, nil
+ return nil
}
func handleAuth(ctx context.Context, conn net.Conn, privKey ed25519.PrivateKey) (snowflake.ID, error) {
diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go
index 8ff7810..61810cd 100644
--- a/internal/client/ui/core/core.go
+++ b/internal/client/ui/core/core.go
@@ -33,6 +33,7 @@ import (
"github.com/kyren223/eko/internal/client/ui/core/state"
"github.com/kyren223/eko/internal/client/ui/core/usersettings"
"github.com/kyren223/eko/internal/client/ui/loadscreen"
+ "github.com/kyren223/eko/internal/client/ui/tosscreen"
"github.com/kyren223/eko/internal/client/ui/viminput"
"github.com/kyren223/eko/internal/data"
"github.com/kyren223/eko/internal/packet"
@@ -40,12 +41,12 @@ import (
"github.com/kyren223/eko/pkg/snowflake"
)
-var (
- connectingToServer = "Connecting to server.."
- connectionFailed = "Connection failed - retrying in %d sec..."
- connectionTimeout = 5 * time.Second
- initialTimeout = 3750 * time.Millisecond
- timerInterval = 50 * time.Millisecond
+const (
+ ConnectingToServer = "Connecting to server.."
+ ConnectionFailed = "Connection failed - retrying in %d sec..."
+ ConnectionTimeout = 5 * time.Second
+ InitialTimeout = 3750 * time.Millisecond
+ TimerInterval = 50 * time.Millisecond
)
const (
@@ -62,14 +63,25 @@ const (
FocusMax
)
+type State int
+
+const (
+ Disconnected State = iota
+ Connected
+ ConnectedReceivedTos
+ ConnectedAcceptedTos
+ Authenticated
+)
+
type Model struct {
name string
privKey ed25519.PrivateKey
- loading loadscreen.Model
- timer timer.Model
- timeout time.Duration
- connected bool
+ loading loadscreen.Model
+ tos *tosscreen.Model
+ timer timer.Model
+ timeout time.Duration
+ state State
helpPopup *HelpPopup
userSettingsPopup *usersettings.Model
@@ -94,10 +106,11 @@ func New(privKey ed25519.PrivateKey, name string) Model {
m := Model{
name: name,
privKey: privKey,
- loading: loadscreen.New(connectingToServer),
- timer: newTimer(initialTimeout),
- timeout: initialTimeout,
- connected: false,
+ loading: loadscreen.New(ConnectingToServer),
+ tos: nil,
+ timer: newTimer(InitialTimeout),
+ timeout: InitialTimeout,
+ state: Disconnected,
helpPopup: nil,
userSettingsPopup: nil,
networkCreationPopup: nil,
@@ -108,6 +121,7 @@ func New(privKey ed25519.PrivateKey, name string) Model {
banReasonPopup: nil,
banViewPopup: nil,
signalAddPopup: nil,
+ profilePopup: nil,
networkList: networklist.New(),
signalList: signallist.New(),
frequencyList: frequencylist.New(),
@@ -121,12 +135,29 @@ func New(privKey ed25519.PrivateKey, name string) Model {
}
func (m Model) Init() tea.Cmd {
- return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init())
+ return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init())
}
func (m Model) View() string {
- if !m.connected {
+ switch m.state {
+ case Disconnected:
return m.loading.View()
+
+ case Connected:
+ return m.loading.View()
+ case ConnectedReceivedTos:
+ assert.NotNil(m.tos, "TOS is expected to not be nil if received TOS already")
+ return m.tos.View()
+
+ case ConnectedAcceptedTos:
+ // TODO: auth
+ return m.loading.View()
+
+ case Authenticated:
+ // Continue
+
+ default:
+ panic(fmt.Sprintf("unexpected core.State: %#v", m.state))
}
if m.HasPopup() {
@@ -188,36 +219,52 @@ func (m Model) View() string {
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- if m.connected {
+ switch m.state {
+
+ case Disconnected:
+ cmd := m.updateDisconnected(msg)
+ return m, cmd
+
+ case Connected:
+ fallthrough
+ case ConnectedReceivedTos:
+ fallthrough
+ case ConnectedAcceptedTos:
cmd := m.updateConnected(msg)
return m, cmd
- } else {
- cmd := m.updateNotConnected(msg)
+
+ case Authenticated:
+ cmd := m.updateAuthenticated(msg)
return m, cmd
+
+ default:
+ panic(fmt.Sprintf("unexpected core.State: %#v", m.state))
}
}
-func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd {
+func (m *Model) updateDisconnected(msg tea.Msg) tea.Cmd {
switch msg := msg.(type) {
case gateway.ConnectionEstablished:
- state.UserID = (*snowflake.ID)(&msg)
- m.connected = true
- m.timeout = initialTimeout
-
- var setName tea.Cmd
- if m.name != "" {
- setName = gateway.Send(&packet.SetUserData{
- Data: nil,
- User: &data.User{
- Name: m.name,
- Description: "",
- IsPublicDM: true,
- },
- })
- m.name = ""
- }
+ m.state = Connected
+ m.timeout = InitialTimeout
- return tea.Batch(m.timer.Stop(), setName)
+ // TODO:
+ // // state.UserID = (*snowflake.ID)(&msg)
+ // var setName tea.Cmd
+ // if m.name != "" {
+ // setName = gateway.Send(&packet.SetUserData{
+ // Data: nil,
+ // User: &data.User{
+ // Name: m.name,
+ // Description: "",
+ // IsPublicDM: true,
+ // },
+ // })
+ // m.name = ""
+ // }
+ //
+ // return tea.Batch(m.timer.Stop(), setName)
+ return m.timer.Stop()
case gateway.ConnectionFailed:
log.Println("failed to connect:", msg)
@@ -227,8 +274,8 @@ func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd {
case timer.TimeoutMsg:
m.timeout = min(m.timeout*2, time.Minute)
- m.loading.SetContent(connectingToServer)
- return gateway.Connect(m.privKey, connectionTimeout)
+ m.loading.SetContent(ConnectingToServer)
+ return gateway.Connect(ConnectionTimeout)
case timer.StartStopMsg:
var cmd tea.Cmd
@@ -252,6 +299,54 @@ func (m *Model) updateNotConnected(msg tea.Msg) tea.Cmd {
}
func (m *Model) updateConnected(message tea.Msg) tea.Cmd {
+ switch msg := message.(type) {
+
+ case gateway.ConnectionLost:
+ m.state = Disconnected
+ m.timeout = InitialTimeout
+ return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init())
+
+ case *packet.TosInfo:
+ m.state = ConnectedReceivedTos
+ combined := msg.Tos + "\n" + msg.PrivacyPolicy
+ tos := tosscreen.New(combined)
+ m.tos = &tos
+ // var setName tea.Cmd
+ // if m.name != "" {
+ // setName = gateway.Send(&packet.SetUserData{
+ // Data: nil,
+ // User: &data.User{
+ // Name: m.name,
+ // Description: "",
+ // IsPublicDM: true,
+ // },
+ // })
+ // m.name = ""
+ // }
+ //
+ // return tea.Batch(m.timer.Stop(), setName)
+
+ case tea.KeyMsg:
+ switch msg.String() {
+ case "enter":
+ if m.state == ConnectedReceivedTos {
+ // TODO:
+ m.state = ConnectedAcceptedTos
+ }
+ }
+
+ }
+
+ if m.state == ConnectedReceivedTos {
+ tos, cmd := m.tos.Update(message)
+ m.tos = &tos
+ return cmd
+ }
+
+ return nil
+}
+
+func (m *Model) updateAuthenticated(message tea.Msg) tea.Cmd {
totalWidth := max(ui.Width, ui.MinWidth)
totalWidth -= NetworkWidth
sidebarWidth := int(math.Round(float64(totalWidth) * SidebarPercentage))
@@ -274,9 +369,9 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd {
case gateway.ConnectionLost:
state.UserID = nil
- m.connected = false
- m.timeout = initialTimeout
- return tea.Batch(gateway.Connect(m.privKey, connectionTimeout), m.loading.Init())
+ m.state = Disconnected
+ m.timeout = InitialTimeout
+ return tea.Batch(gateway.Connect(ConnectionTimeout), m.loading.Init())
case *packet.Error:
err := "new connection from another location, closing this one"
@@ -619,11 +714,11 @@ func (m *Model) updateConnected(message tea.Msg) tea.Cmd {
func (m *Model) updateLoadScreenContent() {
seconds := m.timer.Timeout.Round(time.Second) / time.Second
- m.loading.SetContent(fmt.Sprintf(connectionFailed, seconds))
+ m.loading.SetContent(fmt.Sprintf(ConnectionFailed, seconds))
}
func newTimer(timeout time.Duration) timer.Model {
- return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), timerInterval)
+ return timer.NewWithInterval(timeout.Truncate(time.Second)+(time.Second/2), TimerInterval)
}
func (m *Model) move(direction int) {
@@ -814,3 +909,7 @@ func getSignalNotification(signal snowflake.ID) int {
return pings
}
+
+func (m Model) ViewTos() string {
+ return "TODO TOS"
+}
diff --git a/internal/client/ui/tosscreen/tosscreen.go b/internal/client/ui/tosscreen/tosscreen.go
new file mode 100644
index 0000000..317a724
--- /dev/null
+++ b/internal/client/ui/tosscreen/tosscreen.go
@@ -0,0 +1,332 @@
+package tosscreen
+
+import (
+ "strings"
+
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/lipgloss"
+
+ // "github.com/charmbracelet/glamour"
+ "github.com/kyren223/eko/internal/client/ui"
+ "github.com/kyren223/eko/internal/client/ui/colors"
+ "github.com/kyren223/eko/pkg/assert"
+)
+
+const MarginPercentage = 0.25
+
+var style = func() lipgloss.Style {
+ return lipgloss.NewStyle().
+ Border(lipgloss.ThickBorder()).
+ BorderBackground(colors.Background).
+ Background(colors.Background).
+ MarginBackground(colors.BackgroundDimmer).
+ Padding(0, 3).
+ Margin(1, int(float32(ui.Width)*MarginPercentage), 0)
+}
+
+type Model struct {
+ content string
+ vp viewport.Model
+}
+
+func New(content string) Model {
+ m := Model{
+ content: content,
+ vp: viewport.New(ui.Width, ui.Height),
+ }
+ m.SetContent(content)
+
+ return m
+}
+
+func (m Model) Init() tea.Cmd {
+ return nil
+}
+
+func (m Model) View() string {
+ footer := "-- 󰳝 Accept | ^C Decline | ↑↓ or J/K Scroll | ^D/^U PgDn/PgUp --"
+
+ fWidth := lipgloss.Width(footer)
+ paddingLeft := (ui.Width - fWidth) / 2
+ paddingRight := ui.Width - fWidth - paddingLeft
+ footerStyle := lipgloss.NewStyle().
+ Padding(0, paddingLeft, 0, paddingRight).
+ Background(colors.BackgroundDimmer).
+ MarginBackground(colors.BackgroundDimmer)
+ footer = footerStyle.Render(footer)
+
+ view := m.vp.View()
+
+ return lipgloss.JoinVertical(lipgloss.Center, view, footer)
+}
+
+func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
+ var cmd tea.Cmd
+
+ if m.vp.Width != ui.Width {
+ m.vp.Width = ui.Width
+ m.updateContent()
+ }
+ if m.vp.Height != ui.Height-1 {
+ m.vp.Height = ui.Height - 1
+ m.updateContent()
+ }
+
+ m.vp.Style = style()
+ m.vp, cmd = m.vp.Update(msg)
+
+ return m, cmd
+}
+
+func (m *Model) updateContent() {
+ style := style()
+ margin := style.GetMarginLeft() + style.GetMarginRight()
+ padding := style.GetPaddingLeft() + style.GetPaddingRight()
+ border := 2
+ lineWidth := ui.Width - margin - padding - border
+
+ r, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(MdStyle()),
+ glamour.WithWordWrap(lineWidth),
+ )
+ content := m.content
+ content = strings.ReplaceAll(content, "@", "") // HACK: workaround to remove mailto links
+ content, err := r.Render(content)
+ assert.NoError(err, "this should never error")
+ content = strings.ReplaceAll(content, "", "@") // HACK: workaround to remove mailto links
+ m.vp.SetContent(content)
+}
+
+func (m *Model) SetContent(content string) {
+ m.content = content
+ m.updateContent()
+}
+
+func MdStyle() ansi.StyleConfig {
+ mdStyle := ansi.StyleConfig{
+ Document: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockPrefix: "\n",
+ BlockSuffix: "\n",
+ Color: stringPtr(colors.White),
+ BackgroundColor: stringPtr(colors.Background),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ BlockQuote: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{},
+ Indent: uintPtr(1),
+ IndentToken: stringPtr("│ "),
+ },
+ List: ansi.StyleList{
+ LevelIndent: defaultListIndent,
+ },
+ Heading: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockSuffix: "\n",
+ Color: stringPtr(colors.LightBlue),
+ Bold: boolPtr(true),
+ },
+ },
+ H1: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: " ",
+ Suffix: " ",
+ Color: stringPtr(colors.Black),
+ BackgroundColor: stringPtr(colors.LightBlue),
+ Bold: boolPtr(true),
+ },
+ },
+ H2: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "## ",
+ },
+ },
+ H3: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "### ",
+ },
+ },
+ H4: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "#### ",
+ },
+ },
+ H5: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "##### ",
+ },
+ },
+ H6: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "###### ",
+ Bold: boolPtr(false),
+ },
+ },
+ Strikethrough: ansi.StylePrimitive{
+ CrossedOut: boolPtr(true),
+ },
+ Emph: ansi.StylePrimitive{
+ Italic: boolPtr(true),
+ },
+ Strong: ansi.StylePrimitive{
+ Color: stringPtr(colors.Orange),
+ Bold: boolPtr(true),
+ },
+ HorizontalRule: ansi.StylePrimitive{
+ Color: stringPtr(colors.Gray),
+ Format: "\n--------\n",
+ },
+ Item: ansi.StylePrimitive{
+ BlockPrefix: " ",
+ },
+ Enumeration: ansi.StylePrimitive{
+ BlockPrefix: ". ",
+ },
+ Task: ansi.StyleTask{
+ StylePrimitive: ansi.StylePrimitive{},
+ Ticked: "[✓] ",
+ Unticked: "[ ] ",
+ },
+ Link: ansi.StylePrimitive{
+
+ // Format: "Bozo",
+ },
+ LinkText: ansi.StylePrimitive{
+ Color: stringPtr(colors.Gold),
+ Bold: boolPtr(true),
+ Conceal: boolPtr(true),
+ Faint: boolPtr(true),
+ },
+ Image: ansi.StylePrimitive{
+ Color: stringPtr(colors.Purple),
+ Underline: boolPtr(true),
+ },
+ ImageText: ansi.StylePrimitive{
+ Color: stringPtr(colors.LightGray),
+ Format: "Image: {{.text}} →",
+ },
+ Code: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: " ",
+ Suffix: " ",
+ Color: stringPtr(colors.Purple),
+ BackgroundColor: stringPtr(colors.BackgroundHighlight),
+ },
+ },
+ CodeBlock: ansi.StyleCodeBlock{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(colors.LightGray),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ Chroma: &ansi.Chroma{
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(colors.White),
+ },
+ Error: ansi.StylePrimitive{
+ Color: stringPtr(colors.White),
+ BackgroundColor: stringPtr(colors.Red),
+ },
+ Comment: ansi.StylePrimitive{
+ Color: stringPtr(colors.Gray),
+ },
+ CommentPreproc: ansi.StylePrimitive{
+ Color: stringPtr(colors.Orange),
+ },
+ Keyword: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ KeywordReserved: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ KeywordNamespace: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ KeywordType: ansi.StylePrimitive{
+ Color: stringPtr(colors.Turquoise),
+ },
+ Operator: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ Punctuation: ansi.StylePrimitive{
+ Color: stringPtr(colors.White),
+ },
+ Name: ansi.StylePrimitive{
+ Color: stringPtr(colors.Purple),
+ },
+ NameBuiltin: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ NameTag: ansi.StylePrimitive{
+ Color: stringPtr(colors.Purple),
+ },
+ NameAttribute: ansi.StylePrimitive{
+ Color: stringPtr(colors.LightBlue),
+ },
+ NameClass: ansi.StylePrimitive{
+ Color: stringPtr(colors.White),
+ Underline: boolPtr(true),
+ Bold: boolPtr(true),
+ },
+ NameDecorator: ansi.StylePrimitive{
+ Color: stringPtr(colors.Gold),
+ },
+ NameFunction: ansi.StylePrimitive{
+ Color: stringPtr(colors.LightBlue),
+ },
+ LiteralNumber: ansi.StylePrimitive{
+ Color: stringPtr(colors.Purple),
+ },
+ LiteralString: ansi.StylePrimitive{
+ Color: stringPtr(colors.Orange),
+ },
+ LiteralStringEscape: ansi.StylePrimitive{
+ Color: stringPtr(colors.Purple),
+ },
+ GenericDeleted: ansi.StylePrimitive{
+ Color: stringPtr(colors.Red),
+ },
+ GenericEmph: ansi.StylePrimitive{
+ Italic: boolPtr(true),
+ },
+ GenericInserted: ansi.StylePrimitive{
+ Color: stringPtr(colors.Turquoise),
+ },
+ GenericStrong: ansi.StylePrimitive{
+ Bold: boolPtr(true),
+ },
+ GenericSubheading: ansi.StylePrimitive{
+ Color: stringPtr(colors.LightGray),
+ },
+ Background: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(colors.BackgroundDim),
+ },
+ },
+ },
+ Table: ansi.StyleTable{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{},
+ },
+ },
+ DefinitionDescription: ansi.StylePrimitive{
+ BlockPrefix: "\n🠶 ",
+ },
+ }
+
+ return mdStyle
+}
+
+const (
+ defaultListIndent = 2
+ defaultListLevelIndent = 4
+ defaultMargin = 0
+)
+
+func stringPtr(c lipgloss.Color) *string { return (*string)(&c) }
+func boolPtr(b bool) *bool { return &b }
+func uintPtr(u uint) *uint { return &u }