refactor!: bubblezone inspired architecture

This commit is contained in:
decentral1se 2023-07-12 20:09:12 +02:00
parent 2355d6a053
commit 767ba3ad0b
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
5 changed files with 257 additions and 328 deletions

View File

@ -18,23 +18,7 @@ or [the docs](https://docs.cwtch.im/) are a great place to start.
## Feature set
> Initial goals: a reliable, usable & portable client which supports basic one-to-one chat.
- [ ] connectivity
- [x] ACN
- [ ] peers
- [ ] profile
- [x] create unencrypted profile
- [x] create encrypted profile
- [ ] unlock encrypted profile
- [ ] contacts
- [ ] invite contact
- [ ] accept invite
- [ ] messaging
- [ ] create conversation
> Coming Soon ™
## Prerequisites

176
internal/models/main.go Normal file
View File

@ -0,0 +1,176 @@
package models
import (
"crypto/rand"
"encoding/base64"
"fmt"
mrand "math/rand"
"os"
"path"
"path/filepath"
"strings"
"time"
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
type uiErrMsg struct{ Err error }
func (e uiErrMsg) Error() string {
return e.Err.Error()
}
type appInitialisedMsg struct {
app app.Application
acn connectivity.ACN
}
var mainStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("#F7B3DA"))
type mainModel struct {
username string
userDir string
width int
height int
app app.Application
acn connectivity.ACN
engineHooks connections.EngineHooks
titleModel titleModel
}
func NewMainModel(username, homeDir string) mainModel {
// TODO(d1): integrate from build system later
titleModel := newTitleModel("cairde", "v0.0.1")
return mainModel{
username: username,
userDir: path.Join(homeDir, "/.cairde/"),
titleModel: titleModel,
}
}
func initApp(m mainModel) 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 uiErrMsg{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 uiErrMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)}
}
if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil {
return uiErrMsg{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 uiErrMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)}
}
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 uiErrMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)}
}
if err := acn.WaitTillBootstrapped(); err != nil {
return uiErrMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)}
}
settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "")
if err != nil {
return uiErrMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)}
}
gSettings := settingsFile.ReadGlobalSettings()
gSettings.ExperimentsEnabled = false
gSettings.DownloadPath = "./"
settingsFile.WriteGlobalSettings(gSettings)
app := app.NewApp(acn, m.userDir, settingsFile)
app.InstallEngineHooks(m.engineHooks)
app.LoadProfiles("")
return appInitialisedMsg{
app: app,
acn: acn,
}
}
func (m mainModel) Init() tea.Cmd {
return tea.Batch(
func() tea.Msg { return initApp(m) },
)
}
func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width - 2
m.height = msg.Height - 2
msg.Height -= 1
msg.Width -= 4
return m.propagate(msg), nil
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
return m, tea.Quit
}
}
return m.propagate(msg), tea.Batch(cmds...)
}
func (m mainModel) propagate(msg tea.Msg) tea.Model {
m.titleModel, _ = m.titleModel.Update(msg)
return m
}
func (m mainModel) View() string {
body := strings.Builder{}
body.WriteString(
mainStyle.
Width(m.width).
Height(m.height).
Render(
m.titleModel.View(),
),
)
return body.String()
}

68
internal/models/title.go Normal file
View File

@ -0,0 +1,68 @@
package models
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var titleStyle = lipgloss.NewStyle().
Align(lipgloss.Center).
Bold(true)
var backgroundStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#EE00EE")).
Align(lipgloss.Center)
type titleModel struct {
programName string
programVersion string
title string
width int
height int
}
func newTitleModel(programName, programVersion string) titleModel {
return titleModel{
programName: programName,
programVersion: programVersion,
title: fmt.Sprintf("%s@%s", programName, programVersion),
height: 1,
}
}
func (m titleModel) Init() tea.Cmd {
return nil
}
func (m titleModel) Update(msg tea.Msg) (titleModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
}
return m, nil
}
func (m titleModel) View() string {
body := strings.Builder{}
body.WriteString(
lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
titleStyle.
Width(m.width).
Height(m.height).
Render(
backgroundStyle.
Width(lipgloss.Width(m.title)+2).
Render(m.title),
),
),
)
return body.String()
}

311
main.go
View File

@ -1,311 +0,0 @@
package main
import (
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"log"
mrand "math/rand"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strings"
"time"
"cwtch.im/cwtch/app"
"cwtch.im/cwtch/protocol/connections"
"cwtch.im/cwtch/settings"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor"
openPrivacyLog "git.openprivacy.ca/openprivacy/log"
"github.com/charmbracelet/bubbles/textinput"
_ "github.com/mutecomm/go-sqlcipher/v4"
"golang.org/x/term"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
centerViewportStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.NormalBorder()).
BorderForeground(lipgloss.Color("63")).
Padding(2)
leftViewportStyle = centerViewportStyle
)
type cmdWindow struct {
messages []string
}
func (c *cmdWindow) log(msg string) {
c.messages = append(c.messages, fmt.Sprintf("> %s", msg))
}
func (c *cmdWindow) view() string {
return strings.Join(c.messages, "\n")
}
// model offers the core of the state for the entire UI.
type model struct {
username string // Name of user
userDir string // Directory for user data
app app.Application // Cwtch application core
acn connectivity.ACN // Anonymous Communication Network networking abstraction
engineHooks connections.EngineHooks // Tor connectivity engine hooks
appInitialised bool
leftViewport viewport.Model
centerViewport viewport.Model
input textinput.Model
cmdWindow *cmdWindow
}
// initialModel constucts an initial state for the UI.
func initialModel(username, homeDir string, width, height int) model {
input := textinput.New()
input.Focus()
leftViewport := viewport.New(20, height/2)
centerViewport := viewport.New(width, height/2)
cmdWindow := cmdWindow{}
return model{
username: username,
userDir: path.Join(homeDir, "/.cairde/"),
leftViewport: leftViewport,
centerViewport: centerViewport,
input: input,
cmdWindow: &cmdWindow,
}
}
type uiErrMsg struct{ Err error }
func (e uiErrMsg) Error() string {
return e.Err.Error()
}
// appInitialisedMsg is a Bubbletea Message which signals that Cwtch internals
// (e.g. Tor, profiles, etc.) have been initialised.
type appInitialisedMsg struct {
app app.Application // The initialised Cwtch application
acn connectivity.ACN // Anonymous Communication Network networking abstraction
}
// initApp initialises Cwtch (e.g. Tor, profiles, etc.).
func initApp(m model) tea.Msg {
m.cmdWindow.log("attempting to initialise tor")
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 uiErrMsg{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 uiErrMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)}
}
if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil {
return uiErrMsg{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 uiErrMsg{Err: fmt.Errorf("unable to initialise torrc builder: %s", err)}
}
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 uiErrMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)}
}
m.cmdWindow.log("waiting for ACN to bootstrap...")
if err := acn.WaitTillBootstrapped(); err != nil {
return uiErrMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)}
}
m.cmdWindow.log("loading cwtch global settings")
settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "")
if err != nil {
return uiErrMsg{Err: fmt.Errorf("unable to initialise settings: %s", err)}
}
gSettings := settingsFile.ReadGlobalSettings()
gSettings.ExperimentsEnabled = false
gSettings.DownloadPath = "./"
settingsFile.WriteGlobalSettings(gSettings)
app := app.NewApp(acn, m.userDir, settingsFile)
app.InstallEngineHooks(m.engineHooks)
m.cmdWindow.log("loading unencrypted cwtch profiles")
app.LoadProfiles("")
return appInitialisedMsg{
app: app,
acn: acn,
}
}
// Init initialises the program.
func (m model) Init() tea.Cmd {
return tea.Batch(
func() tea.Msg { return initApp(m) },
textinput.Blink,
)
}
// Update updates the program state.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.leftViewport, cmd = m.leftViewport.Update(msg)
cmds = append(cmds, cmd)
m.centerViewport, cmd = m.centerViewport.Update(msg)
cmds = append(cmds, cmd)
m.input, cmd = m.input.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case uiErrMsg: // TODO
case tea.WindowSizeMsg: // TODO
case appInitialisedMsg:
m.app = msg.app
m.acn = msg.acn
m.appInitialised = true
m.cmdWindow.log("tor successfully initialised")
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c", "q":
if !m.appInitialised {
m.cmdWindow.log("still initialising... just a sec")
break
}
m.app.Shutdown()
m.acn.Close()
return m, tea.Quit
}
}
return m, tea.Batch(cmds...)
}
// View outputs the program state for viewing.
func (m model) View() string {
body := strings.Builder{}
m.centerViewport.SetContent(m.cmdWindow.view())
panes := lipgloss.JoinHorizontal(
lipgloss.Left,
leftViewportStyle.Render(m.leftViewport.View()),
centerViewportStyle.Render(m.centerViewport.View()),
)
body.WriteString(panes)
body.WriteString("\n" + m.input.View())
return body.String()
}
// help is the cairde CLI help output.
const help = `cairde [options]
A text-based user interface for metadata resistant online chat.
Options:
-h output help
`
// helpFlag is the help flag for the command-line interface.
var helpFlag bool
// main is the command-line entrypoint.
func main() {
flag.BoolVar(&helpFlag, "h", false, "output help")
flag.Parse()
if helpFlag {
fmt.Print(help)
os.Exit(0)
}
_, err := exec.LookPath("tor")
if err != nil {
log.Fatal("could not find 'tor' command, is it installed?")
}
f, err := tea.LogToFile("cairde.log", "debug")
if err != nil {
log.Fatal(err)
}
defer f.Close()
filelogger, err := openPrivacyLog.NewFile(openPrivacyLog.LevelInfo, "cwtch.log")
if err == nil {
openPrivacyLog.SetStd(filelogger)
}
user, err := user.Current()
if err != nil {
log.Fatalf("unable to determine current user: %s", err)
}
width, height, err := term.GetSize(0)
if err != nil {
log.Fatal(err)
}
p := tea.NewProgram(
initialModel(
user.Username,
user.HomeDir,
width,
height,
),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if err := p.Start(); err != nil {
log.Fatal(err)
}
}

12
makefile Normal file
View File

@ -0,0 +1,12 @@
.PHONY: clean build run
DEFAULT: run
clean:
@find -type f -name "*.log" -exec rm '{}' \;
build:
@go build -v ./cmd/cairde/
run: clean build
@./cairde