init: beep boop

This commit is contained in:
decentral1se 2023-06-12 13:19:58 +02:00
commit c138b09b84
Signed by: decentral1se
GPG Key ID: 03789458B3D0C410
8 changed files with 729 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
cairde
*.log
*.work

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
cairde: A text-based user interface for metadata resistant online chat.
Copyright (C) 2023 decentral1se
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# cairde
> Cairde /ˈkɑːɾˠdʲə/, an Irish word which translates as "friends"
A text-based user interface which uses the [Cwtch
protocol](https://docs.openprivacy.ca/cwtch-security-handbook/overview.html#what-is-cwtch)
for metadata resistant online chat.
This is a **pre-alpha prototype** and an *unofficial* community implementation
which is currently only ready for experimentation and testing. Please *do not*
use existing `cwtch-ui` profiles with `cairde`! We cannot guarantee that things
will not break at this early stage.
Please see [cwtch.im](https://cwtch.im/download/) for the official Cwtch GUI.
If you're new to Cwtch, this
[video](https://docs.openprivacy.ca/cwtch-security-handbook/overview.html#a-video-explainer)
or [the docs](https://docs.cwtch.im/) are a great place to start.
## Prerequisites
### Tor
On Debian systems, you can install with:
```
sudo apt install tor
```
## Hack
```
go build -v .
./cairde
```
## Contribute
It would be wonderful if you'd like to help out: code, design, thoughts,
testing, documentation or otherwise! Feel free to reach out to me on Cwtch:
`52u75tikfpxm7xgjcbuxmtmtojkh3wb32balgu7bem7aix3qrwubo7qd`. We're also lurking
on the [Release candidate testers
group](https://docs.cwtch.im/docs/contribute/testing#join-the-cwtch-release-candidate-testers-group).
## References
* [`sarah/cwtchbot`](https://git.openprivacy.ca/sarah/cwtchbot)
## License
<img src="https://www.gnu.org/graphics/gplv3-or-later.png" />

41
go.mod Normal file
View File

@ -0,0 +1,41 @@
module cairde
go 1.20
require (
cwtch.im/cwtch v0.20.8
git.openprivacy.ca/openprivacy/connectivity v1.10.0
git.openprivacy.ca/openprivacy/log v1.0.3
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.7.1
github.com/mutecomm/go-sqlcipher/v4 v4.4.2
)
require (
filippo.io/edwards25519 v1.0.0 // indirect
git.openprivacy.ca/cwtch.im/tapir v0.6.0 // indirect
git.openprivacy.ca/openprivacy/bine v0.0.5 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/gtank/merlin v0.1.1 // indirect
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

95
go.sum Normal file
View File

@ -0,0 +1,95 @@
cwtch.im/cwtch v0.20.8 h1:5N95p6uYquu9Vj2E9jK41FJpa679h1oYfiv/2jm5gbA=
cwtch.im/cwtch v0.20.8/go.mod h1:h8S7EgEM+8pE1k+XLB5jAFdIPlOzwoXEY0GH5mQye5A=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.6.0 h1:TtnKjxitkIDMM7Qn0n/u+mOHRLJzuQUYjYRu5n0/QFY=
git.openprivacy.ca/cwtch.im/tapir v0.6.0/go.mod h1:iQIq4y7N+DuP3CxyG66WNEC/d6vzh+wXvvOmelB+KoY=
git.openprivacy.ca/openprivacy/bine v0.0.5 h1:DJs5gqw3SkvLSgRDvroqJxZ7F+YsbxbBRg5t0rU5gYE=
git.openprivacy.ca/openprivacy/bine v0.0.5/go.mod h1:fwdeq6RO08WDkV0k7HfArsjRvurVULoUQmT//iaABZM=
git.openprivacy.ca/openprivacy/connectivity v1.10.0 h1:7P5xdFL4mGg9bwlIY+sdaNGUMazlMB82/v/6kXlvqxY=
git.openprivacy.ca/openprivacy/connectivity v1.10.0/go.mod h1:OQO1+7OIz/jLxDrorEMzvZA6SEbpbDyLGpjoFqT3z1Y=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY=
github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc=
github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY=
github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is=
github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c h1:gkfmnY4Rlt3VINCo4uKdpvngiibQyoENVj5Q88sxXhE=
github.com/gtank/ristretto255 v0.1.3-0.20210930101514-6bb39798585c/go.mod h1:tDPFhGdt3hJWqtKwx57i9baiB1Cj0yAg22VOPUqm5vY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk=
github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2 h1:eM10bFtI4UvibIsKr10/QT7Yfz+NADfjZYh0GKrXUNc=
github.com/mutecomm/go-sqlcipher/v4 v4.4.2/go.mod h1:mF2UmIpBnzFeBdu/ypTDb/LdbS0nk0dfSN1WUsWTjMA=
github.com/onsi/ginkgo/v2 v2.1.4 h1:GNapqRSid3zijZ9H77KrgVG4/8KqiyRsxcSxe+7ApXY=
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

299
main.go Normal file
View File

@ -0,0 +1,299 @@
package main
import (
"crypto/rand"
"encoding/base64"
"flag"
"fmt"
"log"
mrand "math/rand"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"cairde/messages"
"cairde/profile"
"strings"
"time"
"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/tor"
openPrivacyLog "git.openprivacy.ca/openprivacy/log"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/lipgloss"
_ "github.com/mutecomm/go-sqlcipher/v4"
tea "github.com/charmbracelet/bubbletea"
)
const (
initialising = iota
initialised
addProfile
)
// 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
err error // Application error
initSpinner spinner.Model // Spinner to show while initialising the app
app app.Application // Cwtch application core
acn connectivity.ACN // Anonymous Communication Network networking abstraction
engineHooks connections.EngineHooks // Tor connectivity engine hooks
peers []string // List of Cwtch peers
state int
prevState int
addProfile profile.ProfileAddModel
}
// initialModel constucts an initial state for the UI.
func initialModel(username, homeDir string) model {
initSpinner := spinner.New()
initSpinner.Spinner = spinner.Dot
initSpinner.Style = lipgloss.NewStyle().
Foreground(lipgloss.Color("205"))
return model{
username: username,
userDir: path.Join(homeDir, "/.cairde/"),
initSpinner: initSpinner,
state: initialising,
prevState: initialising,
addProfile: profile.InitialModel(),
}
}
// appInitialisedMsg is a Bubbletea Message which signals that Cwtch
// internals (e.g. Tor, profiles, etc.) have been initialised. The UI view can
// proceed now.
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 {
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 messages.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 messages.UIErrMsg{Err: fmt.Errorf("unable to create tor directory: %s", err)}
}
if err := os.MkdirAll(path.Join(m.userDir, "profiles"), 0700); err != nil {
return messages.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 messages.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 messages.UIErrMsg{Err: fmt.Errorf("unable to bootstrap tor: %s", err)}
}
if err := acn.WaitTillBootstrapped(); err != nil {
return messages.UIErrMsg{Err: fmt.Errorf("unable to initialise tor: %s", err)}
}
settingsFile, err := settings.InitGlobalSettingsFile(m.userDir, "")
if err != nil {
return messages.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,
}
}
// Init initialises the program.
func (m model) Init() tea.Cmd {
return tea.Batch(
func() tea.Msg { return initApp(m) },
m.initSpinner.Tick,
)
}
// Update updates the program state.
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
m.initSpinner, cmd = m.initSpinner.Update(msg)
cmds = append(cmds, cmd)
switch msg := msg.(type) {
case messages.ToPreviousStateMsg:
tmpPrevState := m.state
m.state = m.prevState
m.prevState = tmpPrevState
case messages.UIErrMsg:
m.err = msg
case appInitialisedMsg:
m.app = msg.app
m.acn = msg.acn
m.addProfile.App = msg.app
m.peers = m.app.ListProfiles()
m.state = initialised
m.prevState = initialised
case tea.KeyMsg:
switch msg.String() {
case "a":
m.state = addProfile
m.prevState = initialised
cmds = append(cmds, func() tea.Msg { return messages.ToAddProfileMsg{} })
case "ctrl+c", "q":
if m.state == initialising {
break // init before clean up
}
m.app.Shutdown()
m.acn.Close()
return m, tea.Quit
}
}
m.addProfile, cmd = m.addProfile.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
// View outputs the program state for viewing.
func (m model) View() string {
body := strings.Builder{}
if m.err != nil {
// TODO: show this error overlayed over current view
body.WriteString(fmt.Sprintf("error: %v\n", m.err))
return body.String()
}
switch m.state {
case initialising:
body.WriteString(fmt.Sprintf("Initialising... %s\n", m.initSpinner.View()))
case initialised:
body.WriteString("[a]add or [u]nlock profile\n")
if len(m.peers) > 0 {
body.WriteString("profiles:\n")
for _, onion := range m.peers {
peer := m.app.GetPeer(onion)
displayName, _ := peer.GetScopedZonedAttribute(attr.PublicScope, attr.ProfileZone, constants.Name)
body.WriteString(fmt.Sprintf("%s (%s)\n", onion, displayName))
}
}
case addProfile:
body.WriteString(m.addProfile.View())
}
return body.String()
}
// help is the cairde CLI help output.
const help = `cairde [options]
A text-based user interface for metadata resistant one-to-one chats.
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)
}
p := tea.NewProgram(
initialModel(
user.Username,
user.HomeDir,
),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if err := p.Start(); err != nil {
log.Fatal(err)
}
}

17
messages/messages.go Normal file
View File

@ -0,0 +1,17 @@
package messages
type ToAddProfileMsg struct{}
type ToPreviousStateMsg struct{}
type UIErrMsg struct{ Err error }
func (e UIErrMsg) Error() string {
return e.Err.Error()
}
type ProfileValidateErrMsg struct{ Err error }
func (e ProfileValidateErrMsg) Error() string {
return e.Err.Error()
}

209
profile/add.go Normal file
View File

@ -0,0 +1,209 @@
package profile
import (
"fmt"
"cairde/messages"
"cwtch.im/cwtch/app"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const (
hotPink = lipgloss.Color("#FF06B7")
)
var (
inputStyle = lipgloss.NewStyle().Foreground(hotPink)
)
const (
displayName = iota
encryptProfile
password
passwordAgain
)
type ProfileAddModel struct {
App app.Application
focus int
inputs []textinput.Model
err error
}
func (m *ProfileAddModel) nextInput() {
m.inputs[m.focus].Blur()
if m.focus < len(m.inputs)-1 {
m.focus++
} else {
m.focus = 0
}
m.inputs[m.focus].Focus()
}
func (m *ProfileAddModel) prevInput() {
m.inputs[m.focus].Blur()
if m.focus > 0 {
m.focus--
} else {
m.focus = len(m.inputs) - 1
}
m.inputs[m.focus].Focus()
}
func InitialModel() ProfileAddModel {
inputs := make([]textinput.Model, 4)
inputs[displayName] = textinput.New()
inputs[displayName].Placeholder = ""
inputs[displayName].CharLimit = 20
inputs[displayName].Width = 22
inputs[displayName].Prompt = ""
inputs[encryptProfile] = textinput.New()
inputs[encryptProfile].Placeholder = ""
inputs[encryptProfile].CharLimit = 1
inputs[encryptProfile].Width = 3
inputs[encryptProfile].Prompt = ""
inputs[password] = textinput.New()
inputs[password].Placeholder = ""
inputs[password].CharLimit = 30
inputs[password].Width = 32
inputs[password].Prompt = ""
inputs[password].EchoMode = textinput.EchoPassword
inputs[passwordAgain] = textinput.New()
inputs[passwordAgain].Placeholder = ""
inputs[passwordAgain].CharLimit = 30
inputs[passwordAgain].Width = 32
inputs[passwordAgain].Prompt = ""
inputs[passwordAgain].EchoMode = textinput.EchoPassword
return ProfileAddModel{
focus: displayName,
inputs: inputs,
}
}
func (m ProfileAddModel) Init() tea.Cmd {
return textinput.Blink
}
func (m ProfileAddModel) Update(msg tea.Msg) (ProfileAddModel, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
for idx, input := range m.inputs {
m.inputs[idx], cmd = input.Update(msg)
cmds = append(cmds, cmd)
}
switch msg := msg.(type) {
case messages.ToAddProfileMsg:
m.inputs[m.focus].Focus()
case messages.ProfileValidateErrMsg:
m.err = msg.Err
case tea.KeyMsg:
switch msg.String() {
case "tab", "enter":
m.nextInput()
case "down":
m.nextInput()
case "up", "shift+tab":
m.prevInput()
case "esc":
m.inputs[m.focus].Blur()
case "s":
if m.inputs[m.focus].Focused() {
break
}
var (
displayName = m.inputs[displayName].Value()
encryptProfile = m.inputs[encryptProfile].Value()
password = m.inputs[password].Value()
passwordAgain = m.inputs[passwordAgain].Value()
)
if displayName == "" {
return m, func() tea.Msg {
return messages.ProfileValidateErrMsg{Err: fmt.Errorf("display name is empty?")}
}
}
if encryptProfile == "y" {
if password != passwordAgain {
return m, func() tea.Msg {
return messages.ProfileValidateErrMsg{Err: fmt.Errorf("passwords do not match?")}
}
}
}
if m.err != nil {
m.err = nil
}
// TODO guard against not being initialised yet
m.App.CreateProfile(displayName, password, false)
cmds = append(cmds, func() tea.Msg { return messages.ToPreviousStateMsg{} })
case "c":
if m.inputs[m.focus].Focused() {
break
}
m.focus = displayName
for idx := range m.inputs {
m.inputs[idx].Reset()
m.inputs[idx].Blur()
}
cmds = append(cmds, func() tea.Msg { return messages.ToPreviousStateMsg{} })
}
}
return m, tea.Batch(cmds...)
}
func (m ProfileAddModel) View() string {
var view string
view += fmt.Sprintf(`%s
%s
%s
%s
%s
%s
%s
%s
%s
`, inputStyle.Width(30).Render("Display name"),
m.inputs[displayName].View(),
inputStyle.Width(30).Render("Encrypted? (y/n)"),
m.inputs[encryptProfile].View(),
inputStyle.Width(30).Render("Password"),
m.inputs[password].View(),
inputStyle.Width(30).Render("Password again"),
m.inputs[passwordAgain].View(),
"[s]ave | [c]ancel | [esc]: no input",
)
if m.err != nil {
view += fmt.Sprintf("error: %s\n", m.err)
}
return view
}