Compare commits

..

14 Commits

Author SHA1 Message Date
normen
16a6519ba7 add s key to help 2020-11-19 03:05:59 +01:00
normen
bb00a9a8f0 allow displaying images using jp2a 2020-11-19 03:04:22 +01:00
normen
4ef8b76a55 report full path on download 2020-11-19 02:59:01 +01:00
normen
6a832cbcf3 reduce data copying 2020-11-19 01:08:22 +01:00
normen
c2f3a012dc remove highlights when pressing <Esc> or o/d 2020-11-19 00:24:27 +01:00
normen
147feb90ec update README 2020-11-19 00:04:48 +01:00
normen
b39d4891e0 fix inverted logic for selecting messages 2020-11-19 00:02:21 +01:00
normen
19455b0baa add some code documentation 2020-11-18 23:58:28 +01:00
normen
a37e6c7d3b fix selection when highlight is not visible 2020-11-18 23:36:13 +01:00
normen
82d2a6637d allow navigating chat window selection 2020-11-18 23:30:02 +01:00
normen
1eb3af823e move session_manager to messages package 2020-11-18 22:53:00 +01:00
normen
773f9b783d add support for opening attachments 2020-11-18 22:39:53 +01:00
normen
e290a79064 add makefile for VIM 2020-11-18 13:23:49 +01:00
normen
7ae3967fbc improve contact handling, use short name in chat 2020-11-18 13:23:19 +01:00
9 changed files with 520 additions and 140 deletions

22
Makefile Normal file
View File

@@ -0,0 +1,22 @@
# Simple Makefile for go
build:
go build
clean:
go clean
run:
go run .
install:
go install .
get:
go get
update:
go get -u
release:
./release.sh

View File

@@ -32,8 +32,9 @@ A command line interface for whatsapp, based on [go-whatsapp](https://github.com
Things that work.
- Connects through the Web App API without a browser
- Allows sending and receiving WhatsApp messages in a command line app
- Connects through the Web App API without browser
- Allows downloading and opening image/video/audio/document attachments
- Uses QR code for simple setup
- Binaries for Windows, Mac, Linux and RaspBerry Pi
@@ -43,7 +44,6 @@ This is a WIP. Heres some things you might expect to work that don't. Plus some
- Only shows existing chats
- Only fetches a few old messages
- No support for images, videos, documents etc.
- No incoming message notification / count
- No proper connection drop handling
- Not configurable at all

8
go.mod
View File

@@ -3,16 +3,18 @@ module github.com/normen/whatscli
go 1.15
require (
github.com/Rhymen/go-whatsapp v0.1.1-0.20201117204225-79ad714fa46a
github.com/Rhymen/go-whatsapp v0.1.1
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
github.com/golang/protobuf v1.4.3 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/mattn/go-colorable v0.1.8
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/tview v0.0.0-20201117185959-f9f2182520da
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637 // indirect
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7 // indirect
golang.org/x/text v0.3.4 // indirect
google.golang.org/protobuf v1.25.0 // indirect
)

27
go.sum
View File

@@ -9,6 +9,8 @@ github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a h1:LW+rX0NY6L
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp v0.1.1-0.20201117204225-79ad714fa46a h1:IhYU0cQ0cvMO9MOuOO08X8zUtMQ+AVIV0D+7eZVtHe8=
github.com/Rhymen/go-whatsapp v0.1.1-0.20201117204225-79ad714fa46a/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp v0.1.1 h1:OK+bCugQcr2YjyYKeDzULqCtM50TPUFM6LvQtszKfcw=
github.com/Rhymen/go-whatsapp v0.1.1/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME=
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU=
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw=
@@ -27,6 +29,12 @@ github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDO
github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -46,6 +54,12 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
@@ -65,6 +79,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -74,6 +90,8 @@ github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 h1:9BBjVJTRxuYBeCAv9DFH
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743/go.mod h1:t7mcA3nlK9dxD1DMoz/DQRMWFMkGBUj6rJBM5VNfLFA=
github.com/rivo/tview v0.0.0-20201117185959-f9f2182520da h1:XUh+g7tjO81Ph5+7GSco4VLrBbQPnakm8M6sQU+fT5Y=
github.com/rivo/tview v0.0.0-20201117185959-f9f2182520da/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893 h1:24As98PZlIdjZn6V4wUulAbYlG7RPg/du9A1FZdT/vs=
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -82,6 +100,10 @@ github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTe
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -112,12 +134,17 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48 h1:AYCWBZhgIw6XobZ5CibNJr0Rc4ZofGGKvWa1vcx2IGk=
golang.org/x/sys v0.0.0-20201116194326-cc9327a14d48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637 h1:O5hKNaGxIT4A8OTMnuh6UpmBdI3SAPxlZ3g0olDrJVM=
golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7 h1:s330+6z/Ko3J0o6rvOcwXe5nzs7UT9tLKHoOXYn6uE0=
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7 h1:Z991aAXPjz0tLnj74pVXW3eWJ5lHMIBvbRfMq4M2jHA=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

385
main.go
View File

@@ -1,13 +1,17 @@
package main
import (
"bufio"
"fmt"
"io"
"os/exec"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/gdamore/tcell/v2"
"github.com/normen/whatscli/messages"
"github.com/rivo/tview"
"strings"
"time"
)
type waMsg struct {
@@ -15,21 +19,22 @@ type waMsg struct {
Text string
}
var VERSION string = "v0.5.2"
var VERSION string = "v0.6.3"
var sendChannel chan waMsg
var textChannel chan whatsapp.TextMessage
var otherChannel chan interface{}
var contactChannel chan whatsapp.Contact
var sndTxt string = ""
var currentReceiver string = ""
var curRegions []string
var textView *tview.TextView
var treeView *tview.TreeView
var textInput *tview.InputField
var topBar *tview.TextView
//var infoBar *tview.TextView
var connection *whatsapp.Conn
var msgStore messages.MessageDatabase
var contactRoot *tview.TreeNode
@@ -49,6 +54,7 @@ func main() {
topBar = tview.NewTextView()
topBar.SetDynamicColors(true)
topBar.SetScrollable(false)
topBar.SetText("[::b] WhatsCLI " + VERSION + " [-::d]Type /help for help")
//infoBar = tview.NewTextView()
@@ -63,6 +69,99 @@ func main() {
app.Draw()
})
textView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyCtrlE {
app.SetFocus(treeView)
return nil
}
if event.Key() == tcell.KeyCtrlSpace {
app.SetFocus(textInput)
return nil
}
if event.Key() == tcell.KeyTab {
app.SetFocus(textInput)
return nil
}
if event.Key() == tcell.KeyEsc {
textView.Highlight("")
return nil
}
if curRegions == nil || len(curRegions) == 0 {
return event
}
if event.Key() == tcell.KeyDown || event.Rune() == 'j' {
hls := textView.GetHighlights()
if len(hls) > 0 {
newId := GetOffsetMsgId(hls[0], 1)
if newId != "" {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[0])
}
textView.ScrollToHighlight()
return nil
}
if event.Key() == tcell.KeyUp || event.Rune() == 'k' {
hls := textView.GetHighlights()
if len(hls) > 0 {
newId := GetOffsetMsgId(hls[0], -1)
if newId != "" {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[len(curRegions)-1])
}
textView.ScrollToHighlight()
return nil
}
if event.Rune() == 'G' {
textView.Highlight(curRegions[len(curRegions)-1])
textView.ScrollToHighlight()
return nil
}
if event.Rune() == 'g' {
textView.Highlight(curRegions[0])
textView.ScrollToHighlight()
return nil
}
if event.Rune() == 'd' {
hls := textView.GetHighlights()
if len(hls) > 0 {
DownloadMessageId(hls[0], false)
textView.Highlight("")
}
return nil
}
if event.Rune() == 'o' {
hls := textView.GetHighlights()
if len(hls) > 0 {
DownloadMessageId(hls[0], true)
textView.Highlight("")
}
return nil
}
if event.Rune() == 'i' {
hls := textView.GetHighlights()
if len(hls) > 0 {
fmt.Fprintln(textView, msgStore.GetMessageInfo(hls[0]))
textView.Highlight("")
}
return nil
}
if event.Rune() == 's' {
hls := textView.GetHighlights()
if len(hls) > 0 {
go PrintImage(hls[0])
textView.Highlight("")
}
return nil
}
return event
})
// TODO: add better way
messages.SetTextView(textView)
PrintHelp()
//textView.SetBorder(true)
@@ -77,6 +176,10 @@ func main() {
app.SetFocus(treeView)
return nil
}
if event.Key() == tcell.KeyCtrlW {
app.SetFocus(textView)
return nil
}
if event.Key() == tcell.KeyTab {
app.SetFocus(treeView)
return nil
@@ -125,80 +228,6 @@ func main() {
app.Run()
}
// prints help to chat view
func PrintHelp() {
fmt.Fprintln(textView, "[::b]WhatsCLI "+VERSION+"\n\n[-]")
fmt.Fprintln(textView, "[-::u]Commands:[-::-]")
fmt.Fprintln(textView, "/name NewName = name selected contact")
fmt.Fprintln(textView, "/addname 1234567 NewName = add name for number")
fmt.Fprintln(textView, "/load = reload contacts")
fmt.Fprintln(textView, "/quit = exit app")
fmt.Fprintln(textView, "/help = show this help\n")
fmt.Fprintln(textView, "[-::u]Keys:[-::-]")
fmt.Fprintln(textView, "<Tab> = switch input/contacts")
fmt.Fprintln(textView, "<Up/Dn> = scroll history\n")
}
// called when text is entered by the user
func EnterCommand(key tcell.Key) {
if sndTxt == "" {
return
}
if key == tcell.KeyEsc {
textInput.SetText("")
return
}
if sndTxt == "/load" {
//command
LoadContacts()
textInput.SetText("")
return
}
if sndTxt == "/help" {
//command
PrintHelp()
textInput.SetText("")
return
}
if sndTxt == "/quit" {
//command
app.Stop()
return
}
if strings.Index(sndTxt, "/addname ") == 0 {
//command
parts := strings.Split(sndTxt, " ")
if len(parts) < 3 {
fmt.Fprintln(textView, "Use /addname 1234567 NewName")
return
}
messages.SetIdName(parts[1]+messages.CONTACTSUFFIX, strings.TrimPrefix(sndTxt, "/addname "+parts[1]+" "))
SetDisplayedContact(currentReceiver)
LoadContacts()
textInput.SetText("")
return
}
if currentReceiver == "" {
fmt.Fprintln(textView, "[red]no contact selected[-]")
return
}
if strings.Index(sndTxt, "/name ") == 0 {
//command
messages.SetIdName(currentReceiver, strings.TrimPrefix(sndTxt, "/name "))
SetDisplayedContact(currentReceiver)
LoadContacts()
textInput.SetText("")
return
}
// send message
msg := waMsg{
Wid: currentReceiver,
Text: sndTxt,
}
sendChannel <- msg
textInput.SetText("")
}
// creates the TreeView for contacts
func MakeTree() *tview.TreeView {
rootDir := "Contacts"
@@ -233,11 +262,141 @@ func MakeTree() *tview.TreeView {
app.SetFocus(textInput)
return nil
}
if event.Key() == tcell.KeyCtrlW {
app.SetFocus(textView)
return nil
}
return event
})
return treeView
}
// prints help to chat view
func PrintHelp() {
fmt.Fprintln(textView, "[::b]WhatsCLI "+VERSION+"[-]\n")
fmt.Fprintln(textView, "[-::u]Keys:[-::-]")
fmt.Fprintln(textView, "<Tab> = switch input/contacts")
fmt.Fprintln(textView, "<Up/Dn> = scroll history")
fmt.Fprintln(textView, "<Ctrl-W> = focus chat window\n")
fmt.Fprintln(textView, "[-::-]Chat window focused:[-::-]")
fmt.Fprintln(textView, "<Up/Down> = select message")
fmt.Fprintln(textView, "<d> = download attachment")
fmt.Fprintln(textView, "<o> = open attachment\n")
fmt.Fprintln(textView, "<s> = show image using jp2a\n")
fmt.Fprintln(textView, "[-::u]Commands:[-::-]")
fmt.Fprintln(textView, "/name NewName = name selected contact")
fmt.Fprintln(textView, "/addname 1234567 NewName = add name for number")
fmt.Fprintln(textView, "/connect = (re)connect in case the connection dropped")
fmt.Fprintln(textView, "/load = reload contacts")
fmt.Fprintln(textView, "/quit = exit app")
fmt.Fprintln(textView, "/help = show this help\n")
}
// called when text is entered by the user
func EnterCommand(key tcell.Key) {
if sndTxt == "" {
return
}
if key == tcell.KeyEsc {
textInput.SetText("")
return
}
if sndTxt == "/connect" {
//command
messages.GetConnection()
textInput.SetText("")
return
}
if sndTxt == "/load" {
//command
LoadContacts()
textInput.SetText("")
return
}
if sndTxt == "/help" {
//command
PrintHelp()
textInput.SetText("")
return
}
if sndTxt == "/quit" {
//command
app.Stop()
return
}
if strings.Index(sndTxt, "/addname ") == 0 {
//command
parts := strings.Split(sndTxt, " ")
if len(parts) < 3 {
fmt.Fprintln(textView, "Use /addname 1234567 NewName")
return
}
contact := whatsapp.Contact{
Jid: parts[1] + messages.CONTACTSUFFIX,
Name: strings.TrimPrefix(sndTxt, "/addname "+parts[1]+" "),
}
contactChannel <- contact
textInput.SetText("")
return
}
if currentReceiver == "" {
fmt.Fprintln(textView, "[red]no contact selected[-]")
return
}
if strings.Index(sndTxt, "/name ") == 0 {
//command
contact := whatsapp.Contact{
Jid: currentReceiver,
Name: strings.TrimPrefix(sndTxt, "/name "),
}
contactChannel <- contact
textInput.SetText("")
return
}
// send message
msg := waMsg{
Wid: currentReceiver,
Text: sndTxt,
}
sendChannel <- msg
textInput.SetText("")
}
// get the next message id to select (highlighted + offset)
func GetOffsetMsgId(curId string, offset int) string {
if curRegions == nil || len(curRegions) == 0 {
return ""
}
for idx, val := range curRegions {
if val == curId {
arrPos := idx + offset
if len(curRegions) > arrPos && arrPos >= 0 {
return curRegions[arrPos]
}
}
}
if offset > 0 {
return curRegions[0]
} else {
return curRegions[len(curRegions)-1]
}
}
func PrintImage(id string) {
if path, err := msgStore.DownloadMessage(id, false); err == nil {
cmd := exec.Command("jp2a", path)
stdout, err := cmd.StdoutPipe()
if nil != err {
fmt.Fprintln(textView, "Error getting pipe for jp2a command")
}
reader := bufio.NewReader(stdout)
if err := cmd.Start(); nil != err {
fmt.Fprintln(textView, "Error starting jp2a command")
}
io.Copy(tview.ANSIWriter(textView), reader)
}
}
// loads the contact data from storage to the TreeView
func LoadContacts() {
var ids = msgStore.GetContactIds()
@@ -263,13 +422,15 @@ func SetDisplayedContact(wid string) {
currentReceiver = wid
textView.Clear()
textView.SetTitle(messages.GetIdName(wid))
textView.SetText(msgStore.GetMessagesString(wid))
msgTxt, regIds := msgStore.GetMessagesString(wid)
textView.SetText(msgTxt)
curRegions = regIds
}
// starts the receiver and message handling thread
func StartTextReceiver() error {
var wac = GetConnection()
err := LoginWithConnection(wac)
var wac = messages.GetConnection()
err := messages.LoginWithConnection(wac)
if err != nil {
return fmt.Errorf("%v\n", err)
}
@@ -277,17 +438,21 @@ func StartTextReceiver() error {
wac.AddHandler(handler)
sendChannel = make(chan waMsg)
textChannel = make(chan whatsapp.TextMessage)
otherChannel = make(chan interface{})
contactChannel = make(chan whatsapp.Contact)
for {
select {
case msg := <-sendChannel:
SendText(msg.Wid, msg.Text)
case rcvd := <-textChannel:
if msgStore.AddTextMessage(rcvd) {
if msgStore.AddTextMessage(&rcvd) {
app.QueueUpdateDraw(LoadContacts)
}
case other := <-otherChannel:
msgStore.AddOtherMessage(&other)
case contact := <-contactChannel:
messages.SetIdName(contact.Jid, contact.Name)
app.QueueUpdateDraw(LoadContacts)
}
}
fmt.Fprintln(textView, "closing the receiver")
@@ -306,16 +471,43 @@ func SendText(wid string, text string) {
Text: text,
}
PrintTextMessage(msg)
_, err := connection.Send(msg)
msgStore.AddTextMessage(msg)
_, err := messages.GetConnection().Send(msg)
if err != nil {
fmt.Fprintln(textView, "[red]error sending message: ", err, "[-]")
} else {
//fmt.Fprint(textView, "Sent msg with ID: ", msgID, "\n")
msgStore.AddTextMessage(&msg)
PrintTextMessage(msg)
}
}
// initiates a download of a specific message attachment in a new go routine
func DownloadMessageId(id string, open bool) {
fmt.Fprintln(textView, "[::d]..attempt download of #", id, "[::-]")
go func() {
if result, err := msgStore.DownloadMessage(id, open); err == nil {
fmt.Fprintln(textView, "[::d]Downloaded as [yellow]", result, "[-::-]")
} else {
fmt.Fprintln(textView, "[red::d]", err.Error(), "[-::-]")
}
}()
}
// notifies about a new message if its recent
func NotifyMsg(msg whatsapp.TextMessage) {
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
//fmt.Print("\a")
//err := beeep.Notify(messages.GetIdName(msg.Info.RemoteJid), msg.Text, "")
//if err != nil {
// fmt.Fprintln(textView, "[red]error in notification[-]")
//}
}
}
// prints a text message to the TextView
func PrintTextMessage(msg whatsapp.TextMessage) {
fmt.Fprintln(textView, messages.GetTextMessageString(&msg))
}
// handler struct for whatsapp callbacks
type textHandler struct{}
@@ -330,17 +522,12 @@ func (t textHandler) HandleError(err error) {
func (t textHandler) HandleTextMessage(msg whatsapp.TextMessage) {
textChannel <- msg
if msg.Info.RemoteJid != currentReceiver {
//fmt.Print("\a")
NotifyMsg(msg)
return
}
PrintTextMessage(msg)
}
// prints a text message to the TextView
func PrintTextMessage(msg whatsapp.TextMessage) {
fmt.Fprintln(textView, messages.GetTextMessageString(&msg))
}
// methods to convert messages to TextMessage
func (t textHandler) HandleImageMessage(message whatsapp.ImageMessage) {
msg := whatsapp.TextMessage{
@@ -349,10 +536,12 @@ func (t textHandler) HandleImageMessage(message whatsapp.ImageMessage) {
SenderJid: message.Info.SenderJid,
FromMe: message.Info.FromMe,
Timestamp: message.Info.Timestamp,
Id: message.Info.Id,
},
Text: "[IMAGE] " + message.Caption,
}
t.HandleTextMessage(msg)
otherChannel <- message
}
func (t textHandler) HandleDocumentMessage(message whatsapp.DocumentMessage) {
@@ -362,10 +551,12 @@ func (t textHandler) HandleDocumentMessage(message whatsapp.DocumentMessage) {
SenderJid: message.Info.SenderJid,
FromMe: message.Info.FromMe,
Timestamp: message.Info.Timestamp,
Id: message.Info.Id,
},
Text: "[DOCUMENT] " + message.Title,
}
t.HandleTextMessage(msg)
otherChannel <- message
}
func (t textHandler) HandleVideoMessage(message whatsapp.VideoMessage) {
@@ -375,10 +566,12 @@ func (t textHandler) HandleVideoMessage(message whatsapp.VideoMessage) {
SenderJid: message.Info.SenderJid,
FromMe: message.Info.FromMe,
Timestamp: message.Info.Timestamp,
Id: message.Info.Id,
},
Text: "[VIDEO] " + message.Caption,
}
t.HandleTextMessage(msg)
otherChannel <- message
}
func (t textHandler) HandleAudioMessage(message whatsapp.AudioMessage) {
@@ -388,17 +581,21 @@ func (t textHandler) HandleAudioMessage(message whatsapp.AudioMessage) {
SenderJid: message.Info.SenderJid,
FromMe: message.Info.FromMe,
Timestamp: message.Info.Timestamp,
Id: message.Info.Id,
},
Text: "[AUDIO]",
}
t.HandleTextMessage(msg)
otherChannel <- message
}
// add contact info to database TODO: when are these sent??
// add contact info to database (not needed, internal db of connection is used)
func (t textHandler) HandleNewContact(contact whatsapp.Contact) {
contactChannel <- contact
// redundant, wac has contacts
//contactChannel <- contact
}
// handle battery messages
//func (t textHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
// app.QueueUpdate(func() {
// infoBar.SetText("🔋: " + string(msg.Percentage) + "%")

View File

@@ -2,19 +2,17 @@ package messages
import (
"encoding/gob"
"github.com/Rhymen/go-whatsapp"
"os"
"os/user"
"strings"
"github.com/Rhymen/go-whatsapp"
)
var contacts map[string]string
var connection *whatsapp.Conn
func SetConnection(conn *whatsapp.Conn) {
connection = conn
}
// loads custom contacts from disk
func LoadContacts() {
contacts = make(map[string]string)
file, err := os.Open(GetHomeDir() + ".whatscli.contacts")
@@ -29,6 +27,7 @@ func LoadContacts() {
}
}
// saves custom contacts to disk
func SaveContacts() {
file, err := os.Create(GetHomeDir() + ".whatscli.contacts")
if err != nil {
@@ -43,15 +42,31 @@ func SaveContacts() {
return
}
// sets a new name for a whatsapp id
func SetIdName(id string, name string) {
contacts[id] = name
SaveContacts()
}
// gets a pretty name for a whatsapp id
func GetIdName(id string) string {
if _, ok := contacts[id]; ok {
return contacts[id]
}
if val, ok := connection.Store.Contacts[id]; ok {
if val.Name != "" {
return val.Name
} else if val.Short != "" {
return val.Short
} else if val.Notify != "" {
return val.Notify
}
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// gets a short name for a whatsapp id
func GetIdShort(id string) string {
if val, ok := connection.Store.Contacts[id]; ok {
if val.Short != "" {
return val.Short
@@ -61,9 +76,13 @@ func GetIdName(id string) string {
return val.Notify
}
}
if _, ok := contacts[id]; ok {
return contacts[id]
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// gets the OS home dir with a path separator at the end
func GetHomeDir() string {
usr, err := user.Current()
if err != nil {

View File

@@ -1,18 +1,24 @@
package main
package messages
import (
"encoding/gob"
"fmt"
"github.com/rivo/tview"
"os"
"os/user"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/messages"
"github.com/normen/whatscli/qrcode"
)
var textView *tview.TextView
// TODO: remove this circular dependeny in favor of a better way
func SetTextView(tv *tview.TextView) {
textView = tv
}
// gets an existing connection or creates one
func GetConnection() *whatsapp.Conn {
var wac *whatsapp.Conn
if connection == nil {
@@ -26,7 +32,6 @@ func GetConnection() *whatsapp.Conn {
} else {
wac = connection
}
messages.SetConnection(wac)
return wac
}
@@ -78,13 +83,7 @@ func Logout() error {
return removeSession()
}
func GetHomeDir() string {
usr, err := user.Current()
if err != nil {
}
return usr.HomeDir + string(os.PathSeparator)
}
// reads the session file from disk
func readSession() (whatsapp.Session, error) {
session := whatsapp.Session{}
file, err := os.Open(GetHomeDir() + ".whatscli.session")
@@ -100,6 +99,7 @@ func readSession() (whatsapp.Session, error) {
return session, nil
}
// saves the session file to disk
func writeSession(session whatsapp.Session) error {
file, err := os.Create(GetHomeDir() + ".whatscli.session")
if err != nil {
@@ -114,6 +114,7 @@ func writeSession(session whatsapp.Session) error {
return nil
}
// deletes the session file from disk
func removeSession() error {
return os.Remove(GetHomeDir() + ".whatscli.session")
}

View File

@@ -1,33 +1,44 @@
package messages
import (
"github.com/Rhymen/go-whatsapp"
"github.com/rivo/tview"
"errors"
"io/ioutil"
"os"
"sort"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/rivo/tview"
"github.com/skratchdot/open-golang/open"
)
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
type MessageDatabase struct {
textMessages map[string][]whatsapp.TextMessage
latestMessage map[string]uint64
textMessages map[string][]*whatsapp.TextMessage // text messages stored by RemoteJid
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
latestMessage map[string]uint64 // last message from RemoteJid
otherMessages map[string]*interface{} // other non-text messages, stored by ID
}
// initialize the database
func (db *MessageDatabase) Init() {
//var this = *db
(*db).textMessages = make(map[string][]whatsapp.TextMessage)
(*db).textMessages = make(map[string][]*whatsapp.TextMessage)
(*db).messagesById = make(map[string]*whatsapp.TextMessage)
(*db).otherMessages = make(map[string]*interface{})
(*db).latestMessage = make(map[string]uint64)
}
func (db *MessageDatabase) AddTextMessage(msg whatsapp.TextMessage) bool {
// add a text message to the database, stored by RemoteJid
func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
//var this = *db
var didNew = false
var wid = msg.Info.RemoteJid
if (*db).textMessages[wid] == nil {
var newArr = []whatsapp.TextMessage{}
var newArr = []*whatsapp.TextMessage{}
(*db).textMessages[wid] = newArr
(*db).latestMessage[wid] = msg.Info.Timestamp
didNew = true
@@ -36,12 +47,33 @@ func (db *MessageDatabase) AddTextMessage(msg whatsapp.TextMessage) bool {
didNew = true
}
(*db).textMessages[wid] = append((*db).textMessages[wid], msg)
(*db).messagesById[msg.Info.Id] = msg
sort.Slice((*db).textMessages[wid], func(i, j int) bool {
return (*db).textMessages[wid][i].Info.Timestamp < (*db).textMessages[wid][j].Info.Timestamp
})
return didNew
}
// add audio/video/image/doc message, stored by message id
func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
var id = ""
switch v := (*msg).(type) {
default:
case whatsapp.ImageMessage:
id = v.Info.Id
case whatsapp.DocumentMessage:
id = v.Info.Id
case whatsapp.AudioMessage:
id = v.Info.Id
case whatsapp.VideoMessage:
id = v.Info.Id
}
if id != "" {
(*db).otherMessages[id] = msg
}
}
// get an array of all chat ids
func (db *MessageDatabase) GetContactIds() []string {
//var this = *db
keys := make([]string, len((*db).textMessages))
@@ -53,33 +85,111 @@ func (db *MessageDatabase) GetContactIds() []string {
sort.Slice(keys, func(i, j int) bool {
return (*db).latestMessage[keys[i]] > (*db).latestMessage[keys[j]]
})
//sort.Strings(keys)
return keys
}
func (db *MessageDatabase) GetMessagesString(wid string) string {
//var this = *db
var out = ""
for _, element := range (*db).textMessages[wid] {
out += GetTextMessageString(&element)
out += "\n"
func (db *MessageDatabase) GetMessageInfo(id string) string {
if _, ok := (*db).otherMessages[id]; ok {
return "[yellow]OtherMessage[-]"
}
out := ""
if msg, ok := (*db).messagesById[id]; ok {
out += "[yellow]ID: " + msg.Info.Id + "[-]\n"
out += "[yellow]PushName: " + msg.Info.PushName + "[-]\n"
out += "[yellow]RemoteJid: " + msg.Info.RemoteJid + "[-]\n"
out += "[yellow]SenderJid: " + msg.Info.SenderJid + "[-]\n"
out += "[yellow]Participant: " + msg.ContextInfo.Participant + "[-]\n"
out += "[yellow]QuotedMessageID: " + msg.ContextInfo.QuotedMessageID + "[-]\n"
}
return out
}
// get a string containing all messages for a chat by chat id
func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) {
//var this = *db
var out = ""
var arr = []string{}
for _, element := range (*db).textMessages[wid] {
out += GetTextMessageString(element)
out += "\n"
arr = append(arr, element.Info.Id)
}
return out, arr
}
// create a formatted string with regions based on message ID from a text message
func GetTextMessageString(msg *whatsapp.TextMessage) string {
out := ""
text := tview.Escape((*msg).Text)
tim := time.Unix(int64((*msg).Info.Timestamp), 0)
out += "[\""
out += (*msg).Info.Id
out += "\"]"
if (*msg).Info.FromMe { //msg from me
out += "[-::d](" + tim.Format("02-01-06 15:04:05") + ") [blue::b]Me: [-::-]" + text
} else if strings.Contains((*msg).Info.RemoteJid, GROUPSUFFIX) { // group msg
//(*msg).Info.SenderJid
userId := (*msg).Info.SenderJid
//userId := strings.Split(string((*msg).Info.RemoteJid), "-")[0] + CONTACTSUFFIX
out += "[-::d](" + tim.Format("02-01-06 15:04:05") + ") [green::b]" + GetIdName(userId) + ": [-::-]" + text
out += "[-::d](" + tim.Format("02-01-06 15:04:05") + ") [green::b]" + GetIdShort(userId) + ": [-::-]" + text
} else { // message from others
out += "[-::d](" + tim.Format("02-01-06 15:04:05") + ") [green::b]" + GetIdName((*msg).Info.RemoteJid) + ": [-::-]" + text
out += "[-::d](" + tim.Format("02-01-06 15:04:05") + ") [green::b]" + GetIdShort((*msg).Info.RemoteJid) + ": [-::-]" + text
}
out += "[\"\"]"
return out
}
// attempts to download a messages attachments, returns path or error message
func (db *MessageDatabase) DownloadMessage(wid string, open bool) (string, error) {
if msg, ok := (*db).otherMessages[wid]; ok {
var fileName string = ""
switch v := (*msg).(type) {
default:
case whatsapp.ImageMessage:
if data, err := v.Download(); err == nil {
fileName = v.Info.Id + "." + strings.TrimPrefix(v.Type, "image/")
path, err := saveAttachment(data, fileName, open)
return path, err
} else {
return fileName, err
}
case whatsapp.DocumentMessage:
if data, err := v.Download(); err == nil {
fileName = v.Info.Id + "." + strings.TrimPrefix(strings.TrimPrefix(v.Type, "application/"), "document/")
path, err := saveAttachment(data, fileName, open)
return path, err
} else {
return fileName, err
}
case whatsapp.AudioMessage:
if data, err := v.Download(); err == nil {
fileName = v.Info.Id + "." + strings.TrimPrefix(v.Type, "audio/")
path, err := saveAttachment(data, fileName, open)
return path, err
} else {
return fileName, err
}
case whatsapp.VideoMessage:
if data, err := v.Download(); err == nil {
fileName = v.Info.Id + "." + strings.TrimPrefix(v.Type, "video/")
path, err := saveAttachment(data, fileName, open)
return path, err
} else {
return fileName, err
}
}
}
return "", errors.New("No attachments found")
}
// helper to save an attachment and open it if specified
func saveAttachment(data []byte, fileName string, openIt bool) (string, error) {
path := GetHomeDir() + "Downloads" + string(os.PathSeparator) + fileName
err := ioutil.WriteFile(path, data, 0644)
if err == nil {
if openIt {
open.Run(path)
}
} else {
return path, err
}
return path, nil
}

View File

@@ -1,13 +1,15 @@
#!/bin/bash
set -e
if [ $# -eq 0 ]; then
echo "Usage: ./release.sh v1.0.0"
exit 0
VERSION=$(cat main.go|grep "VERSION string"| awk -v FS="(\")" '{print $2}')
else
VERSION=$1
fi
WINF=whatscli-$1-windows.zip
LINUXF=whatscli-$1-linux.zip
MACF=whatscli-$1-macos.zip
RASPIF=whatscli-$1-raspberrypi.zip
echo Releasing $VERSION
WINF=whatscli-$VERSION-windows.zip
LINUXF=whatscli-$VERSION-linux.zip
MACF=whatscli-$VERSION-macos.zip
RASPIF=whatscli-$VERSION-raspberrypi.zip
GOOS=darwin go build -o whatscli
zip $MACF whatscli
@@ -26,6 +28,6 @@ git pull
LASTTAG=$(git describe --tags --abbrev=0)
git log $LASTTAG..HEAD --no-decorate --pretty=format:"- %s" --abbrev-commit > changes.txt
vim changes.txt
gh release create $1 $LINUXF $MACF $WINF $RASPIF -F changes.txt -t $1
gh release create $VERSION $LINUXF $MACF $WINF $RASPIF -F changes.txt -t $VERSION
rm changes.txt
rm *.zip