initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.DS_Store
|
||||
whatscli
|
||||
whatscli.exe
|
||||
28
README.md
Normal file
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# whatscli
|
||||
|
||||
A command line interface for whatsapp, based on go-whatsapp and tview
|
||||
|
||||
## Features
|
||||
|
||||
Things that work.
|
||||
|
||||
- Allows sending and receiving WhatsApp messages in a CLI interface
|
||||
- Connects through Web App API without browser
|
||||
- Uses QR code for simple setup
|
||||
|
||||
### Caveats
|
||||
|
||||
Things you might expect to work that don't
|
||||
|
||||
- Only lists contacts that have been messaged on phone
|
||||
- Only fetches a few messages for last contacted
|
||||
- To display names they have to be entered through the `/name` command for each contact
|
||||
|
||||
## Installation / Usage
|
||||
|
||||
How to get it running and use it
|
||||
|
||||
- Put the binary in your PATH
|
||||
- Run with `whatscli`
|
||||
- Scan QR code with WhatsApp on phone
|
||||
|
||||
13
go.mod
Normal file
13
go.mod
Normal file
@@ -0,0 +1,13 @@
|
||||
module github.com/normen/whatscli
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
|
||||
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
|
||||
github.com/mattn/go-colorable v0.1.1
|
||||
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743
|
||||
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9
|
||||
)
|
||||
57
go.sum
Normal file
57
go.sum
Normal file
@@ -0,0 +1,57 @@
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII=
|
||||
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk=
|
||||
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
|
||||
github.com/Rhymen/go-whatsapp v0.1.0 h1:XTXhFIQ/fx9jKObUnUX2Q+nh58EyeHNhX7DniE8xeuA=
|
||||
github.com/Rhymen/go-whatsapp v0.1.0/go.mod h1:xJSy+okeRjKkQEH/lEYrnekXB3PG33fqL0I6ncAkV50=
|
||||
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a h1:LW+rX0NY6LzMPa2hJcgmQlfiFJUihzOMAaIoCq+P3xc=
|
||||
github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a/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=
|
||||
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
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=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 h1:9BBjVJTRxuYBeCAv9DFH2hSzY0ujLx5sxMg5D3K/Xeg=
|
||||
github.com/rivo/tview v0.0.0-20201018122409-d551c850a743/go.mod h1:t7mcA3nlK9dxD1DMoz/DQRMWFMkGBUj6rJBM5VNfLFA=
|
||||
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/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE=
|
||||
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
288
main.go
Normal file
288
main.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/normen/whatscli/messages"
|
||||
"github.com/rivo/tview"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type textHandler struct{}
|
||||
type waMsg struct {
|
||||
Wid string
|
||||
Text string
|
||||
}
|
||||
|
||||
const CONTACTSUFFIX = "@s.whatsapp.net"
|
||||
|
||||
var sendChannel chan waMsg
|
||||
var textChannel chan whatsapp.TextMessage
|
||||
|
||||
var sndTxt string = ""
|
||||
var currentReceiver string = ""
|
||||
var textView *tview.TextView
|
||||
var treeView *tview.TreeView
|
||||
var textInput *tview.InputField
|
||||
var topBar *tview.TextView
|
||||
var connection *whatsapp.Conn
|
||||
var msgStore messages.MessageDatabase
|
||||
|
||||
var contactRoot *tview.TreeNode
|
||||
var handler textHandler
|
||||
var app *tview.Application
|
||||
|
||||
//var messages map[string]string
|
||||
|
||||
func main() {
|
||||
msgStore = messages.MessageDatabase{}
|
||||
msgStore.Init()
|
||||
messages.LoadContacts()
|
||||
//messages.SetIdName("491732457387"+CONTACTSUFFIX, "Normen")
|
||||
//messages.SetIdName("4917622723621"+CONTACTSUFFIX, "Lilou")
|
||||
app = tview.NewApplication()
|
||||
gridLayout := tview.NewGrid()
|
||||
gridLayout.SetRows(1, 0, 1)
|
||||
gridLayout.SetColumns(30, 0, 30)
|
||||
gridLayout.SetBorders(true)
|
||||
|
||||
//list := tview.NewList()
|
||||
////list.SetTitle("Contacts")
|
||||
////list.AddItem("List Contacts", "get the contacts", 'a', func() {
|
||||
//// list.Clear()
|
||||
//// var ids = msgStore.GetContactIds()
|
||||
//// for _, element := range ids {
|
||||
//// //fmt.Fprint(textView, "\n"+element)
|
||||
//// var elem = element
|
||||
//// list.AddItem(messages.GetIdName(element), "", '-', func() {
|
||||
//// currentReceiver = elem
|
||||
//// textView.Clear()
|
||||
//// textView.SetText(msgStore.GetMessagesString(elem))
|
||||
//// fmt.Fprint(textView, "\nNeuer Empfänger: ", elem)
|
||||
//// })
|
||||
//// }
|
||||
////})
|
||||
//list.ShowSecondaryText(false)
|
||||
//list.AddItem("Load", "Load Contacts", 'l', LoadContacts)
|
||||
//list.AddItem("Quit", "Press to exit", 'q', func() {
|
||||
// app.Stop()
|
||||
//})
|
||||
|
||||
topBar = tview.NewTextView()
|
||||
topBar.SetDynamicColors(true)
|
||||
topBar.SetText("[::b] WhatsCLI v0.1.0 [-][::d]Help: /name [Name] = name contact | /quit = exit app | /load = reload contacts | <Tab> = switch input/tree | <Pgup/dn> = scroll history")
|
||||
|
||||
textView = tview.NewTextView().
|
||||
SetDynamicColors(true).
|
||||
SetRegions(true).
|
||||
SetWordWrap(true).
|
||||
SetChangedFunc(func() {
|
||||
app.Draw()
|
||||
})
|
||||
|
||||
//textView.SetBorder(true)
|
||||
|
||||
textInput = tview.NewInputField()
|
||||
textInput.SetChangedFunc(func(change string) {
|
||||
sndTxt = change
|
||||
})
|
||||
textInput.SetDoneFunc(EnterCommand)
|
||||
textInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyTab {
|
||||
app.SetFocus(treeView)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyPgDn {
|
||||
offset, _ := textView.GetScrollOffset()
|
||||
offset += 10
|
||||
textView.ScrollTo(offset, 0)
|
||||
return nil
|
||||
}
|
||||
if event.Key() == tcell.KeyPgUp {
|
||||
offset, _ := textView.GetScrollOffset()
|
||||
offset -= 10
|
||||
textView.ScrollTo(offset, 0)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
|
||||
gridLayout.AddItem(topBar, 0, 0, 1, 4, 0, 0, false)
|
||||
gridLayout.AddItem(MakeTree(), 1, 0, 2, 1, 0, 0, false)
|
||||
gridLayout.AddItem(textView, 1, 1, 1, 3, 0, 0, false)
|
||||
gridLayout.AddItem(textInput, 2, 1, 1, 3, 0, 0, false)
|
||||
|
||||
app.SetRoot(gridLayout, true)
|
||||
app.EnableMouse(true)
|
||||
app.SetFocus(textInput)
|
||||
go func() {
|
||||
if err := StartTextReceiver(); err != nil {
|
||||
fmt.Fprint(textView, err)
|
||||
}
|
||||
}()
|
||||
app.Run()
|
||||
}
|
||||
|
||||
func EnterCommand(key tcell.Key) {
|
||||
if sndTxt == "" {
|
||||
return
|
||||
}
|
||||
if sndTxt == "/load" {
|
||||
//command
|
||||
LoadContacts()
|
||||
textInput.SetText("")
|
||||
return
|
||||
}
|
||||
if sndTxt == "/quit" {
|
||||
//command
|
||||
app.Stop()
|
||||
return
|
||||
}
|
||||
if currentReceiver == "" {
|
||||
fmt.Fprint(textView, "\nNo recipient set")
|
||||
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("")
|
||||
}
|
||||
|
||||
func MakeTree() *tview.TreeView {
|
||||
rootDir := "Contacts"
|
||||
contactRoot = tview.NewTreeNode(rootDir).
|
||||
SetColor(tcell.ColorRed)
|
||||
treeView = tview.NewTreeView().
|
||||
SetRoot(contactRoot).
|
||||
SetCurrentNode(contactRoot)
|
||||
|
||||
// If a contact was selected, open it.
|
||||
treeView.SetChangedFunc(func(node *tview.TreeNode) {
|
||||
reference := node.GetReference()
|
||||
if reference == nil {
|
||||
return // Selecting the root node does nothing.
|
||||
}
|
||||
children := node.GetChildren()
|
||||
if len(children) == 0 {
|
||||
// Load and show files in this directory.
|
||||
recv := reference.(string)
|
||||
SetDisplayedContact(recv)
|
||||
} else {
|
||||
// Collapse if visible, expand if collapsed.
|
||||
node.SetExpanded(!node.IsExpanded())
|
||||
}
|
||||
})
|
||||
treeView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
if event.Key() == tcell.KeyTab {
|
||||
app.SetFocus(textInput)
|
||||
return nil
|
||||
}
|
||||
return event
|
||||
})
|
||||
return treeView
|
||||
}
|
||||
|
||||
func LoadContacts() {
|
||||
var ids = msgStore.GetContactIds()
|
||||
contactRoot.ClearChildren()
|
||||
for _, element := range ids {
|
||||
node := tview.NewTreeNode(messages.GetIdName(element)).
|
||||
SetReference(element).
|
||||
SetSelectable(true)
|
||||
if strings.Count(element, CONTACTSUFFIX) > 0 {
|
||||
node.SetColor(tcell.ColorGreen)
|
||||
} else {
|
||||
node.SetColor(tcell.ColorBlue)
|
||||
}
|
||||
contactRoot.AddChild(node)
|
||||
}
|
||||
}
|
||||
|
||||
func SetDisplayedContact(wid string) {
|
||||
currentReceiver = wid
|
||||
textView.Clear()
|
||||
textView.SetTitle(messages.GetIdName(wid))
|
||||
textView.SetText(msgStore.GetMessagesString(wid))
|
||||
}
|
||||
|
||||
// HandleError implements the handler interface for go-whatsapp
|
||||
func (t textHandler) HandleError(err error) {
|
||||
// TODO : handle go routine here
|
||||
fmt.Fprint(textView, "\nerror in textHandler : %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// HandleTextMessage implements the text message handler interface for go-whatsapp
|
||||
func (t textHandler) HandleTextMessage(msg whatsapp.TextMessage) {
|
||||
textChannel <- msg
|
||||
if msg.Info.RemoteJid != currentReceiver {
|
||||
return
|
||||
}
|
||||
PrintTextMessage(msg)
|
||||
}
|
||||
|
||||
func PrintTextMessage(msg whatsapp.TextMessage) {
|
||||
fmt.Fprint(textView, messages.GetTextMessageString(&msg))
|
||||
}
|
||||
|
||||
// StartTextReceiver starts the handler for the text messages received
|
||||
func StartTextReceiver() error {
|
||||
var wac = GetConnection()
|
||||
err := LoginWithConnection(wac)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v\n", err)
|
||||
}
|
||||
handler = textHandler{}
|
||||
wac.AddHandler(handler)
|
||||
sendChannel = make(chan waMsg)
|
||||
textChannel = make(chan whatsapp.TextMessage)
|
||||
for {
|
||||
select {
|
||||
case msg := <-sendChannel:
|
||||
SendText(msg.Wid, msg.Text)
|
||||
case rcvd := <-textChannel:
|
||||
if msgStore.AddTextMessage(rcvd) {
|
||||
app.QueueUpdateDraw(LoadContacts)
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Fprint(textView, "\n"+"closing the receiver")
|
||||
wac.Disconnect()
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendText(wid string, text string) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: wid,
|
||||
FromMe: true,
|
||||
Timestamp: uint64(time.Now().Unix()),
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
|
||||
PrintTextMessage(msg)
|
||||
//TODO: workaround for error when receiving&sending
|
||||
connection.RemoveHandlers()
|
||||
_, err := connection.Send(msg)
|
||||
msgStore.AddTextMessage(msg)
|
||||
connection.AddHandler(handler)
|
||||
if err != nil {
|
||||
fmt.Fprint(textView, "\nerror sending message: %v", err)
|
||||
} else {
|
||||
//fmt.Fprint(textView, "\nSent msg with ID: %v", msgID)
|
||||
}
|
||||
}
|
||||
56
messages/contacts.go
Normal file
56
messages/contacts.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"os/user"
|
||||
)
|
||||
|
||||
var contacts map[string]string
|
||||
|
||||
func LoadContacts() {
|
||||
contacts = make(map[string]string)
|
||||
file, err := os.Open(GetHomeDir() + ".whatscli.contacts")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&contacts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func SaveContacts() {
|
||||
file, err := os.Create(GetHomeDir() + ".whatscli.contacts")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
encoder := gob.NewEncoder(file)
|
||||
err = encoder.Encode(contacts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func SetIdName(id string, name string) {
|
||||
contacts[id] = name
|
||||
SaveContacts()
|
||||
}
|
||||
|
||||
func GetIdName(id string) string {
|
||||
if _, ok := contacts[id]; ok {
|
||||
return contacts[id]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func GetHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
}
|
||||
return usr.HomeDir + string(os.PathSeparator)
|
||||
}
|
||||
72
messages/storage.go
Normal file
72
messages/storage.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MessageDatabase struct {
|
||||
textMessages map[string][]whatsapp.TextMessage
|
||||
latestMessage map[string]uint64
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) Init() {
|
||||
//var this = *db
|
||||
(*db).textMessages = make(map[string][]whatsapp.TextMessage)
|
||||
(*db).latestMessage = make(map[string]uint64)
|
||||
}
|
||||
|
||||
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{}
|
||||
(*db).textMessages[wid] = newArr
|
||||
(*db).latestMessage[wid] = msg.Info.Timestamp
|
||||
didNew = true
|
||||
} else if (*db).latestMessage[wid] < msg.Info.Timestamp {
|
||||
(*db).latestMessage[wid] = msg.Info.Timestamp
|
||||
}
|
||||
(*db).textMessages[wid] = append((*db).textMessages[wid], 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
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) GetContactIds() []string {
|
||||
//var this = *db
|
||||
keys := make([]string, len((*db).textMessages))
|
||||
i := 0
|
||||
for k := range (*db).textMessages {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
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)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func GetTextMessageString(msg *whatsapp.TextMessage) string {
|
||||
var out = ""
|
||||
tim := time.Unix(int64((*msg).Info.Timestamp), 0)
|
||||
if (*msg).Info.FromMe {
|
||||
out += "\n[-](" + tim.Format("01-02-06 15:04:05") + ") [blue]Ich: " + (*msg).Text
|
||||
} else {
|
||||
out += "\n[-](" + tim.Format("01-02-06 15:04:05") + ") [green]" + GetIdName((*msg).Info.RemoteJid) + ": " + (*msg).Text
|
||||
}
|
||||
return out
|
||||
}
|
||||
191
qrcode/qrcode.go
Normal file
191
qrcode/qrcode.go
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
*BSD 3-Clause License
|
||||
*
|
||||
*Copyright (c) 2017, Baozisoftware
|
||||
*All rights reserved.
|
||||
*
|
||||
*Redistribution and use in source and binary forms, with or without
|
||||
*modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
** Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
** Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
** Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
*THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
*AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
*IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
*DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
*FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
*DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
*SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
*CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
*OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
*OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
package qrcode
|
||||
|
||||
import (
|
||||
nbytes "bytes"
|
||||
"fmt"
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"image/png"
|
||||
"io"
|
||||
)
|
||||
|
||||
type consoleColor string
|
||||
type consoleColors struct {
|
||||
NormalBlack consoleColor
|
||||
NormalRed consoleColor
|
||||
NormalGreen consoleColor
|
||||
NormalYellow consoleColor
|
||||
NormalBlue consoleColor
|
||||
NormalMagenta consoleColor
|
||||
NormalCyan consoleColor
|
||||
NormalWhite consoleColor
|
||||
BrightBlack consoleColor
|
||||
BrightRed consoleColor
|
||||
BrightGreen consoleColor
|
||||
BrightYellow consoleColor
|
||||
BrightBlue consoleColor
|
||||
BrightMagenta consoleColor
|
||||
BrightCyan consoleColor
|
||||
BrightWhite consoleColor
|
||||
}
|
||||
type qrcodeRecoveryLevel qrcode.RecoveryLevel
|
||||
type qrcodeRecoveryLevels struct {
|
||||
Low qrcodeRecoveryLevel
|
||||
Medium qrcodeRecoveryLevel
|
||||
High qrcodeRecoveryLevel
|
||||
Highest qrcodeRecoveryLevel
|
||||
}
|
||||
|
||||
var (
|
||||
ConsoleColors consoleColors = consoleColors{
|
||||
NormalBlack: "\033[38;5;0m \033[0m",
|
||||
NormalRed: "\033[38;5;1m \033[0m",
|
||||
NormalGreen: "\033[38;5;2m \033[0m",
|
||||
NormalYellow: "\033[38;5;3m \033[0m",
|
||||
NormalBlue: "\033[38;5;4m \033[0m",
|
||||
NormalMagenta: "\033[38;5;5m \033[0m",
|
||||
NormalCyan: "\033[38;5;6m \033[0m",
|
||||
NormalWhite: "\033[38;5;7m \033[0m",
|
||||
BrightBlack: "\033[48;5;0m \033[0m",
|
||||
BrightRed: "\033[48;5;1m \033[0m",
|
||||
BrightGreen: "\033[48;5;2m \033[0m",
|
||||
BrightYellow: "\033[48;5;3m \033[0m",
|
||||
BrightBlue: "\033[48;5;4m \033[0m",
|
||||
BrightMagenta: "\033[48;5;5m \033[0m",
|
||||
BrightCyan: "\033[48;5;6m \033[0m",
|
||||
BrightWhite: "\033[48;5;7m \033[0m"}
|
||||
QRCodeRecoveryLevels = qrcodeRecoveryLevels{
|
||||
Low: qrcodeRecoveryLevel(qrcode.Low),
|
||||
Medium: qrcodeRecoveryLevel(qrcode.Medium),
|
||||
High: qrcodeRecoveryLevel(qrcode.High),
|
||||
Highest: qrcodeRecoveryLevel(qrcode.Highest)}
|
||||
)
|
||||
|
||||
type QRCodeString string
|
||||
|
||||
func (v *QRCodeString) Print() {
|
||||
fmt.Fprint(outer, *v)
|
||||
}
|
||||
|
||||
type qrcodeTerminal struct {
|
||||
front consoleColor
|
||||
back consoleColor
|
||||
level qrcodeRecoveryLevel
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) Get(content interface{}) (result *QRCodeString) {
|
||||
var qr *qrcode.QRCode
|
||||
var err error
|
||||
if t, ok := content.(string); ok {
|
||||
qr, err = qrcode.New(t, qrcode.RecoveryLevel(v.level))
|
||||
} else if t, ok := content.([]byte); ok {
|
||||
qr, err = qrcode.New(string(t), qrcode.RecoveryLevel(v.level))
|
||||
}
|
||||
if qr != nil && err == nil {
|
||||
data := qr.Bitmap()
|
||||
result = v.getQRCodeString(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) Get2(bytes []byte) (result *QRCodeString) {
|
||||
data, err := parseQR(bytes)
|
||||
if err == nil {
|
||||
result = v.getQRCodeString(data)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func New2(front, back consoleColor, level qrcodeRecoveryLevel) *qrcodeTerminal {
|
||||
obj := qrcodeTerminal{front: front, back: back, level: level}
|
||||
return &obj
|
||||
}
|
||||
|
||||
func New() *qrcodeTerminal {
|
||||
front, back, level := ConsoleColors.BrightBlack, ConsoleColors.BrightWhite, QRCodeRecoveryLevels.Medium
|
||||
return New2(front, back, level)
|
||||
}
|
||||
|
||||
func (v *qrcodeTerminal) getQRCodeString(data [][]bool) (result *QRCodeString) {
|
||||
str := ""
|
||||
for ir, row := range data {
|
||||
lr := len(row)
|
||||
if ir == 0 || ir == 1 || ir == 2 ||
|
||||
ir == lr-1 || ir == lr-2 || ir == lr-3 {
|
||||
continue
|
||||
}
|
||||
for ic, col := range row {
|
||||
lc := len(data)
|
||||
if ic == 0 || ic == 1 || ic == 2 ||
|
||||
ic == lc-1 || ic == lc-2 || ic == lc-3 {
|
||||
continue
|
||||
}
|
||||
if col {
|
||||
str += fmt.Sprint(v.front)
|
||||
} else {
|
||||
str += fmt.Sprint(v.back)
|
||||
}
|
||||
}
|
||||
str += fmt.Sprintln()
|
||||
}
|
||||
obj := QRCodeString(str)
|
||||
result = &obj
|
||||
return
|
||||
}
|
||||
|
||||
func parseQR(bytes []byte) (data [][]bool, err error) {
|
||||
r := nbytes.NewReader(bytes)
|
||||
img, err := png.Decode(r)
|
||||
if err == nil {
|
||||
rect := img.Bounds()
|
||||
mx, my := rect.Max.X, rect.Max.Y
|
||||
data = make([][]bool, mx)
|
||||
for x := 0; x < mx; x++ {
|
||||
data[x] = make([]bool, my)
|
||||
for y := 0; y < my; y++ {
|
||||
c := img.At(x, y)
|
||||
r, _, _, _ := c.RGBA()
|
||||
data[x][y] = r == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (_ *qrcodeTerminal) SetOutput(out io.Writer) {
|
||||
outer = out
|
||||
}
|
||||
|
||||
var outer = colorable.NewColorableStdout()
|
||||
118
session_manager.go
Normal file
118
session_manager.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"github.com/rivo/tview"
|
||||
"os"
|
||||
"os/user"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/normen/whatscli/qrcode"
|
||||
)
|
||||
|
||||
func GetConnection() *whatsapp.Conn {
|
||||
var wac *whatsapp.Conn
|
||||
if connection == nil {
|
||||
wacc, err := whatsapp.NewConn(5 * time.Second)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
wac = wacc
|
||||
connection = wac
|
||||
//wac.SetClientVersion(2, 2021, 4)
|
||||
} else {
|
||||
wac = connection
|
||||
}
|
||||
return wac
|
||||
}
|
||||
|
||||
// Login logs in the user. It ries to see if a session already exists. If not, tries to create a
|
||||
// new one using qr scanned on the terminal.
|
||||
func Login() error {
|
||||
return LoginWithConnection(GetConnection())
|
||||
}
|
||||
|
||||
// LoginWithConnection logs in the user using a provided connection. It ries to see if a session already exists. If not, tries to create a
|
||||
// new one using qr scanned on the terminal.
|
||||
func LoginWithConnection(wac *whatsapp.Conn) error {
|
||||
if wac.Info != nil && wac.Info.Connected {
|
||||
return nil
|
||||
}
|
||||
//load saved session
|
||||
session, err := readSession()
|
||||
if err == nil {
|
||||
//restore session
|
||||
session, err = wac.RestoreWithSession(session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("restoring failed: %v\n", err)
|
||||
}
|
||||
} else {
|
||||
//no saved session -> regular login
|
||||
qr := make(chan string)
|
||||
go func() {
|
||||
terminal := qrcode.New()
|
||||
terminal.SetOutput(tview.ANSIWriter(textView))
|
||||
terminal.Get(<-qr).Print()
|
||||
}()
|
||||
session, err = wac.Login(qr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during login: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
//save session
|
||||
err = writeSession(session)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving session: %v\n", err)
|
||||
}
|
||||
//<-time.After(3 * time.Second)
|
||||
fmt.Fprint(textView, "\nlogin successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout logs out the user.
|
||||
func Logout() error {
|
||||
return removeSession()
|
||||
}
|
||||
|
||||
func GetHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
}
|
||||
return usr.HomeDir + string(os.PathSeparator)
|
||||
}
|
||||
|
||||
func readSession() (whatsapp.Session, error) {
|
||||
session := whatsapp.Session{}
|
||||
file, err := os.Open(GetHomeDir() + ".whatscli.session")
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
defer file.Close()
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&session)
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func writeSession(session whatsapp.Session) error {
|
||||
file, err := os.Create(GetHomeDir() + ".whatscli.session")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
encoder := gob.NewEncoder(file)
|
||||
err = encoder.Encode(session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeSession() error {
|
||||
return os.Remove(GetHomeDir() + ".whatscli.session")
|
||||
}
|
||||
Reference in New Issue
Block a user