zap-go

Repositório zap-go
git clone git://git.sivaldodavi.com/zap-go.git
Log | Files | Refs | README

commit 1d596be5caaac14a7e652ecba1e544150b17a8f6
parent 5e54e6ca2635d889639fb5cf9a2317278fc42b48
Author: Sivaldo <sivaldodavi@disroot.org>
Date:   Fri,  3 Apr 2026 18:57:15 -0300

Simplificação: receber mensagem via e-mail e mandar como antes

Diffstat:
M.gitignore | 5+----
DREADME.md | 38--------------------------------------
Mgo.mod | 37++++++++++---------------------------
Mgo.sum | 76++++++++++++++++++++--------------------------------------------------------
Mmain.go | 648+++++++++++++++++++++++--------------------------------------------------------
5 files changed, 218 insertions(+), 586 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,11 +1,8 @@ # Binários -zap-tui -whatsapp_client +zap # Dados sensíveis whatsapp.db -whatsapp.db-shm -whatsapp.db-wal # Logs e temporários /tmp/wtc_status diff --git a/README.md b/README.md @@ -1,38 +0,0 @@ - -### 1. Instalar -```bash -go mod tidy -go build -o zap-tui main.go - -``` - -### 2. Uso Global (Link Simbólico) - -```bash -ln -sf $(pwd)/zap-tui /usr/local/bin - -``` - -### 3. Comandos - -* **Enviar:** `zap-tui send` (abre FZF para escolher contato) -* **Receber:** `zap-tui receive` (monitora mensagens) - -### 4. Contatos - -O arquivo deve estar em: `~/.local/share/contatos` - -**Formato:** - -```csv -Nome,Telefone,Email -Alessandro,+5598xxxxxxxx, - -``` - -### 5. Atalhos - -* **Sair:** `Ctrl + C` -* **Enviar no chat:** `Enter` - -``` diff --git a/go.mod b/go.mod @@ -3,48 +3,31 @@ module wtc go 1.25.5 require ( - github.com/charmbracelet/bubbles v0.21.0 - github.com/charmbracelet/bubbletea v1.3.10 - github.com/charmbracelet/lipgloss v1.1.0 - github.com/mattn/go-sqlite3 v1.14.33 + github.com/BurntSushi/toml v1.6.0 + github.com/mattn/go-sqlite3 v1.14.40 github.com/mdp/qrterminal/v3 v3.2.1 - go.mau.fi/whatsmeow v0.0.0-20260116142645-06f473759141 + go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 google.golang.org/protobuf v1.36.11 ) require ( filippo.io/edwards25519 v1.1.0 // indirect - github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beeper/argo-go v1.1.2 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.10.1 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect github.com/coder/websocket v1.8.14 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect - github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/google/uuid v1.6.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect - github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.mau.fi/libsignal v0.2.1 // indirect - go.mau.fi/util v0.9.5 // indirect - golang.org/x/crypto v0.47.0 // indirect - golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + go.mau.fi/util v0.9.6 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect rsc.io/qr v0.2.0 // indirect ) diff --git a/go.sum b/go.sum @@ -1,31 +1,15 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -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/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= -github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= -github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= -github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= -github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= -github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -33,15 +17,11 @@ 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/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= -github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -49,28 +29,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmUG94= +github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4= github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= -github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= -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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= @@ -80,30 +47,27 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0= go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU= -go.mau.fi/util v0.9.5 h1:7AoWPCIZJGv4jvtFEuCe3GhAbI7uF9ckIooaXvwlIR4= -go.mau.fi/util v0.9.5/go.mod h1:g1uvZ03VQhtTt2BgaRGVytS/Zj67NV0YNIECch0sQCQ= -go.mau.fi/whatsmeow v0.0.0-20260116142645-06f473759141 h1:pa4WhVPKTubDgPnsza/UOKWP4eC1d8kLxNw69O/Npk8= -go.mau.fi/whatsmeow v0.0.0-20260116142645-06f473759141/go.mod h1:jDLOQLLiYXcm4vMB6vtPcBLU387sRY+P3vOElxX8srA= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= -golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts= +go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI= +go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4 h1:E4A6eca9vMJQctC9DIfzUIg27TrJ8IrDHgkJwJ8WPUQ= +go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/main.go b/main.go @@ -3,19 +3,18 @@ package main import ( "bufio" "context" - "encoding/json" + "crypto/tls" "fmt" + "net/smtp" "os" "os/exec" + "os/signal" "path/filepath" - "sort" "strings" + "syscall" "time" - "github.com/charmbracelet/bubbles/textinput" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + "github.com/BurntSushi/toml" "github.com/mdp/qrterminal/v3" _ "github.com/mattn/go-sqlite3" "go.mau.fi/whatsmeow" @@ -27,121 +26,119 @@ import ( "google.golang.org/protobuf/proto" ) -// ─── Estilos (aerc-like) ────────────────────────────────────────────────────── +// --- Config --- -var ( - styleHeader = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("15")). - Background(lipgloss.Color("238")). - Padding(0, 1) - - styleStatus = lipgloss.NewStyle(). - Foreground(lipgloss.Color("243")). - Background(lipgloss.Color("236")). - Padding(0, 1) - - styleDivider = lipgloss.NewStyle(). - Foreground(lipgloss.Color("240")) - - styleMyName = lipgloss.NewStyle(). - Foreground(lipgloss.Color("10")). - Bold(true) - - styleTheirName = lipgloss.NewStyle(). - Foreground(lipgloss.Color("12")). - Bold(true) - - styleTimestamp = lipgloss.NewStyle(). - Foreground(lipgloss.Color("240")) - - styleMyMsg = lipgloss.NewStyle(). - Foreground(lipgloss.Color("252")) - - styleTheirMsg = lipgloss.NewStyle(). - Foreground(lipgloss.Color("255")) - - styleInputPrompt = lipgloss.NewStyle(). - Foreground(lipgloss.Color("10")). - Bold(true) - - styleInputBar = lipgloss.NewStyle(). - BorderTop(true). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) +type SMTPConfig struct { + Host string `toml:"host"` + Port int `toml:"port"` + User string `toml:"user"` + Password string `toml:"password"` +} - styleUnreadBadge = lipgloss.NewStyle(). - Foreground(lipgloss.Color("0")). - Background(lipgloss.Color("9")). - Bold(true). - Padding(0, 1) -) +type Config struct { + SMTP SMTPConfig `toml:"smtp"` +} -// ─── Histórico persistente ──────────────────────────────────────────────────── +func loadConfig() Config { + exePath, _ := os.Executable() + realPath, _ := filepath.EvalSymlinks(exePath) + cfgPath := filepath.Join(filepath.Dir(realPath), "config.toml") -type storedMsg struct { - Sender string `json:"sender"` - Content string `json:"content"` - IsMe bool `json:"is_me"` - Time time.Time `json:"time"` + var cfg Config + if _, err := toml.DecodeFile(cfgPath, &cfg); err != nil { + fmt.Fprintln(os.Stderr, "erro ao ler config.toml:", err) + os.Exit(1) + } + return cfg } -type chatMetadata struct { - LastRead time.Time `json:"last_read"` -} +// --- Email --- -func dataDir() string { - dir := filepath.Join(os.Getenv("HOME"), ".local", "share", "zap-tui") - os.MkdirAll(dir, 0700) - return dir +func portugueseWeekday(d time.Weekday) string { + return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d] } -func historyPath(phone string) string { - return filepath.Join(dataDir(), phone+".json") -} +func sendEmailRaw(cfg Config, subject, body string) { + addr := fmt.Sprintf("%s:%d", cfg.SMTP.Host, cfg.SMTP.Port) + auth := smtp.PlainAuth("", cfg.SMTP.User, cfg.SMTP.Password, cfg.SMTP.Host) -func metadataPath(phone string) string { - return filepath.Join(dataDir(), phone+".meta.json") -} + msg := fmt.Sprintf( + "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s", + cfg.SMTP.User, cfg.SMTP.User, subject, body, + ) + + var err error + if cfg.SMTP.Port == 465 { + conn, e := tls.Dial("tcp", addr, &tls.Config{ServerName: cfg.SMTP.Host}) + if e != nil { + fmt.Fprintln(os.Stderr, "erro tls:", e) + return + } + c, e := smtp.NewClient(conn, cfg.SMTP.Host) + if e != nil { + fmt.Fprintln(os.Stderr, "erro smtp:", e) + return + } + defer c.Close() + c.Auth(auth) + c.Mail(cfg.SMTP.User) + c.Rcpt(cfg.SMTP.User) + w, _ := c.Data() + fmt.Fprint(w, msg) + err = w.Close() + } else { + err = smtp.SendMail(addr, auth, cfg.SMTP.User, []string{cfg.SMTP.User}, []byte(msg)) + } -func loadHistory(phone string) []storedMsg { - data, err := os.ReadFile(historyPath(phone)) if err != nil { - return nil + fmt.Fprintln(os.Stderr, "erro ao enviar email:", err) + } else { + fmt.Printf("[%s] email enviado: %s\n", time.Now().Format("15:04:05"), subject) } - var msgs []storedMsg - json.Unmarshal(data, &msgs) - return msgs } -func saveHistory(phone string, msgs []storedMsg) { - data, _ := json.Marshal(msgs) - os.WriteFile(historyPath(phone), data, 0600) -} +// --- WhatsApp --- + +func newClient() *whatsmeow.Client { + exePath, _ := os.Executable() + realPath, _ := filepath.EvalSymlinks(exePath) + dbPath := filepath.Join(filepath.Dir(realPath), "whatsapp.db") + dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", dbPath) -func loadMetadata(phone string) chatMetadata { - data, err := os.ReadFile(metadataPath(phone)) + container, err := sqlstore.New( + context.Background(), "sqlite3", dbURI, + waLog.Stdout("Database", "ERROR", true), + ) if err != nil { - return chatMetadata{} + fmt.Fprintln(os.Stderr, "erro db:", err) + os.Exit(1) } - var meta chatMetadata - json.Unmarshal(data, &meta) - return meta + device, _ := container.GetFirstDevice(context.Background()) + return whatsmeow.NewClient(device, waLog.Stdout("Client", "ERROR", true)) } -func saveMetadata(phone string, meta chatMetadata) { - data, _ := json.Marshal(meta) - os.WriteFile(metadataPath(phone), data, 0600) +func connectClient(client *whatsmeow.Client) { + if client.Store.ID == nil { + qrChan, _ := client.GetQRChannel(context.Background()) + client.Connect() + for evt := range qrChan { + if evt.Event == "code" { + qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) + } + } + } else { + client.Connect() + } } -// ─── Contatos (formato abook) ───────────────────────────────────────────────── +// --- Abook --- -type contact struct { - name string - mobile string +type Contact struct { + Name string + Mobile string } -func loadAbook() []contact { +func loadAbook() []Contact { path := filepath.Join(os.Getenv("HOME"), ".local", "share", "abook", "addressbook") f, err := os.Open(path) if err != nil { @@ -149,28 +146,28 @@ func loadAbook() []contact { } defer f.Close() - var contacts []contact - var cur contact + var contacts []Contact + var cur Contact scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "[") && line != "[format]" { - if cur.name != "" && cur.mobile != "" { + if cur.Name != "" && cur.Mobile != "" { contacts = append(contacts, cur) } - cur = contact{} + cur = Contact{} continue } if k, v, ok := strings.Cut(line, "="); ok { switch strings.TrimSpace(k) { case "name": - cur.name = strings.TrimSpace(v) + cur.Name = strings.TrimSpace(v) case "mobile": - cur.mobile = strings.TrimSpace(v) + cur.Mobile = strings.TrimSpace(v) } } } - if cur.name != "" && cur.mobile != "" { + if cur.Name != "" && cur.Mobile != "" { contacts = append(contacts, cur) } return contacts @@ -185,402 +182,131 @@ func cleanNumber(s string) string { }, s) } -// ─── Lista de Conversas ─────────────────────────────────────────────────────── - -type chatInfo struct { - contact contact - phone string - lastMsg time.Time - unreadCount int - preview string +func findName(jidUser string, contacts []Contact, pushName string) string { + clean := cleanNumber(jidUser) + if len(clean) >= 8 { + suffix := clean[len(clean)-8:] + for _, c := range contacts { + s := cleanNumber(c.Mobile) + if len(s) >= 8 && s[len(s)-8:] == suffix { + return c.Name + } + } + } + if pushName != "" { + return pushName + } + return jidUser } -func getChats(contacts []contact) []chatInfo { - var chats []chatInfo +// --- Comandos --- - for _, c := range contacts { - phone := cleanNumber(c.mobile) - history := loadHistory(phone) - if len(history) == 0 { - continue - } +func runDaemon() { + cfg := loadConfig() + contacts := loadAbook() + client := newClient() - meta := loadMetadata(phone) - lastMsg := history[len(history)-1] - - unread := 0 - preview := lastMsg.Content - if len(preview) > 50 { - preview = preview[:50] + "..." + client.AddEventHandler(func(evt interface{}) { + v, ok := evt.(*events.Message) + if !ok || v.Info.IsFromMe { + return } - for i := len(history) - 1; i >= 0; i-- { - if history[i].Time.After(meta.LastRead) && !history[i].IsMe { - unread++ - } + // Tenta pegar o texto (conversa simples ou estendida) + txt := "" + if v.Message.Conversation != nil { + txt = *v.Message.Conversation + } else if v.Message.ExtendedTextMessage != nil { + txt = *v.Message.ExtendedTextMessage.Text } - chats = append(chats, chatInfo{ - contact: c, - phone: phone, - lastMsg: lastMsg.Time, - unreadCount: unread, - preview: preview, - }) - } - - sort.Slice(chats, func(i, j int) bool { - return chats[i].lastMsg.After(chats[j].lastMsg) - }) + if txt == "" { + return + } - return chats -} + name := findName(v.Info.Sender.User, contacts, v.Info.PushName) + fmt.Printf("[%s] %s -> Enviando email...\n", time.Now().Format("15:04:05"), name) -func pickChatFzf(chats []chatInfo) (contact, bool) { - if len(chats) == 0 { - return contact{}, false - } + // Formata corpo do email + body := fmt.Sprintf("De: %s\nData: [%s %s]\n\nMensagem:\n%s", + name, + portugueseWeekday(v.Info.Timestamp.Weekday()), + v.Info.Timestamp.Format("02/01 15:04"), + txt, + ) - var lines []string - for _, c := range chats { - badge := "" - if c.unreadCount > 0 { - badge = fmt.Sprintf("[%d] ", c.unreadCount) - } - when := c.lastMsg.Format("02/01 15:04") - line := fmt.Sprintf("%s%s\t%s\t%s\t%s", - badge, c.contact.name, c.contact.mobile, when, c.preview) - lines = append(lines, line) - } + subject := fmt.Sprintf("WhatsApp de %s", name) + sendEmailRaw(cfg, subject, body) + }) - cmd := exec.Command("fzf", - "--prompt=Conversa: ", - "--delimiter=\t", - "--with-nth=1", - "--header=Enter para abrir Tab para novo Esc para sair", - "--bind=tab:abort", - ) - cmd.Stdin = strings.NewReader(strings.Join(lines, "\n")) - cmd.Stderr = os.Stderr - out, err := cmd.Output() - - if err != nil { - if err.Error() == "exit status 130" { // Tab pressed - return pickContactFzf(loadAbook()) - } - return contact{}, false - } + connectClient(client) + fmt.Println("daemon rodando (modo imediato). Ctrl+C para parar.") - parts := strings.SplitN(strings.TrimSpace(string(out)), "\t", 4) - if len(parts) < 2 { - return contact{}, false - } - - name := strings.TrimLeft(parts[0], "0123456789[] ") - mobile := parts[1] - - return contact{name: name, mobile: mobile}, true + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + client.Disconnect() } -func pickContactFzf(contacts []contact) (contact, bool) { - if len(contacts) == 0 { - return contact{}, false - } +func runSend() { + contacts := loadAbook() var lines []string for _, c := range contacts { - lines = append(lines, fmt.Sprintf("%s\t%s", c.name, c.mobile)) + lines = append(lines, fmt.Sprintf("%s\t%s", c.Name, c.Mobile)) } - - cmd := exec.Command("fzf", - "--prompt=Novo contato: ", - "--delimiter=\t", - "--with-nth=1", - "--header=Enter para abrir Esc para sair", - ) + cmd := exec.Command("fzf", "--prompt=para: ", "--delimiter=\t", "--with-nth=1") cmd.Stdin = strings.NewReader(strings.Join(lines, "\n")) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { - return contact{}, false + return } parts := strings.SplitN(strings.TrimSpace(string(out)), "\t", 2) if len(parts) < 2 { - return contact{}, false - } - return contact{name: parts[0], mobile: parts[1]}, true -} - -// ─── Modelo Bubble Tea ──────────────────────────────────────────────────────── - -type incomingMsg struct { - sender string - content string - isMe bool - ts time.Time -} - -type model struct { - client *whatsmeow.Client - target contact - phone string - history []storedMsg - viewport viewport.Model - textInput textinput.Model - width int - height int - ready bool -} - -func newModel(client *whatsmeow.Client, target contact) model { - phone := cleanNumber(target.mobile) - - ti := textinput.New() - ti.Placeholder = "mensagem..." - ti.Focus() - ti.CharLimit = 1000 - - // Marcar como lido ao abrir - meta := loadMetadata(phone) - meta.LastRead = time.Now() - saveMetadata(phone, meta) - - return model{ - client: client, - target: target, - phone: phone, - history: loadHistory(phone), - textInput: ti, - } -} - -func (m model) Init() tea.Cmd { - return textinput.Blink -} - -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - - switch msg := msg.(type) { - - case tea.WindowSizeMsg: - m.width = msg.Width - m.height = msg.Height - vpH := m.height - 4 - if vpH < 1 { - vpH = 1 - } - if !m.ready { - m.viewport = viewport.New(m.width, vpH) - m.ready = true - } else { - m.viewport.Width = m.width - m.viewport.Height = vpH - } - m.viewport.SetContent(m.renderHistory()) - m.viewport.GotoBottom() - - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlC: - saveHistory(m.phone, m.history) - // Atualizar última leitura ao sair - meta := loadMetadata(m.phone) - meta.LastRead = time.Now() - saveMetadata(m.phone, meta) - return m, tea.Quit - - case tea.KeyEnter: - text := strings.TrimSpace(m.textInput.Value()) - if text == "" { - break - } - jid, err := types.ParseJID(m.phone + "@s.whatsapp.net") - if err == nil { - go m.client.SendMessage(context.Background(), jid, &waE2E.Message{ - Conversation: proto.String(text), - }) - } - entry := storedMsg{Sender: "eu", Content: text, IsMe: true, Time: time.Now()} - m.history = append(m.history, entry) - saveHistory(m.phone, m.history) - m.textInput.Reset() - m.viewport.SetContent(m.renderHistory()) - m.viewport.GotoBottom() - } - - case incomingMsg: - entry := storedMsg{ - Sender: msg.sender, - Content: msg.content, - IsMe: msg.isMe, - Time: msg.ts, - } - m.history = append(m.history, entry) - saveHistory(m.phone, m.history) - m.viewport.SetContent(m.renderHistory()) - m.viewport.GotoBottom() - } - - var vpCmd tea.Cmd - m.viewport, vpCmd = m.viewport.Update(msg) - cmds = append(cmds, vpCmd) - - var tiCmd tea.Cmd - m.textInput, tiCmd = m.textInput.Update(msg) - cmds = append(cmds, tiCmd) - - return m, tea.Batch(cmds...) -} - -func (m model) renderHistory() string { - if len(m.history) == 0 { - return styleTimestamp.Render(" sem mensagens ainda") - } - - var sb strings.Builder - lastDate := "" - - for _, msg := range m.history { - date := msg.Time.Format("02 Jan 2006") - if date != lastDate { - lastDate = date - pad := (m.width - len(date) - 4) / 2 - if pad < 1 { - pad = 1 - } - line := strings.Repeat("─", pad) + " " + date + " " + strings.Repeat("─", pad) - sb.WriteString("\n" + styleDivider.Render(line) + "\n\n") - } - - ts := styleTimestamp.Render(msg.Time.Format("15:04")) - - if msg.IsMe { - name := styleMyName.Render("eu") - content := styleMyMsg.Render(msg.Content) - sb.WriteString(fmt.Sprintf(" %s %s %s\n", name, ts, content)) - } else { - name := styleTheirName.Render(msg.Sender) - content := styleTheirMsg.Render(msg.Content) - sb.WriteString(fmt.Sprintf(" %s %s %s\n", name, ts, content)) - } - } - return sb.String() -} - -func (m model) View() string { - if !m.ready { - return "" - } - - header := styleHeader.Width(m.width).Render( - fmt.Sprintf("zap ─ %s (%s)", m.target.name, m.target.mobile), - ) - - chat := m.viewport.View() - - prompt := styleInputPrompt.Render(" > ") - inputArea := styleInputBar.Width(m.width).Render(prompt + m.textInput.View()) - - status := styleStatus.Width(m.width).Render( - fmt.Sprintf(" %d mensagens │ PgUp/PgDn rolar │ Ctrl+C sair", len(m.history)), - ) - - return lipgloss.JoinVertical(lipgloss.Left, header, chat, inputArea, status) -} - -// ─── Main ───────────────────────────────────────────────────────────────────── - -func main() { - contacts := loadAbook() - chats := getChats(contacts) - - var chosen contact - var ok bool - - if len(chats) > 0 { - chosen, ok = pickChatFzf(chats) - } else { - chosen, ok = pickContactFzf(contacts) - } - - if !ok { return } + chosen := Contact{Name: parts[0], Mobile: parts[1]} + phone := cleanNumber(chosen.Mobile) - exePath, _ := os.Executable() - realPath, _ := filepath.EvalSymlinks(exePath) - dbPath := filepath.Join(filepath.Dir(realPath), "whatsapp.db") - dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", dbPath) - - container, err := sqlstore.New( - context.Background(), "sqlite3", dbURI, - waLog.Stdout("Database", "ERROR", true), - ) + jid, err := types.ParseJID(phone + "@s.whatsapp.net") if err != nil { - fmt.Fprintln(os.Stderr, "erro ao abrir db:", err) + fmt.Fprintln(os.Stderr, "numero invalido:", phone) os.Exit(1) } - device, _ := container.GetFirstDevice(context.Background()) - client := whatsmeow.NewClient(device, waLog.Stdout("Client", "ERROR", true)) - - m := newModel(client, chosen) - p := tea.NewProgram(m, tea.WithAltScreen()) - - phoneClean := cleanNumber(chosen.mobile) - - client.AddEventHandler(func(evt interface{}) { - v, ok := evt.(*events.Message) - if !ok { - return - } - - txt := v.Message.GetConversation() - if txt == "" { - txt = v.Message.GetExtendedTextMessage().GetText() - } - if txt == "" { - return - } - - senderClean := cleanNumber(v.Info.Sender.User) - mine := v.Info.IsFromMe + client := newClient() + connectClient(client) + defer client.Disconnect() - relevant := mine - if !mine && len(senderClean) >= 8 && len(phoneClean) >= 8 { - relevant = senderClean[len(senderClean)-8:] == phoneClean[len(phoneClean)-8:] + fmt.Printf("para: %s (%s)\n", chosen.Name, chosen.Mobile) + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print("> ") + if !scanner.Scan() { + break } - if !relevant { - return - } - - senderName := chosen.name - if mine { - senderName = "eu" + text := strings.TrimSpace(scanner.Text()) + if text == "" { + break } - - p.Send(incomingMsg{ - sender: senderName, - content: txt, - isMe: mine, - ts: v.Info.Timestamp, + client.SendMessage(context.Background(), jid, &waE2E.Message{ + Conversation: proto.String(text), }) - }) - - if client.Store.ID == nil { - qrChan, _ := client.GetQRChannel(context.Background()) - client.Connect() - for evt := range qrChan { - if evt.Event == "code" { - qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout) - } - } - } else { - client.Connect() } +} - if _, err := p.Run(); err != nil { +func main() { + if len(os.Args) < 2 { + fmt.Println("uso: zap <daemon|send>") + os.Exit(1) + } + switch os.Args[1] { + case "daemon": + runDaemon() + case "send": + runSend() + default: + fmt.Println("uso: zap <daemon|send>") os.Exit(1) } - client.Disconnect() }