init: beep boop
This commit is contained in:
commit
c138b09b84
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
cairde
|
||||
*.log
|
||||
*.work
|
15
LICENSE
Normal file
15
LICENSE
Normal 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
50
README.md
Normal 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
41
go.mod
Normal 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
95
go.sum
Normal 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
299
main.go
Normal 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
17
messages/messages.go
Normal 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
209
profile/add.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user