From 423cac26fb25432fbdfc6a5880c8b1e0611a1222 Mon Sep 17 00:00:00 2001 From: normen Date: Mon, 16 Nov 2020 01:49:58 +0100 Subject: [PATCH] initial commit --- .gitignore | 3 + README.md | 28 +++++ go.mod | 13 ++ go.sum | 57 +++++++++ main.go | 288 +++++++++++++++++++++++++++++++++++++++++++ messages/contacts.go | 56 +++++++++ messages/storage.go | 72 +++++++++++ qrcode/qrcode.go | 191 ++++++++++++++++++++++++++++ session_manager.go | 118 ++++++++++++++++++ 9 files changed, 826 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 messages/contacts.go create mode 100644 messages/storage.go create mode 100644 qrcode/qrcode.go create mode 100644 session_manager.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a1e696 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +whatscli +whatscli.exe diff --git a/README.md b/README.md new file mode 100644 index 0000000..e17fbea --- /dev/null +++ b/README.md @@ -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 + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c3c8538 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..53d9165 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fa5b895 --- /dev/null +++ b/main.go @@ -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 | = switch input/tree | = 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) + } +} diff --git a/messages/contacts.go b/messages/contacts.go new file mode 100644 index 0000000..2df88a7 --- /dev/null +++ b/messages/contacts.go @@ -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) +} diff --git a/messages/storage.go b/messages/storage.go new file mode 100644 index 0000000..600f05c --- /dev/null +++ b/messages/storage.go @@ -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 +} diff --git a/qrcode/qrcode.go b/qrcode/qrcode.go new file mode 100644 index 0000000..51cf4ca --- /dev/null +++ b/qrcode/qrcode.go @@ -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() diff --git a/session_manager.go b/session_manager.go new file mode 100644 index 0000000..74da312 --- /dev/null +++ b/session_manager.go @@ -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") +}