Compare commits

..

25 Commits

Author SHA1 Message Date
normen
f79e3e46b7 add development info 2020-11-28 15:37:09 +01:00
normen
3053d15929 add exit key binding to help screen 2020-11-28 15:33:26 +01:00
normen
73cd749394 move commands info into separate help screen 2020-11-28 15:28:49 +01:00
normen
8e8f2da43f allow paging when selecting messages 2020-11-28 14:56:58 +01:00
normen
e8d9f266fa allow pasting even if theres no system clipboard 2020-11-28 14:30:15 +01:00
normen
2763bb14b4 switch clipboard library 2020-11-28 04:04:06 +01:00
normen
f91ffbef0f colorize forwarded text differently 2020-11-28 02:24:32 +01:00
normen
3521a3b6f5 add /remove and /removeadmin commands 2020-11-28 00:23:20 +01:00
normen
a6d7954795 don't just print help when no messages are available 2020-11-27 23:23:36 +01:00
normen
96e9d75810 small fix in help screen 2020-11-27 21:48:04 +01:00
normen
b2904929b0 update README 2020-11-27 21:42:32 +01:00
normen
033e7aa1ac allow creating and managing groups 2020-11-27 21:33:40 +01:00
normen
53e404dd55 add command/binding to copy selected user id 2020-11-27 20:08:14 +01:00
normen
8318d2e80f remove redundant "reported an error" 2020-11-27 18:58:51 +01:00
normen
3af2a3738f fix unread count not updating 2020-11-27 14:22:20 +01:00
normen
e31c2c3e36 get the battery state on connection 2020-11-27 03:30:13 +01:00
normen
3f20981550 cleaner logout 2020-11-27 03:10:25 +01:00
normen
f8368e4998 add /read command to set chat to read
- display connection status (connecting, connected, disconnected)
- allow setting unread count color
2020-11-27 00:11:59 +01:00
normen
121a73c312 add unread count (no reset yet) 2020-11-26 22:52:47 +01:00
normen
86af0d82a4 remove contact file name getter 2020-11-26 21:50:41 +01:00
normen
b94129fb0e rename focus_contacts and contact_sidebar_width - sorry! 2020-11-26 21:48:59 +01:00
normen
59a843bb8d use chat and contact list reported by go-whatsapp
Fixes #22
2020-11-26 21:43:59 +01:00
normen
a86aa3eec3 show contact name for status messages 2020-11-26 18:53:22 +01:00
normen
a20b4e3592 update dependencies 2020-11-26 18:45:23 +01:00
normen
9e62295188 unify variable names 2020-11-26 02:46:00 +01:00
8 changed files with 562 additions and 180 deletions

View File

@@ -14,16 +14,17 @@ Things that work.
- Allows downloading and opening image/video/audio/document attachments
- Allows sending documents
- Allows color customization
- Allows basic group management
- Supports desktop notifications
- Binaries for Windows, Mac, Linux and RaspBerry Pi
### Caveats
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
Heres some things you might expect to work that don't. Plus some other things I should mention.
- Only shows existing chats
- No unread message count
- No proper connection drop handling
- No auto-reconnect when connection drops
- No automation of messages, no sending of messages through shell commands
- FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when FaceBook changes stuff in their web app
## Installation / Usage
@@ -51,3 +52,22 @@ Some ways to install via package managers are supported but the installed versio
- `https://aur.archlinux.org/packages/whatscli/`
## Development
This app started as my first attempt at writing something in go. Some areas that are marked with `TODO` can still be improved but work mostly. If you want to contribute features or improve the code thats great, send a PR and we can discuss.
### Building
Using a recent version of go, building should be straightforward. Either use `go build`, `go run` etc. or use the included Makefile.
### Structure Overview
The `main.go` contains most UI elements which are based around a tview app running on the main routine. It uses a keymap configuration based on the tslocum/cbind library. Apart from that it mostly manages the selection of messages in the current chat as well as displaying the messages and chat list that the session manager sends.
The `messages/session_manager.go` runs a separate go routine to receive messages from the Rhymen/go-whatsapp library which in turn runs the websocket connection to the whatsapp server. The session manager receives the messages from go-whatsapp and the commands from the UI via channels that it drains on its main routine. It then updates the UI accordingly using the UiMessageHandler interface. This ensures "thread safe" management of the connection and data while both UI and network connection run separately.
Session manager is designed "object like", the MessageDatabase in `messages/storage.go` is similar and somewhat linked to the session manager. In theory the session manager could be run multiple times (multiple accounts) or a different implementation of a session manager could connect to a different service like e.g. Telegram.
In `messages/messages.go` most interfaces and data structures for communication are kept.
The `config/settings.go` keeps a singleton `Config` struct with the config that is loaded via the gopkg.in/ini.v1 library when the app starts. This makes it easy to quickly add new configuration items with default values that can be used across the app.

View File

@@ -34,8 +34,11 @@ type Keymap struct {
SwitchPanels string
FocusMessages string
FocusInput string
FocusContacts string
FocusChats string
Copyuser string
Pasteuser string
CommandBacklog string
CommandRead string
CommandConnect string
CommandQuit string
CommandHelp string
@@ -48,12 +51,13 @@ type Keymap struct {
}
type Ui struct {
ContactSidebarWidth int
ChatSidebarWidth int
}
type Colors struct {
Background string
Text string
ForwardedText string
ListHeader string
ListContact string
ListGroup string
@@ -62,6 +66,7 @@ type Colors struct {
Borders string
InputBackground string
InputText string
UnreadCount string
Positive string
Negative string
}
@@ -79,8 +84,11 @@ var Config = IniFile{
SwitchPanels: "Tab",
FocusMessages: "Ctrl+w",
FocusInput: "Ctrl+Space",
FocusContacts: "Ctrl+e",
FocusChats: "Ctrl+e",
CommandBacklog: "Ctrl+b",
CommandRead: "Ctrl+n",
Copyuser: "Ctrl+c",
Pasteuser: "Ctrl+v",
CommandConnect: "Ctrl+r",
CommandQuit: "Ctrl+q",
CommandHelp: "Ctrl+?",
@@ -92,11 +100,12 @@ var Config = IniFile{
MessageShow: "s",
},
&Ui{
ContactSidebarWidth: 30,
ChatSidebarWidth: 30,
},
&Colors{
Background: "black",
Text: "white",
ForwardedText: "purple",
ListHeader: "yellow",
ListContact: "green",
ListGroup: "blue",
@@ -105,6 +114,7 @@ var Config = IniFile{
Borders: "white",
InputBackground: "blue",
InputText: "white",
UnreadCount: "yellow",
Positive: "green",
Negative: "red",
},
@@ -160,13 +170,6 @@ func GetSessionFilePath() string {
return GetHomeDir() + ".whatscli.session"
}
func GetContactsFilePath() string {
if sessionFilePath, err := xdg.ConfigFile("whatscli/contacts"); err == nil {
return sessionFilePath
}
return GetHomeDir() + ".whatscli.contacts"
}
// gets the OS home dir with a path separator at the end
func GetHomeDir() string {
usr, err := user.Current()

5
go.mod
View File

@@ -16,9 +16,10 @@ require (
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
github.com/zyedidia/clipboard v1.0.3
gitlab.com/tslocum/cbind v0.1.4
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 // indirect
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 // indirect
golang.org/x/text v0.3.4 // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.62.0

11
go.sum
View File

@@ -20,7 +20,6 @@ github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pm
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/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
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=
@@ -91,12 +90,14 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
github.com/zyedidia/clipboard v1.0.3 h1:F/nCDVYMdbDWTmY8s8cJl0tnwX32q96IF09JHM14bUI=
github.com/zyedidia/clipboard v1.0.3/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
gitlab.com/tslocum/cbind v0.1.4 h1:cbZXPPcieXspk8cShoT6efz7HAT8yMNQcofYWNizis4=
gitlab.com/tslocum/cbind v0.1.4/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@@ -122,8 +123,8 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 h1:WFCmm2Hi9I2gYf1kv7LQ8ajKA5x9heC2v9xuUKwvf68=
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

228
main.go
View File

@@ -13,13 +13,14 @@ import (
"github.com/normen/whatscli/messages"
"github.com/rivo/tview"
"github.com/skratchdot/open-golang/open"
"github.com/zyedidia/clipboard"
"gitlab.com/tslocum/cbind"
)
var VERSION string = "v0.9.6"
var VERSION string = "v1.0.3"
var sndTxt string = ""
var currentReceiver messages.Contact = messages.Contact{}
var currentReceiver messages.Chat = messages.Chat{}
var curRegions []messages.Message
var textView *tview.TextView
@@ -28,7 +29,7 @@ var textInput *tview.InputField
var topBar *tview.TextView
var infoBar *tview.TextView
var contactRoot *tview.TreeNode
var chatRoot *tview.TreeNode
var app *tview.Application
var sessionManager *messages.SessionManager
@@ -45,7 +46,7 @@ func main() {
app = tview.NewApplication()
sideBarWidth := config.Config.Ui.ContactSidebarWidth
sideBarWidth := config.Config.Ui.ChatSidebarWidth
gridLayout := tview.NewGrid()
gridLayout.SetRows(1, 0, 1)
gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth)
@@ -128,28 +129,28 @@ func main() {
app.Run()
}
// creates the TreeView for contacts
// creates the TreeView for chats
func MakeTree() *tview.TreeView {
rootDir := "Contacts"
contactRoot = tview.NewTreeNode(rootDir).
rootDir := "Chats"
chatRoot = tview.NewTreeNode(rootDir).
SetColor(tcell.ColorNames[config.Config.Colors.ListHeader])
treeView = tview.NewTreeView().
SetRoot(contactRoot).
SetCurrentNode(contactRoot)
SetRoot(chatRoot).
SetCurrentNode(chatRoot)
treeView.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
// If a contact was selected, open it.
// If a chat was selected, open it.
treeView.SetChangedFunc(func(node *tview.TreeNode) {
reference := node.GetReference()
if reference == nil {
SetDisplayedContact(messages.Contact{"", false, ""})
SetDisplayedChat(messages.Chat{"", false, "", 0, 0})
return // Selecting the root node does nothing.
}
children := node.GetChildren()
if len(children) == 0 {
// Load and show files in this directory.
recv := reference.(messages.Contact)
SetDisplayedContact(recv)
recv := reference.(messages.Chat)
SetDisplayedChat(recv)
} else {
// Collapse if visible, expand if collapsed.
node.SetExpanded(!node.IsExpanded())
@@ -201,6 +202,29 @@ func handleCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey {
}
}
func handleCopyUser(ev *tcell.EventKey) *tcell.EventKey {
if hls := textView.GetHighlights(); len(hls) > 0 {
for _, val := range curRegions {
if val.Id == hls[0] {
clipboard.WriteAll(val.ContactId, "clipboard")
PrintText("copied id of " + val.ContactName + " to clipboard")
}
}
ResetMsgSelection()
} else if currentReceiver.Id != "" {
clipboard.WriteAll(currentReceiver.Id, "clipboard")
PrintText("copied id of " + currentReceiver.Name + " to clipboard")
}
return nil
}
func handlePasteUser(ev *tcell.EventKey) *tcell.EventKey {
if clip, err := clipboard.ReadAll("clipboard"); err == nil {
textInput.SetText(textInput.GetText() + " " + clip)
}
return nil
}
func handleQuit(ev *tcell.EventKey) *tcell.EventKey {
sessionManager.CommandChannel <- messages.Command{"disconnect", nil}
app.Stop()
@@ -224,38 +248,36 @@ func handleMessageCommand(command string) func(ev *tcell.EventKey) *tcell.EventK
}
}
func handleMessagesUp(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
func handleMessagesMove(amount int) func(ev *tcell.EventKey) *tcell.EventKey {
return func(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
hls := textView.GetHighlights()
if len(hls) > 0 {
newId := GetOffsetMsgId(hls[0], amount)
if newId != "" {
textView.Highlight(newId)
}
} else {
if amount < 0 {
textView.Highlight(curRegions[0].Id)
} else {
textView.Highlight(curRegions[len(curRegions)-1].Id)
}
}
textView.ScrollToHighlight()
return nil
}
hls := textView.GetHighlights()
if len(hls) > 0 {
newId := GetOffsetMsgId(hls[0], -1)
if newId != "" {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[len(curRegions)-1].Id)
}
textView.ScrollToHighlight()
return nil
}
func handleMessagesDown(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
hls := textView.GetHighlights()
if len(hls) > 0 {
newId := GetOffsetMsgId(hls[0], 1)
if newId != "" {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[0].Id)
}
textView.ScrollToHighlight()
return nil
func handleChatPanelUp(ev *tcell.EventKey) *tcell.EventKey {
//TODO: scroll selection in treeView? or chatRoot? How?
return ev
}
func handleChatPanelDown(ev *tcell.EventKey) *tcell.EventKey {
return ev
}
func handleMessagesLast(ev *tcell.EventKey) *tcell.EventKey {
@@ -287,6 +309,7 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
// load the key map
func LoadShortcuts() {
// global bindings for app
keyBindings = cbind.NewConfiguration()
if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil {
PrintErrorMsg("focus_messages:", err)
@@ -294,12 +317,21 @@ func LoadShortcuts() {
if err := keyBindings.Set(config.Config.Keymap.FocusInput, handleFocusInput); err != nil {
PrintErrorMsg("focus_input:", err)
}
if err := keyBindings.Set(config.Config.Keymap.FocusContacts, handleFocusContacts); err != nil {
if err := keyBindings.Set(config.Config.Keymap.FocusChats, handleFocusContacts); err != nil {
PrintErrorMsg("focus_contacts:", err)
}
if err := keyBindings.Set(config.Config.Keymap.SwitchPanels, handleSwitchPanels); err != nil {
PrintErrorMsg("switch_panels:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandRead, handleCommand("read")); err != nil {
PrintErrorMsg("command_read:", err)
}
if err := keyBindings.Set(config.Config.Keymap.Copyuser, handleCopyUser); err != nil {
PrintErrorMsg("copyuser:", err)
}
if err := keyBindings.Set(config.Config.Keymap.Pasteuser, handlePasteUser); err != nil {
PrintErrorMsg("pasteuser:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil {
PrintErrorMsg("command_backlog:", err)
}
@@ -313,6 +345,7 @@ func LoadShortcuts() {
PrintErrorMsg("command_help:", err)
}
app.SetInputCapture(keyBindings.Capture)
// bindings for chat message text view
keysMessages := cbind.NewConfiguration()
if err := keysMessages.Set(config.Config.Keymap.MessageDownload, handleMessageCommand("download")); err != nil {
PrintErrorMsg("message_download:", err)
@@ -320,6 +353,12 @@ func LoadShortcuts() {
if err := keysMessages.Set(config.Config.Keymap.MessageOpen, handleMessageCommand("open")); err != nil {
PrintErrorMsg("message_open:", err)
}
if err := keysMessages.Set(config.Config.Keymap.Copyuser, handleCopyUser); err != nil {
PrintErrorMsg("copyuser:", err)
}
if err := keysMessages.Set(config.Config.Keymap.Pasteuser, handlePasteUser); err != nil {
PrintErrorMsg("pasteuser:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
PrintErrorMsg("message_show:", err)
}
@@ -333,26 +372,33 @@ func LoadShortcuts() {
PrintErrorMsg("message_revoke:", err)
}
keysMessages.SetKey(tcell.ModNone, tcell.KeyEscape, handleExitMessages)
keysMessages.SetKey(tcell.ModNone, tcell.KeyUp, handleMessagesUp)
keysMessages.SetKey(tcell.ModNone, tcell.KeyDown, handleMessagesDown)
keysMessages.SetRune(tcell.ModNone, 'k', handleMessagesUp)
keysMessages.SetRune(tcell.ModNone, 'j', handleMessagesDown)
keysMessages.SetKey(tcell.ModNone, tcell.KeyUp, handleMessagesMove(-1))
keysMessages.SetKey(tcell.ModNone, tcell.KeyDown, handleMessagesMove(1))
keysMessages.SetKey(tcell.ModNone, tcell.KeyPgUp, handleMessagesMove(-10))
keysMessages.SetKey(tcell.ModNone, tcell.KeyPgDn, handleMessagesMove(10))
keysMessages.SetRune(tcell.ModNone, 'k', handleMessagesMove(-1))
keysMessages.SetRune(tcell.ModNone, 'j', handleMessagesMove(1))
keysMessages.SetRune(tcell.ModNone, 'g', handleMessagesFirst)
keysMessages.SetRune(tcell.ModNone, 'G', handleMessagesLast)
keysMessages.SetRune(tcell.ModCtrl, 'u', handleMessagesMove(-10))
keysMessages.SetRune(tcell.ModCtrl, 'd', handleMessagesMove(10))
textView.SetInputCapture(keysMessages.Capture)
keysChatPanel := cbind.NewConfiguration()
keysChatPanel.SetRune(tcell.ModCtrl, 'u', handleChatPanelUp)
keysChatPanel.SetRune(tcell.ModCtrl, 'd', handleChatPanelDown)
treeView.SetInputCapture(keysChatPanel.Capture)
}
// prints help to chat view
func PrintHelp() {
cmdPrefix := config.Config.General.CmdPrefix
fmt.Fprintln(textView, "[::b]WhatsCLI "+VERSION+"[-]")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::u]Keys:[-::-]")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Global")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = Scroll history/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = Switch input/contacts")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = Scroll history/chats")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = Switch input/chats")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusMessages, "[::-] = Focus message panel")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.CommandQuit, "[::-] = Exit app")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Message panel[-::-]")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = select message")
@@ -363,6 +409,15 @@ func PrintHelp() {
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageRevoke, "[::-] = Revoke message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageInfo, "[::-] = Info about message")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Config file in ->", config.GetConfigFilePath())
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Type [::b]"+cmdPrefix+"commands[::-] to see all commands")
fmt.Fprintln(textView, "")
}
func PrintCommands() {
cmdPrefix := config.Config.General.CmdPrefix
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::u]Commands:[-::-]")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Global[-::-]")
@@ -373,18 +428,27 @@ func PrintHelp() {
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Chat[-::-]")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next 10 previous messages")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"read [::-]or[::b]", config.Config.Keymap.CommandRead, "[::-] = mark new messages in chat as read")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"upload[::-] /path/to/file = Upload any file as document")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendimage[::-] /path/to/file = Send image message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendvideo[::-] /path/to/file = Send video message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendaudio[::-] /path/to/file = Send audio message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = Leave group")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Configuration:")
fmt.Fprintln(textView, " ->", config.GetConfigFilePath())
fmt.Fprintln(textView, "[-::-]Groups[-::-]")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = Leave group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"create[::-] [user-id[] [user-id[] Group Subject = Create group with users")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"subject[::-] New Subject = Change subject of group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"add[::-] [user-id[] = Add user to group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"remove[::-] [user-id[] = Remove user from group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"admin[::-] [user-id[] = Set admin role for user in group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"removeadmin[::-] [user-id[] = Remove admin role for user in group")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Use[::b]", config.Config.Keymap.Copyuser, "[::-]to copy a selected user id to clipboard")
fmt.Fprintln(textView, "Use[::b]", config.Config.Keymap.Pasteuser, "[::-]to paste clipboard to text input")
fmt.Fprintln(textView, "")
}
// called when text is entered by the user
// TODO: parse and map commands automatically
func EnterCommand(key tcell.Key) {
if sndTxt == "" {
return
@@ -395,13 +459,16 @@ func EnterCommand(key tcell.Key) {
}
cmdPrefix := config.Config.General.CmdPrefix
if sndTxt == cmdPrefix+"help" {
//command
PrintHelp()
textInput.SetText("")
return
}
if sndTxt == cmdPrefix+"commands" {
PrintCommands()
textInput.SetText("")
return
}
if sndTxt == cmdPrefix+"quit" {
//command
sessionManager.CommandChannel <- messages.Command{"disconnect", nil}
app.Stop()
return
@@ -529,16 +596,16 @@ func UpdateStatusBar(statusInfo messages.SessionStatus) {
//infoBar.SetText("🔋: ??%")
}
// sets the current contact, loads text from storage to TextView
func SetDisplayedContact(wid messages.Contact) {
//TODO: how to get contact to set
// sets the current chat, loads text from storage to TextView
func SetDisplayedChat(wid messages.Chat) {
//TODO: how to get chat to set
currentReceiver = wid
textView.Clear()
textView.SetTitle(wid.Name)
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
}
// get a string representation of all messages for contact
// get a string representation of all messages for chat
func getMessagesString(msgs []messages.Message) string {
out := ""
for _, msg := range msgs {
@@ -549,11 +616,15 @@ func getMessagesString(msgs []messages.Message) string {
}
// create a formatted string with regions based on message ID from a text message
//TODO: optimize, use Sprintf etc
func getTextMessageString(msg *messages.Message) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
out := ""
text := tview.Escape(msg.Text)
if msg.Forwarded {
text = "[" + config.Config.Colors.ForwardedText + "]" + text + "[-]"
}
tim := time.Unix(int64(msg.Timestamp), 0)
time := tim.Format("02-01-06 15:04:05")
out += "[\""
@@ -562,7 +633,7 @@ func getTextMessageString(msg *messages.Message) string {
if msg.FromMe { //msg from me
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
} else { // message from others
out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.SourceShort + ": [-::-]" + text
out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.ContactShort + ": [-::-]" + text
}
out += "[\"\"]"
return out
@@ -586,17 +657,34 @@ func (u UiHandler) NewScreen(msgs []messages.Message) {
textView.SetText(screen)
curRegions = msgs
if screen == "" {
PrintHelp()
if currentReceiver.Id == "" {
PrintHelp()
} else {
PrintText("[::d] ~~~ no messages, press " + config.Config.Keymap.CommandBacklog + " to load backlog if available ~~~[::-]")
}
}
})
}
// loads the contact data from storage to the TreeView
func (u UiHandler) SetContacts(ids []messages.Contact) {
// loads the chat data from storage to the TreeView
func (u UiHandler) SetChats(ids []messages.Chat) {
go app.QueueUpdateDraw(func() {
contactRoot.ClearChildren()
chatRoot.ClearChildren()
oldId := currentReceiver.Id
for _, element := range ids {
node := tview.NewTreeNode(element.Name).
name := element.Name
if name == "" {
name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX)
}
if element.Unread > 0 {
name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])"
//tim := time.Unix(element.LastMessage, 0)
//sin := time.Since(tim)
//since := fmt.Sprintf("%s", sin)
//time := tim.Format("02-01-06 15:04:05")
//name += since
}
node := tview.NewTreeNode(name).
SetReference(element).
SetSelectable(true)
if element.IsGroup {
@@ -604,8 +692,12 @@ func (u UiHandler) SetContacts(ids []messages.Contact) {
} else {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
}
contactRoot.AddChild(node)
if element == currentReceiver {
// store new currentReceiver, else the selection on the left goes off
if element.Id == oldId {
currentReceiver = element
}
chatRoot.AddChild(node)
if element.Id == currentReceiver.Id {
treeView.SetCurrentNode(node)
}
}

View File

@@ -7,7 +7,7 @@ import "io"
type UiMessageHandler interface {
NewMessage(Message)
NewScreen([]Message)
SetContacts([]Contact)
SetChats([]Chat)
PrintError(error)
PrintText(string)
PrintFile(string)
@@ -46,22 +46,33 @@ type Command struct {
// internal message representation to abstract from message lib
type Message struct {
Id string
ContactId string
Timestamp uint64
SourceId string
SourceName string
SourceShort string
FromMe bool
Text string
Id string
ChatId string // the source of the message (group id or contact id)
ContactId string
ContactName string
ContactShort string
Timestamp uint64
FromMe bool
Forwarded bool
Text string
}
// internal contact representation to abstract from message lib
type Contact struct {
type Chat struct {
Id string
IsGroup bool
Name string
Unread int
//TODO: convert to uint64
LastMessage int64
}
type Contact struct {
Id string
Name string
Short string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
const STATUSSUFFIX = "status@broadcast"

View File

@@ -2,36 +2,39 @@ package messages
import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/gabriel-vasile/mimetype"
"github.com/gdamore/tcell/v2"
"github.com/gen2brain/beeep"
"github.com/rivo/tview"
"mvdan.cc/xurls/v2"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
"github.com/normen/whatscli/qrcode"
"github.com/rivo/tview"
"mvdan.cc/xurls/v2"
)
// SessionManager deals with the connection and receives commands from the UI
// it updates the UI accordingly
type SessionManager struct {
db *MessageDatabase
currentReceiver string // currently selected contact for message handling
currentReceiver string // currently selected chat for message handling
uiHandler UiMessageHandler
connection *whatsapp.Conn
BatteryChannel chan BatteryMsg
StatusChannel chan StatusMsg
CommandChannel chan Command
ChatChannel chan whatsapp.Chat
ContactChannel chan whatsapp.Contact
TextChannel chan whatsapp.TextMessage
OtherChannel chan interface{}
statusInfo SessionStatus
@@ -47,6 +50,8 @@ func (sm *SessionManager) Init(handler UiMessageHandler) {
sm.BatteryChannel = make(chan BatteryMsg, 10)
sm.StatusChannel = make(chan StatusMsg, 10)
sm.CommandChannel = make(chan Command, 10)
sm.ChatChannel = make(chan whatsapp.Chat, 10)
sm.ContactChannel = make(chan whatsapp.Contact, 10)
sm.TextChannel = make(chan whatsapp.TextMessage, 10)
sm.OtherChannel = make(chan interface{}, 10)
}
@@ -79,12 +84,13 @@ func (sm *SessionManager) runManager() error {
screen := sm.getMessages(sm.currentReceiver)
sm.uiHandler.NewScreen(screen)
}
// notify if contact is in focus and we didn't send a message recently
// TODO: move to UI (when UI has time in messages)
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
err := beeep.Notify(sm.getIdShort(msg.Info.RemoteJid), msg.Text, "")
// notify if chat is in focus and we didn't send a message recently
// TODO: move notify to UI
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
sm.db.NewUnreadChat(msg.Info.RemoteJid)
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
err := beeep.Notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
if err != nil {
sm.uiHandler.PrintError(err)
}
@@ -92,25 +98,49 @@ func (sm *SessionManager) runManager() error {
}
}
} else {
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
// notify if message is younger than 30 sec and not in focus
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
err := beeep.Notify(sm.getIdShort(msg.Info.RemoteJid), msg.Text, "")
// notify if message is younger than 30 sec and not in focus
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
sm.db.NewUnreadChat(msg.Info.RemoteJid)
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
err := beeep.Notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
//TODO: create here this way? -> updates list quickly
contactIds := sm.db.GetContactIds()
contacts := make([]Contact, len(contactIds))
for idx, id := range contactIds {
contacts[idx] = Contact{id, strings.Contains(id, GROUPSUFFIX), sm.getIdName(id)}
}
sm.uiHandler.SetContacts(contacts)
sm.uiHandler.SetChats(sm.db.GetChatIds())
case other := <-sm.OtherChannel:
sm.db.AddOtherMessage(&other)
case c := <-sm.ContactChannel:
contact := Contact{
c.Jid,
c.Name,
c.Short,
}
if contact.Name == "" && c.Notify != "" {
contact.Name = c.Notify
}
if contact.Short == "" && c.Notify != "" {
contact.Short = c.Notify
}
sm.db.AddContact(contact)
sm.uiHandler.SetChats(sm.db.GetChatIds())
case c := <-sm.ChatChannel:
if c.IsMarkedSpam == "false" {
isGroup := strings.Contains(c.Jid, GROUPSUFFIX)
unread, _ := strconv.ParseInt(c.Unread, 10, 0)
last, _ := strconv.ParseInt(c.LastMessageTime, 10, 64)
chat := Chat{
c.Jid,
isGroup,
c.Name,
int(unread),
last,
}
sm.db.AddChat(chat)
sm.uiHandler.SetChats(sm.db.GetChatIds())
}
case command := <-sm.CommandChannel:
sm.execCommand(command)
case batteryMsg := <-sm.BatteryChannel:
@@ -119,6 +149,7 @@ func (sm *SessionManager) runManager() error {
sm.statusInfo.BatteryCharge = batteryMsg.charge
sm.uiHandler.SetStatus(sm.statusInfo)
case statusMsg := <-sm.StatusChannel:
prevStatus := sm.statusInfo.Connected
if statusMsg.err != nil {
} else {
sm.statusInfo.Connected = statusMsg.connected
@@ -127,6 +158,13 @@ func (sm *SessionManager) runManager() error {
connected := wac.GetConnected()
sm.statusInfo.Connected = connected
sm.uiHandler.SetStatus(sm.statusInfo)
if prevStatus != sm.statusInfo.Connected {
if sm.statusInfo.Connected {
sm.uiHandler.PrintText("connected")
} else {
sm.uiHandler.PrintText("disconnected")
}
}
}
}
fmt.Fprintln(sm.uiHandler.GetWriter(), "closing the receiver")
@@ -134,7 +172,7 @@ func (sm *SessionManager) runManager() error {
return nil
}
// set the currently selected contact
// set the currently selected chat
func (sm *SessionManager) setCurrentReceiver(id string) {
sm.currentReceiver = id
screen := sm.getMessages(id)
@@ -145,7 +183,12 @@ func (sm *SessionManager) setCurrentReceiver(id string) {
func (sm *SessionManager) getConnection() *whatsapp.Conn {
var wac *whatsapp.Conn
if sm.connection == nil {
wacc, err := whatsapp.NewConn(5 * time.Second)
options := &whatsapp.Options{
Timeout: 5 * time.Second,
LongClientName: "WhatsCLI Client",
ShortClientName: "whatscli",
}
wacc, err := whatsapp.NewConnWithOptions(options)
if err != nil {
return nil
}
@@ -167,6 +210,7 @@ func (sm *SessionManager) login() error {
// 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 (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
sm.uiHandler.PrintText("connecting..")
if wac != nil && wac.GetConnected() {
wac.Disconnect()
sm.StatusChannel <- StatusMsg{false, nil}
@@ -198,7 +242,12 @@ func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
if err != nil {
return fmt.Errorf("error saving session: %v\n", err)
}
//<-time.After(3 * time.Second)
//get initial battery state
sm.BatteryChannel <- BatteryMsg{
wac.Info.Battery,
wac.Info.Plugged,
false,
}
sm.StatusChannel <- StatusMsg{true, nil}
return nil
}
@@ -216,7 +265,9 @@ func (sm *SessionManager) disconnect() error {
// logout logs out the user, deletes session file
func (ub *SessionManager) logout() error {
ub.getConnection().Disconnect()
err := ub.getConnection().Logout()
ub.StatusChannel <- StatusMsg{false, err}
ub.uiHandler.PrintText("removing login data..")
return removeSession()
}
@@ -227,15 +278,18 @@ func (sm *SessionManager) execCommand(command Command) {
default:
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Unknown command: [-]" + cmd)
case "backlog":
if sm.currentReceiver == "" {
return
}
count := 10
if currentMsgs, ok := sm.db.textMessages[sm.currentReceiver]; ok {
if len(currentMsgs) > 0 {
firstMsg := currentMsgs[0]
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, firstMsg.Info.Id, firstMsg.Info.FromMe, false, sm)
if sm.currentReceiver != "" {
count := 10
if currentMsgs, ok := sm.db.textMessages[sm.currentReceiver]; ok {
if len(currentMsgs) > 0 {
firstMsg := currentMsgs[0]
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, firstMsg.Info.Id, firstMsg.Info.FromMe, false, sm)
}
} else {
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, "", false, false, sm)
}
} else {
sm.printCommandUsage("backlog", "-> only works in a chat")
}
case "login":
sm.uiHandler.PrintError(sm.login())
@@ -251,13 +305,33 @@ func (sm *SessionManager) execCommand(command Command) {
text := strings.Join(textParams, " ")
sm.sendText(command.Params[0], text)
} else {
sm.printCommandUsage("send", "[user-id[] [message text[]")
sm.printCommandUsage("send", "[chat-id[] [message text[]")
}
case "select":
if checkParam(command.Params, 1) {
sm.setCurrentReceiver(command.Params[0])
} else {
sm.printCommandUsage("select", "[user-id[]")
sm.printCommandUsage("select", "[chat-id[]")
}
case "read":
if sm.currentReceiver != "" {
// need to send message id, so get all (unread count)
// recent messages and send "read"
if chat, ok := sm.db.chats[sm.currentReceiver]; ok {
count := chat.Unread
msgs := sm.db.GetMessages(chat.Id)
length := len(msgs)
for idx, msg := range msgs {
if idx >= length-count {
sm.getConnection().Read(chat.Id, msg.Info.Id)
}
}
chat.Unread = 0
sm.db.chats[sm.currentReceiver] = chat
sm.uiHandler.SetChats(sm.db.GetChatIds())
}
} else {
sm.printCommandUsage("read", "-> only works in a chat")
}
case "info":
if checkParam(command.Params, 1) {
@@ -309,6 +383,7 @@ func (sm *SessionManager) execCommand(command Command) {
}
case "upload":
if sm.currentReceiver == "" {
sm.printCommandUsage("upload", "-> only works in a chat")
return
}
var err error
@@ -336,6 +411,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendimage":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendimage", "-> only works in a chat")
return
}
var err error
@@ -363,6 +439,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendvideo":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendvideo", "-> only works in a chat")
return
}
var err error
@@ -390,6 +467,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendaudio":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendaudio", "-> only works in a chat")
return
}
var err error
@@ -446,8 +524,9 @@ func (sm *SessionManager) execCommand(command Command) {
}
case "leave":
groupId := sm.currentReceiver
if checkParam(command.Params, 1) {
groupId = command.Params[0]
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
wac := sm.getConnection()
var err error
@@ -456,6 +535,133 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintText("left group " + groupId)
}
sm.uiHandler.PrintError(err)
case "create":
if !checkParam(command.Params, 1) {
sm.printCommandUsage("create", "[user-id[] [user-id[] New Group Subject")
sm.printCommandUsage("create", "New Group Subject")
return
}
// first params are users if ending in CONTACTSUFFIX, rest is name
users := []string{}
idx := 0
size := len(command.Params)
for idx = 0; idx < size && strings.Index(command.Params[idx], CONTACTSUFFIX) > 0; idx++ {
users = append(users, command.Params[idx])
}
name := ""
if len(command.Params) > idx {
name = strings.Join(command.Params[idx:], " ")
}
wac := sm.getConnection()
var err error
var groupId <-chan string
groupId, err = wac.CreateGroup(name, users)
if err == nil {
sm.uiHandler.PrintText("creating new group " + name)
resultInfo := <-groupId
//{"status":200,"gid":"491600000009-0606000436@g.us","participants":[{"491700000000@c.us":{"code":"200"}},{"4917600000001@c.us":{"code": "200"}}]}
var result map[string]interface{}
json.Unmarshal([]byte(resultInfo), &result)
newChatId := result["gid"].(string)
sm.uiHandler.PrintText("got new Id " + newChatId)
newChat := Chat{}
newChat.Id = newChatId
newChat.Name = name
newChat.IsGroup = true
sm.db.chats[newChatId] = newChat
sm.uiHandler.SetChats(sm.db.GetChatIds())
}
sm.uiHandler.PrintError(err)
case "add":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) {
sm.printCommandUsage("add", "[user-id[]")
return
}
wac := sm.getConnection()
var err error
_, err = wac.AddMember(groupId, command.Params)
if err == nil {
sm.uiHandler.PrintText("added new members for " + groupId)
}
sm.uiHandler.PrintError(err)
case "remove":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) {
sm.printCommandUsage("remove", "[user-id[]")
return
}
wac := sm.getConnection()
var err error
_, err = wac.RemoveMember(groupId, command.Params)
if err == nil {
sm.uiHandler.PrintText("removed from " + groupId)
}
sm.uiHandler.PrintError(err)
case "removeadmin":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) {
sm.printCommandUsage("removeadmin", "[user-id[]")
return
}
wac := sm.getConnection()
var err error
_, err = wac.RemoveAdmin(groupId, command.Params)
if err == nil {
sm.uiHandler.PrintText("removed admin for " + groupId)
}
sm.uiHandler.PrintError(err)
case "admin":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) {
sm.printCommandUsage("admin", "[user-id[]")
return
}
wac := sm.getConnection()
var err error
_, err = wac.SetAdmin(groupId, command.Params)
if err == nil {
sm.uiHandler.PrintText("added admin for " + groupId)
}
sm.uiHandler.PrintError(err)
case "subject":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) || groupId == "" {
sm.printCommandUsage("subject", "new-subject -> in group chat")
return
}
name := strings.Join(command.Params, " ")
wac := sm.getConnection()
var err error
_, err = wac.UpdateGroupSubject(name, groupId)
if err == nil {
sm.uiHandler.PrintText("updated subject for " + groupId)
}
newChat := sm.db.chats[groupId]
newChat.Name = name
sm.db.chats[groupId] = newChat
sm.uiHandler.SetChats(sm.db.GetChatIds())
sm.uiHandler.PrintError(err)
case "colorlist":
out := ""
for idx, _ := range tcell.ColorNames {
@@ -478,35 +684,7 @@ func checkParam(arr []string, length int) bool {
return true
}
// gets a pretty name for a whatsapp id
func (sm *SessionManager) getIdName(id string) string {
if val, ok := sm.getConnection().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(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
// gets a short name for a whatsapp id
func (sm *SessionManager) getIdShort(id string) string {
if val, ok := sm.getConnection().Store.Contacts[id]; ok {
if val.Short != "" {
return val.Short
} else if val.Name != "" {
return val.Name
} else if val.Notify != "" {
return val.Notify
}
}
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
// get all messages for one contact id
// get all messages for one chat id
func (sm *SessionManager) getMessages(wid string) []Message {
msgs := sm.db.GetMessages(wid)
ids := []Message{}
@@ -517,22 +695,27 @@ func (sm *SessionManager) getMessages(wid string) []Message {
}
// create internal message from whatsapp message
// TODO: store these instead of generating each time
func (sm *SessionManager) createMessage(msg *whatsapp.TextMessage) Message {
newMsg := Message{}
newMsg.Id = msg.Info.Id
newMsg.SourceId = msg.Info.RemoteJid
newMsg.ChatId = msg.Info.RemoteJid
newMsg.FromMe = msg.Info.FromMe
newMsg.Timestamp = msg.Info.Timestamp
newMsg.Text = msg.Text
if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) {
newMsg.Forwarded = msg.ContextInfo.IsForwarded
if strings.Contains(msg.Info.RemoteJid, STATUSSUFFIX) {
newMsg.ContactId = msg.Info.SenderJid
newMsg.SourceName = sm.getIdName(msg.Info.SenderJid)
newMsg.SourceShort = sm.getIdShort(msg.Info.SenderJid)
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) {
newMsg.ContactId = msg.Info.SenderJid
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
} else {
newMsg.ContactId = msg.Info.RemoteJid
newMsg.SourceName = sm.getIdName(msg.Info.RemoteJid)
newMsg.SourceShort = sm.getIdShort(msg.Info.RemoteJid)
newMsg.ContactName = sm.db.GetIdName(msg.Info.RemoteJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.RemoteJid)
}
return newMsg
}
@@ -655,7 +838,6 @@ func (sm *SessionManager) sendText(wid string, text string) {
// HandleError implements the error handler interface for go-whatsapp
func (sm *SessionManager) HandleError(err error) {
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]go-whatsapp reported an error:[-]")
sm.uiHandler.PrintError(err)
statusMsg := StatusMsg{false, err}
sm.StatusChannel <- statusMsg
@@ -731,7 +913,7 @@ func (sm *SessionManager) HandleAudioMessage(message whatsapp.AudioMessage) {
// add contact info to database (not needed, internal db of connection is used)
func (sm *SessionManager) HandleNewContact(contact whatsapp.Contact) {
// redundant, wac has contacts
//contactChannel <- contact
sm.ContactChannel <- contact
}
// handle battery messages
@@ -739,6 +921,22 @@ func (sm *SessionManager) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
sm.BatteryChannel <- BatteryMsg{msg.Percentage, msg.Plugged, msg.Powersave}
}
func (sm *SessionManager) HandleContactList(contacts []whatsapp.Contact) {
for _, c := range contacts {
sm.ContactChannel <- c
}
}
func (sm *SessionManager) HandleChatList(chats []whatsapp.Chat) {
for _, c := range chats {
sm.ChatChannel <- c
}
}
func (sm *SessionManager) HandleJsonMessage(message string) {
//sm.uiHandler.PrintText(message)
}
// helper to save an attachment and open it if specified
func saveAttachment(data []byte, path string) (string, error) {
err := ioutil.WriteFile(path, data, 0644)

View File

@@ -2,6 +2,7 @@ package messages
import (
"sort"
"strings"
"github.com/Rhymen/go-whatsapp"
)
@@ -11,6 +12,8 @@ type MessageDatabase struct {
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
contacts map[string]Contact
chats map[string]Chat
}
// initialize the database
@@ -20,6 +23,8 @@ func (db *MessageDatabase) Init() {
db.messagesById = make(map[string]*whatsapp.TextMessage)
db.otherMessages = make(map[string]*interface{})
db.latestMessage = make(map[string]uint64)
db.contacts = make(map[string]Contact)
db.chats = make(map[string]Chat)
}
// add a text message to the database, stored by RemoteJid
@@ -36,6 +41,18 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
db.latestMessage[wid] = msg.Info.Timestamp
didNew = true
}
//do we know this chat? if not add
if _, ok := db.chats[msg.Info.RemoteJid]; !ok {
//don't have this chat!
isGroup := strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX)
db.chats[msg.Info.RemoteJid] = Chat{
msg.Info.RemoteJid,
isGroup,
db.GetIdName(msg.Info.RemoteJid),
1,
int64(msg.Info.Timestamp),
}
}
//check if message exists, ignore otherwise
if _, ok := db.messagesById[msg.Info.Id]; !ok {
db.messagesById[msg.Info.Id] = msg
@@ -47,6 +64,13 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
return didNew
}
func (db *MessageDatabase) NewUnreadChat(id string) {
if chat, ok := db.chats[id]; ok {
chat.Unread++
db.chats[id] = chat
}
}
// add audio/video/image/doc message, stored by message id
func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
var id = ""
@@ -66,21 +90,53 @@ func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
}
}
func (db *MessageDatabase) AddContact(contact Contact) {
db.contacts[contact.Id] = contact
}
func (db *MessageDatabase) AddChat(chat Chat) {
db.chats[chat.Id] = chat
}
// get an array of all chat ids
func (db *MessageDatabase) GetContactIds() []string {
//var this = *db
keys := make([]string, len(db.textMessages))
func (db *MessageDatabase) GetChatIds() []Chat {
keys := make([]Chat, len(db.chats))
i := 0
for k := range db.textMessages {
for _, k := range db.chats {
keys[i] = k
i++
}
sort.Slice(keys, func(i, j int) bool {
return db.latestMessage[keys[i]] > db.latestMessage[keys[j]]
return db.latestMessage[keys[i].Id] > db.latestMessage[keys[j].Id]
})
return keys
}
// gets a pretty name for a whatsapp id
func (sm *MessageDatabase) GetIdName(id string) string {
if val, ok := sm.contacts[id]; ok {
if val.Name != "" {
return val.Name
} else if val.Short != "" {
return val.Short
}
}
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
// gets a short name for a whatsapp id
func (sm *MessageDatabase) GetIdShort(id string) string {
if val, ok := sm.contacts[id]; ok {
//TODO val.notify from whatsapp??
if val.Short != "" {
return val.Short
} else if val.Name != "" {
return val.Name
}
}
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
func (db *MessageDatabase) GetMessageInfo(id string) string {
if _, ok := db.otherMessages[id]; ok {
return "[yellow]OtherMessage[-]"