Compare commits
	
		
			25 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f79e3e46b7 | ||
| 
						 | 
					3053d15929 | ||
| 
						 | 
					73cd749394 | ||
| 
						 | 
					8e8f2da43f | ||
| 
						 | 
					e8d9f266fa | ||
| 
						 | 
					2763bb14b4 | ||
| 
						 | 
					f91ffbef0f | ||
| 
						 | 
					3521a3b6f5 | ||
| 
						 | 
					a6d7954795 | ||
| 
						 | 
					96e9d75810 | ||
| 
						 | 
					b2904929b0 | ||
| 
						 | 
					033e7aa1ac | ||
| 
						 | 
					53e404dd55 | ||
| 
						 | 
					8318d2e80f | ||
| 
						 | 
					3af2a3738f | ||
| 
						 | 
					e31c2c3e36 | ||
| 
						 | 
					3f20981550 | ||
| 
						 | 
					f8368e4998 | ||
| 
						 | 
					121a73c312 | ||
| 
						 | 
					86af0d82a4 | ||
| 
						 | 
					b94129fb0e | ||
| 
						 | 
					59a843bb8d | ||
| 
						 | 
					a86aa3eec3 | ||
| 
						 | 
					a20b4e3592 | ||
| 
						 | 
					9e62295188 | 
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							@@ -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
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.sum
									
									
									
									
									
								
							@@ -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
									
									
									
									
									
								
							
							
						
						
									
										228
									
								
								main.go
									
									
									
									
									
								
							@@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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[-]"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user