refactor: further breaking up of model code
This commit is contained in:
parent
cf93ff24d8
commit
119a65b9fe
|
@ -0,0 +1,119 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
mrand "math/rand"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"cwtch.im/cwtch/app"
|
||||||
|
"cwtch.im/cwtch/protocol/connections"
|
||||||
|
"cwtch.im/cwtch/settings"
|
||||||
|
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
offline = iota
|
||||||
|
connecting
|
||||||
|
connected
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureConnected(m model) (bool, tea.Msg) {
|
||||||
|
switch m.connState {
|
||||||
|
case offline:
|
||||||
|
return false, offlineMsg{}
|
||||||
|
case connecting:
|
||||||
|
return false, stillConnectingMsg{}
|
||||||
|
default:
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func attemptShutdown(m *model) tea.Msg {
|
||||||
|
if isConnected, msg := ensureConnected(*m); !isConnected {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
m.app.Shutdown()
|
||||||
|
m.acn.Close()
|
||||||
|
close(m.statusBuffer)
|
||||||
|
|
||||||
|
return shutdownMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func connInit(m *model) tea.Msg {
|
||||||
|
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
port := mrand.Intn(1000) + 9600
|
||||||
|
controlPort := port + 1
|
||||||
|
|
||||||
|
key := make([]byte, 64)
|
||||||
|
_, err := rand.Read(key)
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tor.NewTorrc().
|
||||||
|
WithSocksPort(port).
|
||||||
|
WithOnionTrafficOnly().
|
||||||
|
WithControlPort(controlPort).
|
||||||
|
WithHashedPassword(base64.StdEncoding.EncodeToString(key)).
|
||||||
|
Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.statusBuffer <- "initialising Tor ACN..."
|
||||||
|
|
||||||
|
acn, err := tor.NewTorACNWithAuth(
|
||||||
|
path.Join(m.userDir, "/.tor"),
|
||||||
|
"",
|
||||||
|
path.Join(m.userDir, "/.tor", "data"),
|
||||||
|
controlPort,
|
||||||
|
tor.HashedPasswordAuthenticator{
|
||||||
|
Password: base64.StdEncoding.EncodeToString(key),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.statusBuffer <- "waiting for Tor ACN to bootstrap..."
|
||||||
|
|
||||||
|
if err := acn.WaitTillBootstrapped(); err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)}
|
||||||
|
}
|
||||||
|
|
||||||
|
settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "")
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)}
|
||||||
|
}
|
||||||
|
gSettings := settingsFile.ReadGlobalSettings()
|
||||||
|
gSettings.ExperimentsEnabled = false
|
||||||
|
gSettings.DownloadPath = "./"
|
||||||
|
settingsFile.WriteGlobalSettings(gSettings)
|
||||||
|
|
||||||
|
m.statusBuffer <- "creating new Cwtch application"
|
||||||
|
|
||||||
|
app := app.NewApp(acn, m.userDir, settingsFile)
|
||||||
|
|
||||||
|
app.InstallEngineHooks(connections.DefaultEngineHooks{})
|
||||||
|
|
||||||
|
m.statusBuffer <- "the Tor ACN is up"
|
||||||
|
|
||||||
|
return appInitialisedMsg{
|
||||||
|
app: app,
|
||||||
|
acn: acn,
|
||||||
|
}
|
||||||
|
}
|
35
input.go
35
input.go
|
@ -56,6 +56,14 @@ func isClearCmd(cmds []string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommand(cmd, hiddenInput string) tea.Msg {
|
func handleCommand(cmd, hiddenInput string) tea.Msg {
|
||||||
|
if len(cmd) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(cmd[0]) != "/" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
cmds := strings.Split(cmd, " ")
|
cmds := strings.Split(cmd, " ")
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
@ -102,3 +110,30 @@ func handleCommand(cmd, hiddenInput string) tea.Msg {
|
||||||
return cmdMsg{output: "unknown command"}
|
return cmdMsg{output: "unknown command"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hidePasswordInput(m *model) {
|
||||||
|
val := m.input.Value()
|
||||||
|
|
||||||
|
if len(val) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(val[0]) != "/" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds := strings.Split(val[1:], " ")
|
||||||
|
if !(cmds[0] == "profile" && len(cmds) >= 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmds[1] == "create" && len(cmds) > 3 || cmds[1] == "unlock" && len(cmds) > 2 {
|
||||||
|
lastChar := string(val[len(val)-1])
|
||||||
|
m.hiddenInput += lastChar
|
||||||
|
if lastChar != " " {
|
||||||
|
newCmd := []rune(val)
|
||||||
|
newCmd[len(val)-1] = '*'
|
||||||
|
m.input.SetValue(string(newCmd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,10 @@ type cmdMsg struct {
|
||||||
output string
|
output string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type offlineMsg struct{}
|
||||||
|
|
||||||
|
type stillConnectingMsg struct{}
|
||||||
|
|
||||||
type shutdownMsg struct{}
|
type shutdownMsg struct{}
|
||||||
|
|
||||||
type createProfileMsg struct {
|
type createProfileMsg struct {
|
215
model.go
215
model.go
|
@ -1,40 +1,18 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
mrand "math/rand"
|
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"cwtch.im/cwtch/app"
|
"cwtch.im/cwtch/app"
|
||||||
"cwtch.im/cwtch/model/attr"
|
|
||||||
"cwtch.im/cwtch/model/constants"
|
|
||||||
"cwtch.im/cwtch/protocol/connections"
|
|
||||||
"cwtch.im/cwtch/settings"
|
|
||||||
"git.openprivacy.ca/openprivacy/connectivity"
|
"git.openprivacy.ca/openprivacy/connectivity"
|
||||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
"github.com/charmbracelet/bubbles/viewport"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
offline = iota
|
|
||||||
connecting
|
|
||||||
connected
|
|
||||||
)
|
|
||||||
|
|
||||||
type profile struct {
|
|
||||||
name string
|
|
||||||
onion string
|
|
||||||
}
|
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
username string
|
username string
|
||||||
userDir string
|
userDir string
|
||||||
|
@ -73,78 +51,6 @@ func newModel(username, homeDir string) model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) initApp() tea.Msg {
|
|
||||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
|
||||||
port := mrand.Intn(1000) + 9600
|
|
||||||
controlPort := port + 1
|
|
||||||
|
|
||||||
key := make([]byte, 64)
|
|
||||||
_, err := rand.Read(key)
|
|
||||||
if err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to generate control port password: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(path.Join(m.userDir, "/.tor", "tor"), 0700); err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to create profiles directory: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tor.NewTorrc().
|
|
||||||
WithSocksPort(port).
|
|
||||||
WithOnionTrafficOnly().
|
|
||||||
WithControlPort(controlPort).
|
|
||||||
WithHashedPassword(base64.StdEncoding.EncodeToString(key)).
|
|
||||||
Build(filepath.Join(m.userDir, ".tor", "tor", "torrc")); err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.statusBuffer <- "initialising Tor ACN..."
|
|
||||||
|
|
||||||
acn, err := tor.NewTorACNWithAuth(
|
|
||||||
path.Join(m.userDir, "/.tor"),
|
|
||||||
"",
|
|
||||||
path.Join(m.userDir, "/.tor", "data"),
|
|
||||||
controlPort,
|
|
||||||
tor.HashedPasswordAuthenticator{
|
|
||||||
Password: base64.StdEncoding.EncodeToString(key),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.statusBuffer <- "waiting for Tor ACN to bootstrap..."
|
|
||||||
|
|
||||||
if err := acn.WaitTillBootstrapped(); err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)}
|
|
||||||
}
|
|
||||||
|
|
||||||
settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "")
|
|
||||||
if err != nil {
|
|
||||||
return errMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)}
|
|
||||||
}
|
|
||||||
gSettings := settingsFile.ReadGlobalSettings()
|
|
||||||
gSettings.ExperimentsEnabled = false
|
|
||||||
gSettings.DownloadPath = "./"
|
|
||||||
settingsFile.WriteGlobalSettings(gSettings)
|
|
||||||
|
|
||||||
m.statusBuffer <- "creating new Cwtch application"
|
|
||||||
|
|
||||||
app := app.NewApp(acn, m.userDir, settingsFile)
|
|
||||||
|
|
||||||
app.InstallEngineHooks(connections.DefaultEngineHooks{})
|
|
||||||
|
|
||||||
m.statusBuffer <- "the Tor ACN is up"
|
|
||||||
|
|
||||||
return appInitialisedMsg{
|
|
||||||
app: app,
|
|
||||||
acn: acn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) handleStatusMessage() tea.Msg {
|
func (m model) handleStatusMessage() tea.Msg {
|
||||||
return statusMsg{msg: <-m.statusBuffer}
|
return statusMsg{msg: <-m.statusBuffer}
|
||||||
}
|
}
|
||||||
|
@ -156,38 +62,6 @@ func (m model) Init() tea.Cmd {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m model) isConnecting() bool {
|
|
||||||
if m.connState == connecting {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) isOffline() bool {
|
|
||||||
if m.connState == offline {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) shutdown() bool {
|
|
||||||
if m.isConnecting() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.isOffline() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
m.app.Shutdown()
|
|
||||||
m.acn.Close()
|
|
||||||
close(m.statusBuffer)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var (
|
var (
|
||||||
cmd tea.Cmd
|
cmd tea.Cmd
|
||||||
|
@ -212,50 +86,35 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.String() {
|
switch msg.String() {
|
||||||
case "ctrl+c":
|
case "ctrl+c":
|
||||||
if m.shutdown() {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
return m, tea.Quit
|
return attemptShutdown(&m)
|
||||||
}
|
})
|
||||||
|
|
||||||
case "enter":
|
case "enter":
|
||||||
val := m.input.Value()
|
val := m.input.Value()
|
||||||
if len(val) == 0 {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
break
|
return handleCommand(val[1:], m.hiddenInput)
|
||||||
}
|
})
|
||||||
|
|
||||||
if string(val[0]) == "/" {
|
|
||||||
cmds = append(cmds, func() tea.Msg {
|
|
||||||
return handleCommand(val[1:], m.hiddenInput)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
val := m.input.Value()
|
hidePasswordInput(&m)
|
||||||
if len(val) != 0 {
|
|
||||||
if string(val[0]) == "/" {
|
|
||||||
cmds := strings.Split(val[1:], " ")
|
|
||||||
if cmds[0] == "profile" && len(cmds) >= 2 {
|
|
||||||
if cmds[1] == "create" && len(cmds) > 3 || cmds[1] == "unlock" && len(cmds) > 2 {
|
|
||||||
lastChar := string(val[len(val)-1])
|
|
||||||
m.hiddenInput += lastChar
|
|
||||||
if lastChar != " " {
|
|
||||||
newCmd := []rune(val)
|
|
||||||
newCmd[len(val)-1] = '*'
|
|
||||||
m.input.SetValue(string(newCmd))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case cmdMsg:
|
case cmdMsg:
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
m.statusBuffer <- msg.output
|
m.statusBuffer <- msg.output
|
||||||
|
|
||||||
|
case offlineMsg:
|
||||||
|
m.input.Reset()
|
||||||
|
m.statusBuffer <- "conn: currently offline"
|
||||||
|
|
||||||
|
case stillConnectingMsg:
|
||||||
|
m.input.Reset()
|
||||||
|
m.statusBuffer <- "conn: initialising, please hold"
|
||||||
|
|
||||||
case shutdownMsg:
|
case shutdownMsg:
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
if m.shutdown() {
|
return m, tea.Quit
|
||||||
return m, tea.Quit
|
|
||||||
}
|
|
||||||
|
|
||||||
case createProfileMsg:
|
case createProfileMsg:
|
||||||
m.hiddenInput = ""
|
m.hiddenInput = ""
|
||||||
|
@ -274,44 +133,30 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.hiddenInput = ""
|
m.hiddenInput = ""
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
|
|
||||||
switch m.connState {
|
if isConnected, msg := ensureConnected(m); !isConnected {
|
||||||
case offline:
|
cmds = append(cmds, func() tea.Msg { return msg })
|
||||||
m.statusBuffer <- "offline, run /connect first?"
|
break
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
case connecting:
|
|
||||||
m.statusBuffer <- "still connecting, hold on a sec..."
|
|
||||||
return m, tea.Batch(cmds...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.app.LoadProfiles(msg.password)
|
profiles := unlockProfiles(m, msg.password)
|
||||||
|
if len(profiles) > 0 {
|
||||||
var unlocked []profile
|
m.statusBuffer <- fmt.Sprintf("unlocked profile(s): %s", profiles.names())
|
||||||
for _, onion := range m.app.ListProfiles() {
|
|
||||||
peer := m.app.GetPeer(onion)
|
|
||||||
name, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
|
||||||
unlocked = append(unlocked, profile{name: name, onion: onion})
|
|
||||||
}
|
}
|
||||||
|
m.profiles = profiles
|
||||||
if len(unlocked) > 0 {
|
|
||||||
var names []string
|
|
||||||
for _, p := range unlocked {
|
|
||||||
names = append(names, p.name)
|
|
||||||
}
|
|
||||||
m.statusBuffer <- fmt.Sprintf("unlocked profile(s): %s", strings.Join(names, " "))
|
|
||||||
}
|
|
||||||
|
|
||||||
m.profiles = unlocked
|
|
||||||
|
|
||||||
case profileInfoMsg:
|
case profileInfoMsg:
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
if m.connState != connected {
|
|
||||||
m.statusBuffer <- "offline, run /connect first?"
|
if isConnected, msg := ensureConnected(m); !isConnected {
|
||||||
|
cmds = append(cmds, func() tea.Msg { return msg })
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.profiles) == 0 {
|
if len(m.profiles) == 0 {
|
||||||
m.statusBuffer <- "no profiles loaded"
|
m.statusBuffer <- "no profiles loaded"
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := m.profiles[m.profileState]
|
profile := m.profiles[m.profileState]
|
||||||
m.statusBuffer <- fmt.Sprintf("profile onion: %s", profile.onion)
|
m.statusBuffer <- fmt.Sprintf("profile onion: %s", profile.onion)
|
||||||
|
|
||||||
|
@ -328,7 +173,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
case connectMsg:
|
case connectMsg:
|
||||||
m.input.Reset()
|
m.input.Reset()
|
||||||
cmds = append(cmds, m.initApp)
|
cmds = append(cmds, func() tea.Msg {
|
||||||
|
return connInit(&m)
|
||||||
|
})
|
||||||
|
|
||||||
case appInitialisedMsg:
|
case appInitialisedMsg:
|
||||||
m.app = msg.app
|
m.app = msg.app
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cwtch.im/cwtch/model/attr"
|
||||||
|
"cwtch.im/cwtch/model/constants"
|
||||||
|
)
|
||||||
|
|
||||||
|
type profile struct {
|
||||||
|
name string
|
||||||
|
onion string
|
||||||
|
}
|
||||||
|
|
||||||
|
type profiles []profile
|
||||||
|
|
||||||
|
func (ps profiles) names() []string {
|
||||||
|
var names []string
|
||||||
|
for _, profile := range ps {
|
||||||
|
names = append(names, profile.name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func unlockProfiles(m model, password string) profiles {
|
||||||
|
var unlocked []profile
|
||||||
|
|
||||||
|
m.app.LoadProfiles(password)
|
||||||
|
|
||||||
|
for _, onion := range m.app.ListProfiles() {
|
||||||
|
peer := m.app.GetPeer(onion)
|
||||||
|
name, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
|
||||||
|
unlocked = append(unlocked, profile{name: name, onion: onion})
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlocked
|
||||||
|
}
|
Reference in New Issue