Compare commits
	
		
			61 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fd676f13cf | ||
|   | 63b5f3a604 | ||
|   | b20031ff6a | ||
|   | 00516c3191 | ||
|   | 76c4010ce2 | ||
|   | 0b8d265024 | ||
|   | 80825b0dff | ||
|   | 42daf1a9f7 | ||
|   | 04960123da | ||
|   | ab03aeb7a0 | ||
|   | f6860b56b8 | ||
|   | 6bea425367 | ||
|   | 1b3379b613 | ||
|   | 1cc68c9203 | ||
|   | c3a6f98b13 | ||
|   | 21fec5a5d0 | ||
|   | b2b798c514 | ||
|   | 5d63cf41eb | ||
|   | e519c4a892 | ||
|   | 754106de62 | ||
|   | 07474c5b3d | ||
|   | b142aef93c | ||
|   | babe81c1aa | ||
|   | b89ecfec79 | ||
|   | 5e7659b845 | ||
|   | eee49d713d | ||
|   | 2a6b0efe68 | ||
|   | 9ee28a011c | ||
|   | 16a6519ba7 | ||
|   | bb00a9a8f0 | ||
|   | 4ef8b76a55 | ||
|   | 6a832cbcf3 | ||
|   | c2f3a012dc | ||
|   | 147feb90ec | ||
|   | b39d4891e0 | ||
|   | 19455b0baa | ||
|   | a37e6c7d3b | ||
|   | 82d2a6637d | ||
|   | 1eb3af823e | ||
|   | 773f9b783d | ||
|   | e290a79064 | ||
|   | 7ae3967fbc | ||
|   | 5045ac59f5 | ||
|   | 2db7e51327 | ||
|   | e698b66819 | ||
|   | 7fdafb477a | ||
|   | 13632156a1 | ||
|   | 08b69673e3 | ||
|   | 36b8bdac2f | ||
|   | 43c5795cc7 | ||
|   | 2063aeec91 | ||
|   | 310e9fa2ab | ||
|   | c860542052 | ||
|   | 172fad115b | ||
|   | cfd41b2de6 | ||
|   | 6eb872ef8a | ||
|   | 3f5ec9ff8c | ||
|   | 48fca0e6ad | ||
|   | 28d06730c7 | ||
|   | fff039e776 | ||
|   | 82f6795aef | 
							
								
								
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| # These are supported funding model platforms | ||||
|  | ||||
| github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] | ||||
| open_collective: #normen | ||||
| ko_fi: normen | ||||
| patreon: normen | ||||
| tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel | ||||
| community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry | ||||
| liberapay: # Replace with a single Liberapay username | ||||
| issuehunt: # Replace with a single IssueHunt username | ||||
| otechie: # Replace with a single Otechie username | ||||
| custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] | ||||
							
								
								
									
										22
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # Simple Makefile for go | ||||
|  | ||||
| build: | ||||
| 	go build | ||||
|  | ||||
| clean: | ||||
| 	go clean | ||||
|  | ||||
| run: | ||||
| 	go run . | ||||
|  | ||||
| install: | ||||
| 	go install . | ||||
|  | ||||
| get: | ||||
| 	go get | ||||
|  | ||||
| update: | ||||
| 	go get -u | ||||
|  | ||||
| release: | ||||
| 	./release.sh | ||||
							
								
								
									
										76
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,66 +1,52 @@ | ||||
| # whatscli | ||||
|  | ||||
| A command line interface for whatsapp, based on go-whatsapp and tview | ||||
| A command line interface for whatsapp, based on [go-whatsapp](https://github.com/Rhymen/go-whatsapp) and [tview](https://github.com/rivo/tview) | ||||
|  | ||||
| ``` | ||||
| ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐ | ||||
| │ WhatsCLI v0.1.0  Help: /name  = name contact | /quit = exit app | /load = reload contacts | <Tab> =│ | ||||
| ├──────────────────────────────┬─────────────────────────────────────────────────────────────────────┤ | ||||
| │Contacts                      │                                                                     │ | ||||
| │├──Normen                     │(03-22-13 21:18:24) Daniel: Sachste bescheid wenn der kram vorbei ist│ | ||||
| │├──Lilou                      │? :)                                                                 │ | ||||
| │├──Boris                      │(03-22-13 21:37:29) Ich: Jo                                          │ | ||||
| │├──Malte                      │(03-22-13 22:02:13) Daniel: Mensch das geht ja lang..                │ | ||||
| │├──Daniel                     │(03-22-13 22:02:53) Ich: Nu is schulz                                │ | ||||
| │├──Seb                        │(03-22-13 22:03:08) Daniel: Jo bis gleich !                          │ | ||||
| │├──Bettina                    │(04-07-14 18:06:15) Daniel: Hey wie schauts :)                       │ | ||||
| │├──Elternbeirat               │(04-07-14 18:07:56) Ich: Ich komme laut Navigationssystem um 19:40   │ | ||||
| │├──4911758758720-1565273421@g.│Uhr an                                                               │ | ||||
| │├──491111250015@s.whatsapp.net│(04-07-14 18:08:33) Daniel: Sauber, ruf ma so ca 10 min vorher durch │ | ||||
| │├──4911758758714-1537904683@g.│dann bin ich da :)                                                   │ | ||||
| │├──491114171906-1448397341@g.u│(04-07-14 19:24:21) Ich: Bin jetzt in Bremen. Circa 10 Minuten       │ | ||||
| │├──491192855547-1561396191@g.u│(04-07-14 19:24:45) Daniel: Ok mach mich los                         │ | ||||
| │├──491152456088@s.whatsapp.net│(07-27-14 19:14:19) Ich: Moin do. Sag' mal bist Du morgen um fünf in │ | ||||
| │├──491107382606-1364411990@g.u│Bremen? Ich bräuchte jemanden um das Mischpult etc. wieder in einen  │ | ||||
| │├──491111250017-1603197565@g.u│LKW zu laden..                                                       │ | ||||
| │├──491131942996@s.whatsapp.net│(07-27-14 19:15:22) Daniel: Ich bin unterwegs, sorry                 │ | ||||
| │├──491122978981@s.whatsapp.net│(07-27-14 19:15:52) Ich: Kein Ding, danke!                           │ | ||||
| │├──491192855528@s.whatsapp.net│(07-27-14 19:24:50) Daniel: Jou bin quasi noch im breminale stress :)│ | ||||
| │├──491154447429@s.whatsapp.net│(07-27-14 19:25:12) Ich: Na dann noch viel Spass :)                  │ | ||||
| │├──491132457405-1526385826@g.u│(07-27-14 19:25:34) Daniel: Ja danke, bin froh wenns vorbei is ;)    │ | ||||
| │├──491103663035@s.whatsapp.net│(07-27-14 19:26:12) Ich: Augen zu und durch, Lock'n'Loll             │ | ||||
| │├──491113075747@s.whatsapp.net│(11-15-20 15:27:06) Ich: testjj                                      │ | ||||
| │├──491147048885@s.whatsapp.net├─────────────────────────────────────────────────────────────────────┤ | ||||
| │├──491124146101@s.whatsapp.net│                                                                     │ | ||||
| └──────────────────────────────┴─────────────────────────────────────────────────────────────────────┘ | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Features | ||||
|  | ||||
| Things that work. | ||||
|  | ||||
| - Allows sending and receiving WhatsApp messages in a CLI interface | ||||
| - Connects through Web App API without browser | ||||
| - Connects through the Web App API without a browser | ||||
| - Allows sending and receiving WhatsApp messages in a command line app | ||||
| - Allows downloading and opening image/video/audio/document attachments | ||||
| - Uses QR code for simple setup | ||||
| - Binaries for Windows, Mac, Linux and RaspBerry Pi | ||||
|  | ||||
| ### Caveats | ||||
|  | ||||
| This is a WIP. Heres some things you might expect to work that don't. Plus some other things I should mention. | ||||
| 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. | ||||
|  | ||||
| - Only lists contacts and groups that have been messaged on phone | ||||
| - Only fetches a few messages for last contacted | ||||
| - To display names they have to be entered through the `/name` or `/addname` commands for each contact | ||||
| - No support for images, videos, documents etc | ||||
| - Only shows existing chats | ||||
| - Only fetches a few old messages | ||||
| - No incoming message notification / count | ||||
| - Not configurable at all | ||||
| - Leaves its config files in your home folder | ||||
| - FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when they change stuff in their web app | ||||
| - No proper connection drop handling | ||||
| - No uploading of images/video/audio/data | ||||
| - 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 | ||||
|  | ||||
| How to get it running and use it | ||||
| How to get it running and how to use it | ||||
|  | ||||
| ### Latest Release | ||||
|  | ||||
| Always fresh, always up to date. | ||||
|  | ||||
| - Download a release | ||||
| - Put the binary in your PATH (optional) | ||||
| - Run with `whatscli` (or double-click) | ||||
| - Scan QR code with WhatsApp on phone (maybe resize shell) | ||||
| - Scan the QR code with WhatsApp on your phone (resize shell or change font size to see whole code) | ||||
|  | ||||
| ### Package Managers | ||||
|  | ||||
| Some ways to install via package managers are supported but the installed version might be out of date. | ||||
|  | ||||
| #### MacOS (homebrew) | ||||
|  | ||||
| - `brew install normen/tap/whatscli` | ||||
|  | ||||
| #### Arch Linux (AUR) | ||||
|  | ||||
| - `https://aur.archlinux.org/packages/whatscli/` | ||||
|  | ||||
|   | ||||
							
								
								
									
										128
									
								
								config/settings.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								config/settings.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/user" | ||||
|  | ||||
| 	"github.com/adrg/xdg" | ||||
| 	"github.com/gdamore/tcell/v2" | ||||
| 	"gitlab.com/tslocum/cbind" | ||||
| 	"gopkg.in/ini.v1" | ||||
| ) | ||||
|  | ||||
| var configFilePath string | ||||
| var keyConfig *cbind.Configuration | ||||
| var cfg *ini.File | ||||
|  | ||||
| func InitConfig() { | ||||
| 	var err error | ||||
| 	if configFilePath, err = xdg.ConfigFile("whatscli/whatscli.config"); err == nil { | ||||
| 		if cfg, err = ini.Load(configFilePath); err == nil { | ||||
| 			//TODO: check config for new parameters | ||||
| 		} else { | ||||
| 			cfg = ini.Empty() | ||||
| 			cfg.NewSection("general") | ||||
| 			cfg.Section("general").NewKey("download_path", GetHomeDir()+"Downloads") | ||||
| 			cfg.Section("general").NewKey("preview_path", GetHomeDir()+"Downloads") | ||||
| 			cfg.NewSection("keymap") | ||||
| 			cfg.Section("keymap").NewKey("switch_panels", "Tab") | ||||
| 			cfg.Section("keymap").NewKey("focus_messages", "Ctrl+w") | ||||
| 			cfg.Section("keymap").NewKey("focus_input", "Ctrl+Space") | ||||
| 			cfg.Section("keymap").NewKey("focus_contacts", "Ctrl+e") | ||||
| 			cfg.Section("keymap").NewKey("command_connect", "Ctrl+r") | ||||
| 			cfg.Section("keymap").NewKey("command_quit", "Ctrl+q") | ||||
| 			cfg.Section("keymap").NewKey("command_help", "Ctrl+?") | ||||
| 			cfg.Section("keymap").NewKey("message_download", "d") | ||||
| 			cfg.Section("keymap").NewKey("message_open", "o") | ||||
| 			cfg.Section("keymap").NewKey("message_show", "s") | ||||
| 			cfg.Section("keymap").NewKey("message_info", "i") | ||||
| 			cfg.NewSection("ui") | ||||
| 			cfg.Section("ui").NewKey("contact_sidebar_width", "30") | ||||
| 			cfg.NewSection("colors") | ||||
| 			cfg.Section("colors").NewKey("background", "black") | ||||
| 			cfg.Section("colors").NewKey("text", "white") | ||||
| 			cfg.Section("colors").NewKey("list_header", "yellow") | ||||
| 			cfg.Section("colors").NewKey("list_contact", "green") | ||||
| 			cfg.Section("colors").NewKey("list_group", "blue") | ||||
| 			cfg.Section("colors").NewKey("chat_contact", "green") | ||||
| 			cfg.Section("colors").NewKey("chat_me", "blue") | ||||
| 			err = cfg.SaveTo(configFilePath) | ||||
| 		} | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		fmt.Printf(err.Error()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func GetConfigFilePath() string { | ||||
| 	return configFilePath | ||||
| } | ||||
|  | ||||
| func GetSessionFilePath() string { | ||||
| 	if sessionFilePath, err := xdg.ConfigFile("whatscli/session"); err == nil { | ||||
| 		return sessionFilePath | ||||
| 	} | ||||
| 	return GetHomeDir() + ".whatscli.session" | ||||
| } | ||||
|  | ||||
| func GetContactsFilePath() string { | ||||
| 	if sessionFilePath, err := xdg.ConfigFile("whatscli/contacts"); err == nil { | ||||
| 		return sessionFilePath | ||||
| 	} | ||||
| 	return GetHomeDir() + ".whatscli.session" | ||||
| } | ||||
|  | ||||
| func GetKey(name string) string { | ||||
| 	if sec, err := cfg.GetSection("keymap"); err == nil { | ||||
| 		if key, err := sec.GetKey(name); err == nil { | ||||
| 			return key.String() | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func GetColorName(key string) string { | ||||
| 	if sec, err := cfg.GetSection("colors"); err == nil { | ||||
| 		if key, err := sec.GetKey(key); err == nil { | ||||
| 			return key.String() | ||||
| 		} | ||||
| 	} | ||||
| 	return "white" | ||||
| } | ||||
|  | ||||
| func GetColor(key string) tcell.Color { | ||||
| 	name := GetColorName(key) | ||||
| 	if color, ok := tcell.ColorNames[name]; ok { | ||||
| 		return color | ||||
| 	} | ||||
| 	return tcell.ColorWhite | ||||
| } | ||||
|  | ||||
| func GetSetting(name string) string { | ||||
| 	if sec, err := cfg.GetSection("general"); err == nil { | ||||
| 		if key, err := sec.GetKey(name); err == nil { | ||||
| 			return key.String() | ||||
| 		} | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
|  | ||||
| func GetIntSetting(section string, name string) int { | ||||
| 	if sec, err := cfg.GetSection(section); err == nil { | ||||
| 		if key, err := sec.GetKey(name); err == nil { | ||||
| 			if val, err := key.Int(); err == nil { | ||||
| 				return val | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return 0 | ||||
| } | ||||
|  | ||||
| // gets the OS home dir with a path separator at the end | ||||
| func GetHomeDir() string { | ||||
| 	usr, err := user.Current() | ||||
| 	if err != nil { | ||||
| 	} | ||||
| 	return usr.HomeDir + string(os.PathSeparator) | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								doc/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/screenshot.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 421 KiB | 
							
								
								
									
										22
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								go.mod
									
									
									
									
									
								
							| @@ -3,11 +3,21 @@ module github.com/normen/whatscli | ||||
| go 1.15 | ||||
|  | ||||
| require ( | ||||
| 	github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f | ||||
| 	github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a | ||||
| 	github.com/gdamore/tcell v1.4.0 | ||||
| 	github.com/Rhymen/go-whatsapp v0.1.1 | ||||
| 	github.com/adrg/xdg v0.2.3 | ||||
| 	github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 | ||||
| 	github.com/mattn/go-colorable v0.1.1 | ||||
| 	github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 | ||||
| 	github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 | ||||
| 	github.com/golang/protobuf v1.4.3 // indirect | ||||
| 	github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect | ||||
| 	github.com/gorilla/websocket v1.4.2 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.8 | ||||
| 	github.com/pkg/errors v0.9.1 // indirect | ||||
| 	github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893 | ||||
| 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e | ||||
| 	github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 | ||||
| 	gitlab.com/tslocum/cbind v0.1.3 | ||||
| 	golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect | ||||
| 	golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect | ||||
| 	golang.org/x/text v0.3.4 // indirect | ||||
| 	google.golang.org/protobuf v1.25.0 // indirect | ||||
| 	gopkg.in/ini.v1 v1.62.0 | ||||
| ) | ||||
|   | ||||
							
								
								
									
										125
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										125
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,57 +1,140 @@ | ||||
| github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f h1:2dk3eOnYllh+wUOuDhOoC2vUVoJF/5z478ryJ+wzEII= | ||||
| cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= | ||||
| github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= | ||||
| github.com/Rhymen/go-whatsapp v0.1.0 h1:XTXhFIQ/fx9jKObUnUX2Q+nh58EyeHNhX7DniE8xeuA= | ||||
| github.com/Rhymen/go-whatsapp v0.1.0/go.mod h1:xJSy+okeRjKkQEH/lEYrnekXB3PG33fqL0I6ncAkV50= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a h1:LW+rX0NY6LzMPa2hJcgmQlfiFJUihzOMAaIoCq+P3xc= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1-0.20201007125822-005103751b7a/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1 h1:OK+bCugQcr2YjyYKeDzULqCtM50TPUFM6LvQtszKfcw= | ||||
| github.com/Rhymen/go-whatsapp v0.1.1/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk= | ||||
| github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME= | ||||
| github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU= | ||||
| github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw= | ||||
| github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM= | ||||
| github.com/adrg/xdg v0.2.3 h1:GxXngdYxNDkoUvZXjNJGwqZxWXi43MKbOOlA/00qZi4= | ||||
| github.com/adrg/xdg v0.2.3/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= | ||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||
| github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
| github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | ||||
| github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= | ||||
| github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= | ||||
| github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | ||||
| github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= | ||||
| github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= | ||||
| github.com/gdamore/tcell/v2 v2.0.0-dev/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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= | ||||
| github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= | ||||
| github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= | ||||
| github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= | ||||
| github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= | ||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||
| github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= | ||||
| github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= | ||||
| github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= | ||||
| github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= | ||||
| github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= | ||||
| github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= | ||||
| github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= | ||||
| github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||
| github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||
| github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= | ||||
| github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= | ||||
| github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= | ||||
| github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= | ||||
| github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | ||||
| github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= | ||||
| github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= | ||||
| github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= | ||||
| github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= | ||||
| github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||||
| github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= | ||||
| github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
| github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= | ||||
| github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= | ||||
| github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= | ||||
| github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/rivo/tview v0.0.0-20201018122409-d551c850a743 h1:9BBjVJTRxuYBeCAv9DFH2hSzY0ujLx5sxMg5D3K/Xeg= | ||||
| github.com/rivo/tview v0.0.0-20201018122409-d551c850a743/go.mod h1:t7mcA3nlK9dxD1DMoz/DQRMWFMkGBUj6rJBM5VNfLFA= | ||||
| github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= | ||||
| github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= | ||||
| github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= | ||||
| github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893 h1:24As98PZlIdjZn6V4wUulAbYlG7RPg/du9A1FZdT/vs= | ||||
| github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY= | ||||
| github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= | ||||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | ||||
| github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= | ||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= | ||||
| github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= | ||||
| github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= | ||||
| github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||
| gitlab.com/tslocum/cbind v0.1.3 h1:FT/fTQ4Yj3eo5021lB3IbkIt8eVtYGhrw/xur+cjvUU= | ||||
| gitlab.com/tslocum/cbind v0.1.3/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 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= | ||||
| 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/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= | ||||
| golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7 h1:XtNJkfEjb4zR3q20BBBcYUykVOEMgZeIUOpBPfNYgxg= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-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/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 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= | ||||
| golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= | ||||
| golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= | ||||
| golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= | ||||
| google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= | ||||
| google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= | ||||
| google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= | ||||
| google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= | ||||
| google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= | ||||
| google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= | ||||
| google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= | ||||
| google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= | ||||
| google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= | ||||
| google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= | ||||
| google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= | ||||
| google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= | ||||
| google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= | ||||
| google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= | ||||
| google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= | ||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= | ||||
| gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||
| honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
| honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= | ||||
|   | ||||
							
								
								
									
										616
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										616
									
								
								main.go
									
									
									
									
									
								
							| @@ -1,13 +1,20 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"fmt" | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/gdamore/tcell/v2" | ||||
| 	"github.com/normen/whatscli/messages" | ||||
| 	"github.com/rivo/tview" | ||||
| 	"io" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/gdamore/tcell/v2" | ||||
| 	"github.com/normen/whatscli/config" | ||||
| 	"github.com/normen/whatscli/messages" | ||||
| 	"github.com/rivo/tview" | ||||
| 	"github.com/skratchdot/open-golang/open" | ||||
| 	"gitlab.com/tslocum/cbind" | ||||
| ) | ||||
|  | ||||
| type waMsg struct { | ||||
| @@ -15,64 +22,48 @@ type waMsg struct { | ||||
| 	Text string | ||||
| } | ||||
|  | ||||
| var VERSION string = "v0.4.1" | ||||
| var VERSION string = "v0.7.3" | ||||
|  | ||||
| var sendChannel chan waMsg | ||||
| var textChannel chan whatsapp.TextMessage | ||||
| var otherChannel chan interface{} | ||||
| var contactChannel chan whatsapp.Contact | ||||
|  | ||||
| var sndTxt string = "" | ||||
| var currentReceiver string = "" | ||||
| var curRegions []string | ||||
| var textView *tview.TextView | ||||
| var treeView *tview.TreeView | ||||
| var textInput *tview.InputField | ||||
| var topBar *tview.TextView | ||||
|  | ||||
| //var infoBar *tview.TextView | ||||
| var connection *whatsapp.Conn | ||||
| var msgStore messages.MessageDatabase | ||||
| var keysApp *cbind.Configuration | ||||
|  | ||||
| var contactRoot *tview.TreeNode | ||||
| var handler textHandler | ||||
| var app *tview.Application | ||||
|  | ||||
| //var messages map[string]string | ||||
|  | ||||
| func main() { | ||||
| 	config.InitConfig() | ||||
| 	msgStore = messages.MessageDatabase{} | ||||
| 	msgStore.Init() | ||||
| 	messages.LoadContacts() | ||||
| 	app = tview.NewApplication() | ||||
|  | ||||
| 	sideBarWidth := config.GetIntSetting("ui", "contact_sidebar_width") | ||||
| 	gridLayout := tview.NewGrid() | ||||
| 	gridLayout.SetRows(1, 0, 1) | ||||
| 	gridLayout.SetColumns(30, 0, 30) | ||||
| 	gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth) | ||||
| 	gridLayout.SetBorders(true) | ||||
|  | ||||
| 	//list := tview.NewList() | ||||
| 	////list.SetTitle("Contacts") | ||||
| 	////list.AddItem("List Contacts", "get the contacts", 'a', func() { | ||||
| 	////  list.Clear() | ||||
| 	////  var ids = msgStore.GetContactIds() | ||||
| 	////  for _, element := range ids { | ||||
| 	////    //fmt.Fprint(textView, "\n"+element) | ||||
| 	////    var elem = element | ||||
| 	////    list.AddItem(messages.GetIdName(element), "", '-', func() { | ||||
| 	////      currentReceiver = elem | ||||
| 	////      textView.Clear() | ||||
| 	////      textView.SetText(msgStore.GetMessagesString(elem)) | ||||
| 	////      fmt.Fprint(textView, "\nNeuer Empfänger: ", elem) | ||||
| 	////    }) | ||||
| 	////  } | ||||
| 	////}) | ||||
| 	//list.ShowSecondaryText(false) | ||||
| 	//list.AddItem("Load", "Load Contacts", 'l', LoadContacts) | ||||
| 	//list.AddItem("Quit", "Press to exit", 'q', func() { | ||||
| 	//  app.Stop() | ||||
| 	//}) | ||||
| 	gridLayout.SetBackgroundColor(config.GetColor("background")) | ||||
|  | ||||
| 	topBar = tview.NewTextView() | ||||
| 	topBar.SetDynamicColors(true) | ||||
| 	topBar.SetText("[::b] WhatsCLI " + VERSION + "  [-::d]Help: /name NewName | /addname 123456 NewName | /quit | <Tab> = contacts/message | <Up/Dn> = scroll") | ||||
| 	topBar.SetScrollable(false) | ||||
| 	topBar.SetText("[::b] WhatsCLI " + VERSION + "  [-::d]Type /help for help") | ||||
| 	topBar.SetBackgroundColor(config.GetColor("background")) | ||||
|  | ||||
| 	//infoBar = tview.NewTextView() | ||||
| 	//infoBar.SetDynamicColors(true) | ||||
| @@ -85,21 +76,21 @@ func main() { | ||||
| 		SetChangedFunc(func() { | ||||
| 			app.Draw() | ||||
| 		}) | ||||
| 	textView.SetBackgroundColor(config.GetColor("background")) | ||||
| 	textView.SetTextColor(config.GetColor("text")) | ||||
|  | ||||
| 	fmt.Fprint(textView, "[::b]WhatsCLI "+VERSION+"\n\n[-][-::u]Commands:[-::-]\n/name NewName = name current contact\n/addname number NewName = name by number\n/load = reload contacts\n/quit = exit app\n\n[-::u]Keys:[-::-]\n<Tab> = switch input/contacts\n<Up/Dn> = scroll history") | ||||
|  | ||||
| 	//textView.SetBorder(true) | ||||
| 	// TODO: add better way | ||||
| 	messages.SetTextView(textView) | ||||
| 	PrintHelp() | ||||
|  | ||||
| 	textInput = tview.NewInputField() | ||||
| 	textInput.SetBackgroundColor(config.GetColor("background")) | ||||
| 	textInput.SetFieldTextColor(config.GetColor("text")) | ||||
| 	textInput.SetChangedFunc(func(change string) { | ||||
| 		sndTxt = change | ||||
| 	}) | ||||
| 	textInput.SetDoneFunc(EnterCommand) | ||||
| 	textInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { | ||||
| 		if event.Key() == tcell.KeyTab { | ||||
| 			app.SetFocus(treeView) | ||||
| 			return nil | ||||
| 		} | ||||
| 		if event.Key() == tcell.KeyDown { | ||||
| 			offset, _ := textView.GetScrollOffset() | ||||
| 			offset += 1 | ||||
| @@ -138,68 +129,22 @@ func main() { | ||||
| 	app.SetFocus(textInput) | ||||
| 	go func() { | ||||
| 		if err := StartTextReceiver(); err != nil { | ||||
| 			fmt.Fprint(textView, err) | ||||
| 			PrintError(err) | ||||
| 		} | ||||
| 	}() | ||||
| 	LoadShortcuts() | ||||
| 	app.Run() | ||||
| } | ||||
|  | ||||
| func EnterCommand(key tcell.Key) { | ||||
| 	if sndTxt == "" { | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/load" { | ||||
| 		//command | ||||
| 		LoadContacts() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/quit" { | ||||
| 		//command | ||||
| 		app.Stop() | ||||
| 		return | ||||
| 	} | ||||
| 	if strings.Index(sndTxt, "/addname ") == 0 { | ||||
| 		//command | ||||
| 		parts := strings.Split(sndTxt, " ") | ||||
| 		if len(parts) < 3 { | ||||
| 			fmt.Fprint(textView, "\nUse /addname 1234567 NewName") | ||||
| 			return | ||||
| 		} | ||||
| 		messages.SetIdName(parts[1]+messages.CONTACTSUFFIX, strings.TrimPrefix(sndTxt, "/addname "+parts[1]+" ")) | ||||
| 		SetDisplayedContact(currentReceiver) | ||||
| 		LoadContacts() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if currentReceiver == "" { | ||||
| 		fmt.Fprint(textView, "\nNo recipient set") | ||||
| 		return | ||||
| 	} | ||||
| 	if strings.Index(sndTxt, "/name ") == 0 { | ||||
| 		//command | ||||
| 		messages.SetIdName(currentReceiver, strings.TrimPrefix(sndTxt, "/name ")) | ||||
| 		SetDisplayedContact(currentReceiver) | ||||
| 		LoadContacts() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	// send message | ||||
| 	msg := waMsg{ | ||||
| 		Wid:  currentReceiver, | ||||
| 		Text: sndTxt, | ||||
| 	} | ||||
| 	sendChannel <- msg | ||||
| 	textInput.SetText("") | ||||
| } | ||||
|  | ||||
| // creates the TreeView for contacts | ||||
| func MakeTree() *tview.TreeView { | ||||
| 	rootDir := "Contacts" | ||||
| 	contactRoot = tview.NewTreeNode(rootDir). | ||||
| 		SetColor(tcell.ColorYellow) | ||||
| 		SetColor(config.GetColor("list_header")) | ||||
| 	treeView = tview.NewTreeView(). | ||||
| 		SetRoot(contactRoot). | ||||
| 		SetCurrentNode(contactRoot) | ||||
| 	treeView.SetBackgroundColor(config.GetColor("background")) | ||||
|  | ||||
| 	// If a contact was selected, open it. | ||||
| 	treeView.SetChangedFunc(func(node *tview.TreeNode) { | ||||
| @@ -217,16 +162,425 @@ func MakeTree() *tview.TreeView { | ||||
| 			node.SetExpanded(!node.IsExpanded()) | ||||
| 		} | ||||
| 	}) | ||||
| 	treeView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { | ||||
| 		if event.Key() == tcell.KeyTab { | ||||
| 			app.SetFocus(textInput) | ||||
| 			return nil | ||||
| 		} | ||||
| 		return event | ||||
| 	}) | ||||
| 	return treeView | ||||
| } | ||||
|  | ||||
| func handleFocusMessage(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	if !textView.HasFocus() { | ||||
| 		app.SetFocus(textView) | ||||
| 		if curRegions != nil && len(curRegions) > 0 { | ||||
| 			textView.Highlight(curRegions[len(curRegions)-1]) | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleFocusInput(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	ResetMsgSelection() | ||||
| 	if !textInput.HasFocus() { | ||||
| 		app.SetFocus(textInput) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleFocusContacts(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	ResetMsgSelection() | ||||
| 	if !treeView.HasFocus() { | ||||
| 		app.SetFocus(treeView) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleSwitchPanels(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	ResetMsgSelection() | ||||
| 	if !textInput.HasFocus() { | ||||
| 		app.SetFocus(textInput) | ||||
| 	} else { | ||||
| 		app.SetFocus(treeView) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleConnect(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	msgStore.Init() | ||||
| 	messages.Login() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleQuit(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	messages.Disconnect() | ||||
| 	app.Stop() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleHelp(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	PrintHelp() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleDownload(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	hls := textView.GetHighlights() | ||||
| 	if len(hls) > 0 { | ||||
| 		go DownloadMessageId(hls[0], false) | ||||
| 		ResetMsgSelection() | ||||
| 		app.SetFocus(textInput) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleOpen(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	hls := textView.GetHighlights() | ||||
| 	if len(hls) > 0 { | ||||
| 		go DownloadMessageId(hls[0], true) | ||||
| 		ResetMsgSelection() | ||||
| 		app.SetFocus(textInput) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleShow(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	hls := textView.GetHighlights() | ||||
| 	if len(hls) > 0 { | ||||
| 		go PrintImage(hls[0]) | ||||
| 		ResetMsgSelection() | ||||
| 		app.SetFocus(textInput) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleInfo(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	hls := textView.GetHighlights() | ||||
| 	if len(hls) > 0 { | ||||
| 		PrintText(msgStore.GetMessageInfo(hls[0])) | ||||
| 		ResetMsgSelection() | ||||
| 		app.SetFocus(textInput) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleMessagesUp(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[len(curRegions)-1]) | ||||
| 	} | ||||
| 	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]) | ||||
| 	} | ||||
| 	textView.ScrollToHighlight() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleMessagesLast(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	if curRegions == nil || len(curRegions) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	textView.Highlight(curRegions[len(curRegions)-1]) | ||||
| 	textView.ScrollToHighlight() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleMessagesFirst(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	if curRegions == nil || len(curRegions) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	textView.Highlight(curRegions[0]) | ||||
| 	textView.ScrollToHighlight() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey { | ||||
| 	if curRegions == nil || len(curRegions) == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
| 	ResetMsgSelection() | ||||
| 	app.SetFocus(textInput) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func LoadShortcuts() { | ||||
| 	keysApp = cbind.NewConfiguration() | ||||
| 	if err := keysApp.Set(config.GetKey("focus_messages"), handleFocusMessage); err != nil { | ||||
| 		PrintErrorMsg("focus_messages:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("focus_input"), handleFocusInput); err != nil { | ||||
| 		PrintErrorMsg("focus_input:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("focus_contacts"), handleFocusContacts); err != nil { | ||||
| 		PrintErrorMsg("focus_contacts:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("switch_panels"), handleSwitchPanels); err != nil { | ||||
| 		PrintErrorMsg("switch_panels:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("command_connect"), handleConnect); err != nil { | ||||
| 		PrintErrorMsg("command_connect:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("command_quit"), handleQuit); err != nil { | ||||
| 		PrintErrorMsg("command_quit:", err) | ||||
| 	} | ||||
| 	if err := keysApp.Set(config.GetKey("command_help"), handleHelp); err != nil { | ||||
| 		PrintErrorMsg("command_help:", err) | ||||
| 	} | ||||
| 	app.SetInputCapture(keysApp.Capture) | ||||
| 	keysMessages := cbind.NewConfiguration() | ||||
| 	if err := keysMessages.Set(config.GetKey("message_download"), handleDownload); err != nil { | ||||
| 		PrintErrorMsg("message_download:", err) | ||||
| 	} | ||||
| 	if err := keysMessages.Set(config.GetKey("message_open"), handleOpen); err != nil { | ||||
| 		PrintErrorMsg("message_open:", err) | ||||
| 	} | ||||
| 	if err := keysMessages.Set(config.GetKey("message_show"), handleShow); err != nil { | ||||
| 		PrintErrorMsg("message_show:", err) | ||||
| 	} | ||||
| 	if err := keysMessages.Set(config.GetKey("message_info"), handleInfo); err != nil { | ||||
| 		PrintErrorMsg("message_info:", 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.SetRune(tcell.ModNone, 'g', handleMessagesFirst) | ||||
| 	keysMessages.SetRune(tcell.ModNone, 'G', handleMessagesLast) | ||||
| 	textView.SetInputCapture(keysMessages.Capture) | ||||
| } | ||||
|  | ||||
| // prints help to chat view | ||||
| func PrintHelp() { | ||||
| 	fmt.Fprintln(textView, "[::b]WhatsCLI "+VERSION+"[-]") | ||||
| 	fmt.Fprintln(textView, "") | ||||
| 	fmt.Fprintln(textView, "[-::u]Keys:[-::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] Up/Down[::-] = scroll history/contacts") | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("switch_panels"), "[::-] = switch input/contacts") | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("focus_messages"), "[::-] = focus message panel") | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("focus_contacts"), "[::-] = focus contacts panel") | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("focus_input"), "[::-] = focus input") | ||||
| 	fmt.Fprintln(textView, "") | ||||
| 	fmt.Fprintln(textView, "[-::-]Message panel focused:[-::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] Up/Down[::-] = select message") | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("message_download"), "[::-] = download attachment -> ", config.GetSetting("download_path")) | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("message_open"), "[::-] = download & open attachment -> ", config.GetSetting("preview_path")) | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("message_show"), "[::-] = download & show image using jp2a -> ", config.GetSetting("preview_path")) | ||||
| 	fmt.Fprintln(textView, "[::b]", config.GetKey("message_info"), "[::-] = info about message") | ||||
| 	fmt.Fprintln(textView, "") | ||||
| 	fmt.Fprintln(textView, "[-::u]Commands:[-::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] /connect[::-] = (re)connect in case the connection dropped ->[::b]", config.GetKey("command_connect"), "[::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] /help[::-] = show this help ->[::b]", config.GetKey("command_help"), "[::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] /quit[::-] = exit app ->[::b]", config.GetKey("command_quit"), "[::-]") | ||||
| 	fmt.Fprintln(textView, "[::b] /disconnect[::-] = close the connection") | ||||
| 	fmt.Fprintln(textView, "[::b] /logout[::-] = remove login data from computer (stays connected until app closes)") | ||||
| 	fmt.Fprintln(textView, "") | ||||
| 	fmt.Fprintln(textView, "Config file in \n-> ", config.GetConfigFilePath()) | ||||
| } | ||||
|  | ||||
| // called when text is entered by the user | ||||
| func EnterCommand(key tcell.Key) { | ||||
| 	if sndTxt == "" { | ||||
| 		return | ||||
| 	} | ||||
| 	if key == tcell.KeyEsc { | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/connect" { | ||||
| 		//command | ||||
| 		msgStore.Init() | ||||
| 		messages.Login() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/disconnect" { | ||||
| 		PrintError(messages.Disconnect()) | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/logout" { | ||||
| 		//command | ||||
| 		PrintError(messages.Logout()) | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/load" { | ||||
| 		//command | ||||
| 		LoadContacts() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/help" { | ||||
| 		//command | ||||
| 		PrintHelp() | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/quit" { | ||||
| 		//command | ||||
| 		messages.Disconnect() | ||||
| 		app.Stop() | ||||
| 		return | ||||
| 	} | ||||
| 	if sndTxt == "/keys" { | ||||
| 		//command | ||||
| 		//config.PrintKeys(textView) | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if strings.Index(sndTxt, "/addname ") == 0 { | ||||
| 		//command | ||||
| 		parts := strings.Split(sndTxt, " ") | ||||
| 		if len(parts) < 3 { | ||||
| 			fmt.Fprintln(textView, "Use /addname 1234567 NewName") | ||||
| 			return | ||||
| 		} | ||||
| 		contact := whatsapp.Contact{ | ||||
| 			Jid:  parts[1] + messages.CONTACTSUFFIX, | ||||
| 			Name: strings.TrimPrefix(sndTxt, "/addname "+parts[1]+" "), | ||||
| 		} | ||||
| 		contactChannel <- contact | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	if currentReceiver == "" { | ||||
| 		fmt.Fprintln(textView, "[red]no contact selected[-]") | ||||
| 		return | ||||
| 	} | ||||
| 	if strings.Index(sndTxt, "/name ") == 0 { | ||||
| 		//command | ||||
| 		contact := whatsapp.Contact{ | ||||
| 			Jid:  currentReceiver, | ||||
| 			Name: strings.TrimPrefix(sndTxt, "/name "), | ||||
| 		} | ||||
| 		contactChannel <- contact | ||||
| 		textInput.SetText("") | ||||
| 		return | ||||
| 	} | ||||
| 	// send message | ||||
| 	msg := waMsg{ | ||||
| 		Wid:  currentReceiver, | ||||
| 		Text: sndTxt, | ||||
| 	} | ||||
| 	sendChannel <- msg | ||||
| 	textInput.SetText("") | ||||
| } | ||||
|  | ||||
| // get the next message id to select (highlighted + offset) | ||||
| func GetOffsetMsgId(curId string, offset int) string { | ||||
| 	if curRegions == nil || len(curRegions) == 0 { | ||||
| 		return "" | ||||
| 	} | ||||
| 	for idx, val := range curRegions { | ||||
| 		if val == curId { | ||||
| 			arrPos := idx + offset | ||||
| 			if len(curRegions) > arrPos && arrPos >= 0 { | ||||
| 				return curRegions[arrPos] | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	if offset > 0 { | ||||
| 		return curRegions[0] | ||||
| 	} else { | ||||
| 		return curRegions[len(curRegions)-1] | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // resets the selection in the textView and scrolls it down | ||||
| func ResetMsgSelection() { | ||||
| 	if len(textView.GetHighlights()) > 0 { | ||||
| 		textView.Highlight("") | ||||
| 	} | ||||
| 	textView.ScrollToEnd() | ||||
| } | ||||
|  | ||||
| // prints a text message to the TextView | ||||
| func PrintTextMessage(msg whatsapp.TextMessage) { | ||||
| 	fmt.Fprintln(textView, messages.GetTextMessageString(&msg)) | ||||
| } | ||||
|  | ||||
| // prints text to the TextView | ||||
| func PrintText(txt string) { | ||||
| 	fmt.Fprintln(textView, txt) | ||||
| } | ||||
|  | ||||
| // prints an error to the TextView | ||||
| func PrintError(err error) { | ||||
| 	if err == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	fmt.Fprintln(textView, "[red]", err.Error(), "[-]") | ||||
| } | ||||
|  | ||||
| // prints an error to the TextView | ||||
| func PrintErrorMsg(text string, err error) { | ||||
| 	if err == nil { | ||||
| 		return | ||||
| 	} | ||||
| 	fmt.Fprintln(textView, "[red]", text, err.Error(), "[-]") | ||||
| } | ||||
|  | ||||
| // prints an image attachment to the TextView (by message id) | ||||
| func PrintImage(id string) { | ||||
| 	var err error | ||||
| 	var path string | ||||
| 	PrintText("[::d]loading..[::-]") | ||||
| 	if path, err = msgStore.DownloadMessage(id, true); err == nil { | ||||
| 		cmd := exec.Command("jp2a", "--color", path) | ||||
| 		var stdout io.ReadCloser | ||||
| 		if stdout, err = cmd.StdoutPipe(); err == nil { | ||||
| 			if err = cmd.Start(); err == nil { | ||||
| 				reader := bufio.NewReader(stdout) | ||||
| 				io.Copy(tview.ANSIWriter(textView), reader) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	PrintError(err) | ||||
| } | ||||
|  | ||||
| // downloads a specific message attachment | ||||
| func DownloadMessageId(id string, openIt bool) { | ||||
| 	PrintText("[::d]loading..[::-]") | ||||
| 	if result, err := msgStore.DownloadMessage(id, openIt); err == nil { | ||||
| 		PrintText("[::d]Downloaded as [yellow]" + result + "[-::-]") | ||||
| 		if openIt { | ||||
| 			open.Run(result) | ||||
| 		} | ||||
| 	} else { | ||||
| 		PrintError(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // notifies about a new message if its recent | ||||
| func NotifyMsg(msg whatsapp.TextMessage) { | ||||
| 	if int64(msg.Info.Timestamp) > time.Now().Unix()-30 { | ||||
| 		//fmt.Print("\a") | ||||
| 		//err := beeep.Notify(messages.GetIdName(msg.Info.RemoteJid), msg.Text, "") | ||||
| 		//if err != nil { | ||||
| 		//  fmt.Fprintln(textView, "[red]error in notification[-]") | ||||
| 		//} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // loads the contact data from storage to the TreeView | ||||
| func LoadContacts() { | ||||
| 	var ids = msgStore.GetContactIds() | ||||
| 	contactRoot.ClearChildren() | ||||
| @@ -235,9 +589,9 @@ func LoadContacts() { | ||||
| 			SetReference(element). | ||||
| 			SetSelectable(true) | ||||
| 		if strings.Count(element, messages.CONTACTSUFFIX) > 0 { | ||||
| 			node.SetColor(tcell.ColorGreen) | ||||
| 			node.SetColor(config.GetColor("list_contact")) | ||||
| 		} else { | ||||
| 			node.SetColor(tcell.ColorBlue) | ||||
| 			node.SetColor(config.GetColor("list_group")) | ||||
| 		} | ||||
| 		contactRoot.AddChild(node) | ||||
| 		if element == currentReceiver { | ||||
| @@ -246,17 +600,20 @@ func LoadContacts() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // sets the current contact, loads text from storage to TextView | ||||
| func SetDisplayedContact(wid string) { | ||||
| 	currentReceiver = wid | ||||
| 	textView.Clear() | ||||
| 	textView.SetTitle(messages.GetIdName(wid)) | ||||
| 	textView.SetText(msgStore.GetMessagesString(wid)) | ||||
| 	msgTxt, regIds := msgStore.GetMessagesString(wid) | ||||
| 	textView.SetText(msgTxt) | ||||
| 	curRegions = regIds | ||||
| } | ||||
|  | ||||
| // StartTextReceiver starts the handler for the text messages received | ||||
| // starts the receiver and message handling thread | ||||
| func StartTextReceiver() error { | ||||
| 	var wac = GetConnection() | ||||
| 	err := LoginWithConnection(wac) | ||||
| 	var wac = messages.GetConnection() | ||||
| 	err := messages.LoginWithConnection(wac) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("%v\n", err) | ||||
| 	} | ||||
| @@ -264,24 +621,29 @@ func StartTextReceiver() error { | ||||
| 	wac.AddHandler(handler) | ||||
| 	sendChannel = make(chan waMsg) | ||||
| 	textChannel = make(chan whatsapp.TextMessage) | ||||
| 	otherChannel = make(chan interface{}) | ||||
| 	contactChannel = make(chan whatsapp.Contact) | ||||
| 	for { | ||||
| 		select { | ||||
| 		case msg := <-sendChannel: | ||||
| 			SendText(msg.Wid, msg.Text) | ||||
| 		case rcvd := <-textChannel: | ||||
| 			if msgStore.AddTextMessage(rcvd) { | ||||
| 			if msgStore.AddTextMessage(&rcvd) { | ||||
| 				app.QueueUpdateDraw(LoadContacts) | ||||
| 			} | ||||
| 		case other := <-otherChannel: | ||||
| 			msgStore.AddOtherMessage(&other) | ||||
| 		case contact := <-contactChannel: | ||||
| 			messages.SetIdName(contact.Jid, contact.Name) | ||||
| 			app.QueueUpdateDraw(LoadContacts) | ||||
| 		} | ||||
| 	} | ||||
| 	fmt.Fprint(textView, "\n"+"closing the receiver") | ||||
| 	fmt.Fprintln(textView, "closing the receiver") | ||||
| 	wac.Disconnect() | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // sends text to whatsapp id | ||||
| func SendText(wid string, text string) { | ||||
| 	msg := whatsapp.TextMessage{ | ||||
| 		Info: whatsapp.MessageInfo{ | ||||
| @@ -292,25 +654,22 @@ func SendText(wid string, text string) { | ||||
| 		Text: text, | ||||
| 	} | ||||
|  | ||||
| 	PrintTextMessage(msg) | ||||
| 	//TODO: workaround for error when receiving&sending | ||||
| 	connection.RemoveHandlers() | ||||
| 	_, err := connection.Send(msg) | ||||
| 	msgStore.AddTextMessage(msg) | ||||
| 	connection.AddHandler(handler) | ||||
| 	_, err := messages.GetConnection().Send(msg) | ||||
| 	if err != nil { | ||||
| 		fmt.Fprint(textView, "\nerror sending message: %v", err) | ||||
| 		PrintError(err) | ||||
| 	} else { | ||||
| 		//fmt.Fprint(textView, "\nSent msg with ID: %v", msgID) | ||||
| 		msgStore.AddTextMessage(&msg) | ||||
| 		PrintTextMessage(msg) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // handler struct for whatsapp callbacks | ||||
| type textHandler struct{} | ||||
|  | ||||
| // HandleError implements the handler interface for go-whatsapp | ||||
| // HandleError implements the error handler interface for go-whatsapp | ||||
| func (t textHandler) HandleError(err error) { | ||||
| 	// TODO : handle go routine here | ||||
| 	fmt.Fprint(textView, "\nerror in textHandler : %v", err) | ||||
| 	PrintText("[red]go-whatsapp reported an error:[-]") | ||||
| 	PrintError(err) | ||||
| 	return | ||||
| } | ||||
|  | ||||
| @@ -318,16 +677,18 @@ func (t textHandler) HandleError(err error) { | ||||
| func (t textHandler) HandleTextMessage(msg whatsapp.TextMessage) { | ||||
| 	textChannel <- msg | ||||
| 	if msg.Info.RemoteJid != currentReceiver { | ||||
| 		//fmt.Print("\a") | ||||
| 		NotifyMsg(msg) | ||||
| 		return | ||||
| 	} | ||||
| 	PrintTextMessage(msg) | ||||
| 	// add to regions if current window, otherwise its not selectable | ||||
| 	id := msg.Info.Id | ||||
| 	app.QueueUpdate(func() { | ||||
| 		curRegions = append(curRegions, id) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func PrintTextMessage(msg whatsapp.TextMessage) { | ||||
| 	fmt.Fprint(textView, messages.GetTextMessageString(&msg)) | ||||
| } | ||||
|  | ||||
| // methods to convert messages to TextMessage | ||||
| func (t textHandler) HandleImageMessage(message whatsapp.ImageMessage) { | ||||
| 	msg := whatsapp.TextMessage{ | ||||
| 		Info: whatsapp.MessageInfo{ | ||||
| @@ -335,10 +696,12 @@ func (t textHandler) HandleImageMessage(message whatsapp.ImageMessage) { | ||||
| 			SenderJid: message.Info.SenderJid, | ||||
| 			FromMe:    message.Info.FromMe, | ||||
| 			Timestamp: message.Info.Timestamp, | ||||
| 			Id:        message.Info.Id, | ||||
| 		}, | ||||
| 		Text: "[IMAGE] " + message.Caption, | ||||
| 	} | ||||
| 	t.HandleTextMessage(msg) | ||||
| 	otherChannel <- message | ||||
| } | ||||
|  | ||||
| func (t textHandler) HandleDocumentMessage(message whatsapp.DocumentMessage) { | ||||
| @@ -348,10 +711,12 @@ func (t textHandler) HandleDocumentMessage(message whatsapp.DocumentMessage) { | ||||
| 			SenderJid: message.Info.SenderJid, | ||||
| 			FromMe:    message.Info.FromMe, | ||||
| 			Timestamp: message.Info.Timestamp, | ||||
| 			Id:        message.Info.Id, | ||||
| 		}, | ||||
| 		Text: "[DOCUMENT] " + message.Title, | ||||
| 	} | ||||
| 	t.HandleTextMessage(msg) | ||||
| 	otherChannel <- message | ||||
| } | ||||
|  | ||||
| func (t textHandler) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||
| @@ -361,10 +726,12 @@ func (t textHandler) HandleVideoMessage(message whatsapp.VideoMessage) { | ||||
| 			SenderJid: message.Info.SenderJid, | ||||
| 			FromMe:    message.Info.FromMe, | ||||
| 			Timestamp: message.Info.Timestamp, | ||||
| 			Id:        message.Info.Id, | ||||
| 		}, | ||||
| 		Text: "[VIDEO] " + message.Caption, | ||||
| 	} | ||||
| 	t.HandleTextMessage(msg) | ||||
| 	otherChannel <- message | ||||
| } | ||||
|  | ||||
| func (t textHandler) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||
| @@ -374,16 +741,21 @@ func (t textHandler) HandleAudioMessage(message whatsapp.AudioMessage) { | ||||
| 			SenderJid: message.Info.SenderJid, | ||||
| 			FromMe:    message.Info.FromMe, | ||||
| 			Timestamp: message.Info.Timestamp, | ||||
| 			Id:        message.Info.Id, | ||||
| 		}, | ||||
| 		Text: "[AUDIO]", | ||||
| 	} | ||||
| 	t.HandleTextMessage(msg) | ||||
| 	otherChannel <- message | ||||
| } | ||||
|  | ||||
| // add contact info to database (not needed, internal db of connection is used) | ||||
| func (t textHandler) HandleNewContact(contact whatsapp.Contact) { | ||||
| 	contactChannel <- contact | ||||
| 	// redundant, wac has contacts | ||||
| 	//contactChannel <- contact | ||||
| } | ||||
|  | ||||
| // handle battery messages | ||||
| //func (t textHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) { | ||||
| //  app.QueueUpdate(func() { | ||||
| //    infoBar.SetText("🔋: " + string(msg.Percentage) + "%") | ||||
|   | ||||
| @@ -5,15 +5,27 @@ import ( | ||||
| 	"os" | ||||
| 	"os/user" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/normen/whatscli/config" | ||||
| ) | ||||
|  | ||||
| var contacts map[string]string | ||||
| var connection *whatsapp.Conn | ||||
|  | ||||
| // loads custom contacts from disk | ||||
| func LoadContacts() { | ||||
| 	contacts = make(map[string]string) | ||||
| 	file, err := os.Open(GetHomeDir() + ".whatscli.contacts") | ||||
| 	file, err := os.Open(config.GetContactsFilePath()) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 		// load old contacts file, re-save in new location if found | ||||
| 		file, err = os.Open(GetHomeDir() + ".whatscli.contacts") | ||||
| 		if err != nil { | ||||
| 			return | ||||
| 		} else { | ||||
| 			os.Remove(GetHomeDir() + ".whatscli.contacts") | ||||
| 			SaveContacts() | ||||
| 		} | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 	decoder := gob.NewDecoder(file) | ||||
| @@ -23,8 +35,9 @@ func LoadContacts() { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // saves custom contacts to disk | ||||
| func SaveContacts() { | ||||
| 	file, err := os.Create(GetHomeDir() + ".whatscli.contacts") | ||||
| 	file, err := os.Open(config.GetContactsFilePath()) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| @@ -37,18 +50,47 @@ func SaveContacts() { | ||||
| 	return | ||||
| } | ||||
|  | ||||
| // sets a new name for a whatsapp id | ||||
| func SetIdName(id string, name string) { | ||||
| 	contacts[id] = name | ||||
| 	SaveContacts() | ||||
| } | ||||
|  | ||||
| // gets a pretty name for a whatsapp id | ||||
| func GetIdName(id string) string { | ||||
| 	if _, ok := contacts[id]; ok { | ||||
| 		return contacts[id] | ||||
| 	} | ||||
| 	if val, ok := connection.Store.Contacts[id]; ok { | ||||
| 		if val.Name != "" { | ||||
| 			return val.Name | ||||
| 		} else if val.Short != "" { | ||||
| 			return val.Short | ||||
| 		} else if val.Notify != "" { | ||||
| 			return val.Notify | ||||
| 		} | ||||
| 	} | ||||
| 	return strings.TrimSuffix(id, CONTACTSUFFIX) | ||||
| } | ||||
|  | ||||
| // gets a short name for a whatsapp id | ||||
| func GetIdShort(id string) string { | ||||
| 	if val, ok := connection.Store.Contacts[id]; ok { | ||||
| 		if val.Short != "" { | ||||
| 			return val.Short | ||||
| 		} else if val.Name != "" { | ||||
| 			return val.Name | ||||
| 		} else if val.Notify != "" { | ||||
| 			return val.Notify | ||||
| 		} | ||||
| 	} | ||||
| 	if _, ok := contacts[id]; ok { | ||||
| 		return contacts[id] | ||||
| 	} | ||||
| 	return strings.TrimSuffix(id, CONTACTSUFFIX) | ||||
| } | ||||
|  | ||||
| // gets the OS home dir with a path separator at the end | ||||
| func GetHomeDir() string { | ||||
| 	usr, err := user.Current() | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -1,18 +1,31 @@ | ||||
| package main | ||||
| package messages | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/gob" | ||||
| 	"fmt" | ||||
| 	"github.com/rivo/tview" | ||||
| 	"os" | ||||
| 	"os/user" | ||||
| 	"sync" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/rivo/tview" | ||||
| 
 | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/normen/whatscli/config" | ||||
| 	"github.com/normen/whatscli/qrcode" | ||||
| ) | ||||
| 
 | ||||
| var textView *tview.TextView | ||||
| var connMutex sync.Mutex | ||||
| 
 | ||||
| // TODO: remove this circular dependeny in favor of a better way | ||||
| func SetTextView(tv *tview.TextView) { | ||||
| 	textView = tv | ||||
| } | ||||
| 
 | ||||
| // gets an existing connection or creates one | ||||
| func GetConnection() *whatsapp.Conn { | ||||
| 	connMutex.Lock() | ||||
| 	defer connMutex.Unlock() | ||||
| 	var wac *whatsapp.Conn | ||||
| 	if connection == nil { | ||||
| 		wacc, err := whatsapp.NewConn(5 * time.Second) | ||||
| @@ -37,8 +50,10 @@ func 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 LoginWithConnection(wac *whatsapp.Conn) error { | ||||
| 	if wac.Info != nil && wac.Info.Connected { | ||||
| 		return nil | ||||
| 	connMutex.Lock() | ||||
| 	defer connMutex.Unlock() | ||||
| 	if wac != nil && wac.GetConnected() { | ||||
| 		wac.Disconnect() | ||||
| 	} | ||||
| 	//load saved session | ||||
| 	session, err := readSession() | ||||
| @@ -68,27 +83,37 @@ func LoginWithConnection(wac *whatsapp.Conn) error { | ||||
| 		return fmt.Errorf("error saving session: %v\n", err) | ||||
| 	} | ||||
| 	//<-time.After(3 * time.Second) | ||||
| 	//fmt.Fprint(textView, "\nlogin successful") | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func Disconnect() error { | ||||
| 	wac := GetConnection() | ||||
| 	if wac != nil && wac.GetConnected() { | ||||
| 		_, err := wac.Disconnect() | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Logout logs out the user. | ||||
| func Logout() error { | ||||
| 	connMutex.Lock() | ||||
| 	defer connMutex.Unlock() | ||||
| 	return removeSession() | ||||
| } | ||||
| 
 | ||||
| func GetHomeDir() string { | ||||
| 	usr, err := user.Current() | ||||
| 	if err != nil { | ||||
| 	} | ||||
| 	return usr.HomeDir + string(os.PathSeparator) | ||||
| } | ||||
| 
 | ||||
| // reads the session file from disk | ||||
| func readSession() (whatsapp.Session, error) { | ||||
| 	session := whatsapp.Session{} | ||||
| 	file, err := os.Open(GetHomeDir() + ".whatscli.session") | ||||
| 	file, err := os.Open(config.GetSessionFilePath()) | ||||
| 	if err != nil { | ||||
| 		return session, err | ||||
| 		// load old session file, delete if found | ||||
| 		file, err = os.Open(GetHomeDir() + ".whatscli.session") | ||||
| 		if err != nil { | ||||
| 			return session, err | ||||
| 		} else { | ||||
| 			os.Remove(GetHomeDir() + ".whatscli.session") | ||||
| 		} | ||||
| 	} | ||||
| 	defer file.Close() | ||||
| 	decoder := gob.NewDecoder(file) | ||||
| @@ -99,8 +124,9 @@ func readSession() (whatsapp.Session, error) { | ||||
| 	return session, nil | ||||
| } | ||||
| 
 | ||||
| // saves the session file to disk | ||||
| func writeSession(session whatsapp.Session) error { | ||||
| 	file, err := os.Create(GetHomeDir() + ".whatscli.session") | ||||
| 	file, err := os.Create(config.GetSessionFilePath()) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -113,6 +139,7 @@ func writeSession(session whatsapp.Session) error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // deletes the session file from disk | ||||
| func removeSession() error { | ||||
| 	return os.Remove(GetHomeDir() + ".whatscli.session") | ||||
| 	return os.Remove(config.GetSessionFilePath()) | ||||
| } | ||||
| @@ -1,84 +1,242 @@ | ||||
| package messages | ||||
|  | ||||
| import ( | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/rivo/tview" | ||||
| 	"errors" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/Rhymen/go-whatsapp" | ||||
| 	"github.com/normen/whatscli/config" | ||||
| 	"github.com/rivo/tview" | ||||
| ) | ||||
|  | ||||
| const GROUPSUFFIX = "@g.us" | ||||
| const CONTACTSUFFIX = "@s.whatsapp.net" | ||||
|  | ||||
| type MessageDatabase struct { | ||||
| 	textMessages  map[string][]whatsapp.TextMessage | ||||
| 	latestMessage map[string]uint64 | ||||
| 	textMessages  map[string][]*whatsapp.TextMessage // text messages stored by RemoteJid | ||||
| 	messagesById  map[string]*whatsapp.TextMessage   // text messages stored by message ID | ||||
| 	latestMessage map[string]uint64                  // last message from RemoteJid | ||||
| 	otherMessages map[string]*interface{}            // other non-text messages, stored by ID | ||||
| 	mutex         sync.Mutex | ||||
| } | ||||
|  | ||||
| // initialize the database | ||||
| func (db *MessageDatabase) Init() { | ||||
| 	//var this = *db | ||||
| 	(*db).textMessages = make(map[string][]whatsapp.TextMessage) | ||||
| 	(*db).latestMessage = make(map[string]uint64) | ||||
| 	db.textMessages = make(map[string][]*whatsapp.TextMessage) | ||||
| 	db.messagesById = make(map[string]*whatsapp.TextMessage) | ||||
| 	db.otherMessages = make(map[string]*interface{}) | ||||
| 	db.latestMessage = make(map[string]uint64) | ||||
| } | ||||
|  | ||||
| func (db *MessageDatabase) AddTextMessage(msg whatsapp.TextMessage) bool { | ||||
| // add a text message to the database, stored by RemoteJid | ||||
| func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	//var this = *db | ||||
| 	var didNew = false | ||||
| 	var wid = msg.Info.RemoteJid | ||||
| 	if (*db).textMessages[wid] == nil { | ||||
| 		var newArr = []whatsapp.TextMessage{} | ||||
| 		(*db).textMessages[wid] = newArr | ||||
| 		(*db).latestMessage[wid] = msg.Info.Timestamp | ||||
| 	if db.textMessages[wid] == nil { | ||||
| 		var newArr = []*whatsapp.TextMessage{} | ||||
| 		db.textMessages[wid] = newArr | ||||
| 		db.latestMessage[wid] = msg.Info.Timestamp | ||||
| 		didNew = true | ||||
| 	} else if (*db).latestMessage[wid] < msg.Info.Timestamp { | ||||
| 		(*db).latestMessage[wid] = msg.Info.Timestamp | ||||
| 	} else if db.latestMessage[wid] < msg.Info.Timestamp { | ||||
| 		db.latestMessage[wid] = msg.Info.Timestamp | ||||
| 		didNew = true | ||||
| 	} | ||||
| 	(*db).textMessages[wid] = append((*db).textMessages[wid], msg) | ||||
| 	sort.Slice((*db).textMessages[wid], func(i, j int) bool { | ||||
| 		return (*db).textMessages[wid][i].Info.Timestamp < (*db).textMessages[wid][j].Info.Timestamp | ||||
| 	db.textMessages[wid] = append(db.textMessages[wid], msg) | ||||
| 	db.messagesById[msg.Info.Id] = msg | ||||
| 	sort.Slice(db.textMessages[wid], func(i, j int) bool { | ||||
| 		return db.textMessages[wid][i].Info.Timestamp < db.textMessages[wid][j].Info.Timestamp | ||||
| 	}) | ||||
| 	return didNew | ||||
| } | ||||
|  | ||||
| // add audio/video/image/doc message, stored by message id | ||||
| func (db *MessageDatabase) AddOtherMessage(msg *interface{}) { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	var id = "" | ||||
| 	switch v := (*msg).(type) { | ||||
| 	default: | ||||
| 	case whatsapp.ImageMessage: | ||||
| 		id = v.Info.Id | ||||
| 	case whatsapp.DocumentMessage: | ||||
| 		id = v.Info.Id | ||||
| 	case whatsapp.AudioMessage: | ||||
| 		id = v.Info.Id | ||||
| 	case whatsapp.VideoMessage: | ||||
| 		id = v.Info.Id | ||||
| 	} | ||||
| 	if id != "" { | ||||
| 		db.otherMessages[id] = msg | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // get an array of all chat ids | ||||
| func (db *MessageDatabase) GetContactIds() []string { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	//var this = *db | ||||
| 	keys := make([]string, len((*db).textMessages)) | ||||
| 	keys := make([]string, len(db.textMessages)) | ||||
| 	i := 0 | ||||
| 	for k := range (*db).textMessages { | ||||
| 	for k := range db.textMessages { | ||||
| 		keys[i] = k | ||||
| 		i++ | ||||
| 	} | ||||
| 	sort.Slice(keys, func(i, j int) bool { | ||||
| 		return (*db).latestMessage[keys[i]] > (*db).latestMessage[keys[j]] | ||||
| 		return db.latestMessage[keys[i]] > db.latestMessage[keys[j]] | ||||
| 	}) | ||||
| 	//sort.Strings(keys) | ||||
| 	return keys | ||||
| } | ||||
|  | ||||
| func (db *MessageDatabase) GetMessagesString(wid string) string { | ||||
| 	//var this = *db | ||||
| 	var out = "" | ||||
| 	for _, element := range (*db).textMessages[wid] { | ||||
| 		out += GetTextMessageString(&element) | ||||
| func (db *MessageDatabase) GetMessageInfo(id string) string { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	if _, ok := db.otherMessages[id]; ok { | ||||
| 		return "[yellow]OtherMessage[-]" | ||||
| 	} | ||||
| 	out := "" | ||||
| 	if msg, ok := db.messagesById[id]; ok { | ||||
| 		out += "[yellow]ID: " + msg.Info.Id + "[-]\n" | ||||
| 		out += "[yellow]PushName: " + msg.Info.PushName + "[-]\n" | ||||
| 		out += "[yellow]RemoteJid: " + msg.Info.RemoteJid + "[-]\n" | ||||
| 		out += "[yellow]SenderJid: " + msg.Info.SenderJid + "[-]\n" | ||||
| 		out += "[yellow]Participant: " + msg.ContextInfo.Participant + "[-]\n" | ||||
| 		out += "[yellow]QuotedMessageID: " + msg.ContextInfo.QuotedMessageID + "[-]\n" | ||||
| 	} | ||||
| 	return out | ||||
| } | ||||
|  | ||||
| func GetTextMessageString(msg *whatsapp.TextMessage) string { | ||||
| 	out := "" | ||||
| 	text := tview.Escape((*msg).Text) | ||||
| 	tim := time.Unix(int64((*msg).Info.Timestamp), 0) | ||||
| 	if (*msg).Info.FromMe { //msg from me | ||||
| 		out += "\n[-::d](" + tim.Format("01-02-06 15:04:05") + ") [blue::b]Me: [-::-]" + text | ||||
| 	} else if strings.Contains((*msg).Info.RemoteJid, GROUPSUFFIX) { // group msg | ||||
| 		//(*msg).Info.SenderJid | ||||
| 		userId := (*msg).Info.SenderJid | ||||
| 		//userId := strings.Split(string((*msg).Info.RemoteJid), "-")[0] + CONTACTSUFFIX | ||||
| 		out += "\n[-::d](" + tim.Format("01-02-06 15:04:05") + ") [green::b]" + GetIdName(userId) + ": [-::-]" + text | ||||
| 	} else { // message from others | ||||
| 		out += "\n[-::d](" + tim.Format("01-02-06 15:04:05") + ") [green::b]" + GetIdName((*msg).Info.RemoteJid) + ": [-::-]" + text | ||||
| // get a string containing all messages for a chat by chat id | ||||
| func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	//var this = *db | ||||
| 	var out = "" | ||||
| 	var arr = []string{} | ||||
| 	for _, element := range db.textMessages[wid] { | ||||
| 		out += GetTextMessageString(element) | ||||
| 		out += "\n" | ||||
| 		arr = append(arr, element.Info.Id) | ||||
| 	} | ||||
| 	return out, arr | ||||
| } | ||||
|  | ||||
| // load data for message specified by message id TODO: support types | ||||
| func (db *MessageDatabase) LoadMessageData(wid string) ([]byte, error) { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	if msg, ok := db.otherMessages[wid]; ok { | ||||
| 		switch v := (*msg).(type) { | ||||
| 		default: | ||||
| 		case whatsapp.ImageMessage: | ||||
| 			return v.Download() | ||||
| 		case whatsapp.DocumentMessage: | ||||
| 			//return v.Download() | ||||
| 		case whatsapp.AudioMessage: | ||||
| 			//return v.Download() | ||||
| 		case whatsapp.VideoMessage: | ||||
| 			//return v.Download() | ||||
| 		} | ||||
| 	} | ||||
| 	return []byte{}, errors.New("This is not an image message") | ||||
| } | ||||
|  | ||||
| // attempts to download a messages attachments, returns path or error message | ||||
| func (db *MessageDatabase) DownloadMessage(wid string, preview bool) (string, error) { | ||||
| 	db.mutex.Lock() | ||||
| 	defer db.mutex.Unlock() | ||||
| 	if msg, ok := db.otherMessages[wid]; ok { | ||||
| 		var fileName string = "" | ||||
| 		if preview { | ||||
| 			fileName += config.GetSetting("download_path") | ||||
| 		} else { | ||||
| 			fileName += config.GetSetting("preview_path") | ||||
| 		} | ||||
| 		fileName += string(os.PathSeparator) | ||||
| 		switch v := (*msg).(type) { | ||||
| 		default: | ||||
| 		case whatsapp.ImageMessage: | ||||
| 			fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "image/") | ||||
| 			if _, err := os.Stat(fileName); err == nil { | ||||
| 				return fileName, err | ||||
| 			} else if os.IsNotExist(err) { | ||||
| 				if data, err := v.Download(); err == nil { | ||||
| 					return saveAttachment(data, fileName) | ||||
| 				} else { | ||||
| 					return fileName, err | ||||
| 				} | ||||
| 			} | ||||
| 		case whatsapp.DocumentMessage: | ||||
| 			fileName += v.Info.Id + "." + strings.TrimPrefix(strings.TrimPrefix(v.Type, "application/"), "document/") | ||||
| 			if _, err := os.Stat(fileName); err == nil { | ||||
| 				return fileName, err | ||||
| 			} else if os.IsNotExist(err) { | ||||
| 				if data, err := v.Download(); err == nil { | ||||
| 					return saveAttachment(data, fileName) | ||||
| 				} else { | ||||
| 					return fileName, err | ||||
| 				} | ||||
| 			} | ||||
| 		case whatsapp.AudioMessage: | ||||
| 			fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "audio/") | ||||
| 			if _, err := os.Stat(fileName); err == nil { | ||||
| 				return fileName, err | ||||
| 			} else if os.IsNotExist(err) { | ||||
| 				if data, err := v.Download(); err == nil { | ||||
| 					return saveAttachment(data, fileName) | ||||
| 				} else { | ||||
| 					return fileName, err | ||||
| 				} | ||||
| 			} | ||||
| 		case whatsapp.VideoMessage: | ||||
| 			fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "video/") | ||||
| 			if _, err := os.Stat(fileName); err == nil { | ||||
| 				return fileName, err | ||||
| 			} else if os.IsNotExist(err) { | ||||
| 				if data, err := v.Download(); err == nil { | ||||
| 					return saveAttachment(data, fileName) | ||||
| 				} else { | ||||
| 					return fileName, err | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return "", errors.New("No attachments found") | ||||
| } | ||||
|  | ||||
| // create a formatted string with regions based on message ID from a text message | ||||
| func GetTextMessageString(msg *whatsapp.TextMessage) string { | ||||
| 	colorMe := config.GetColorName("chat_me") | ||||
| 	colorContact := config.GetColorName("chat_contact") | ||||
| 	out := "" | ||||
| 	text := tview.Escape(msg.Text) | ||||
| 	tim := time.Unix(int64(msg.Info.Timestamp), 0) | ||||
| 	time := tim.Format("02-01-06 15:04:05") | ||||
| 	out += "[\"" | ||||
| 	out += msg.Info.Id | ||||
| 	out += "\"]" | ||||
| 	if msg.Info.FromMe { //msg from me | ||||
| 		out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text | ||||
| 	} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) { // group msg | ||||
| 		userId := msg.Info.SenderJid | ||||
| 		out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(userId) + ": [-::-]" + text | ||||
| 	} else { // message from others | ||||
| 		out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(msg.Info.RemoteJid) + ": [-::-]" + text | ||||
| 	} | ||||
| 	out += "[\"\"]" | ||||
| 	return out | ||||
| } | ||||
|  | ||||
| // helper to save an attachment and open it if specified | ||||
| func saveAttachment(data []byte, path string) (string, error) { | ||||
| 	err := ioutil.WriteFile(path, data, 0644) | ||||
| 	return path, err | ||||
| } | ||||
|   | ||||
| @@ -96,7 +96,7 @@ var ( | ||||
| type QRCodeString string | ||||
|  | ||||
| func (v *QRCodeString) Print() { | ||||
| 	fmt.Fprint(outer, *v) | ||||
| 	fmt.Fprintln(outer, *v) | ||||
| } | ||||
|  | ||||
| type qrcodeTerminal struct { | ||||
|   | ||||
							
								
								
									
										49
									
								
								release.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										49
									
								
								release.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| #!/bin/bash | ||||
| set -e | ||||
| # get verison from main.go VERSION string | ||||
| if [ $# -eq 0 ]; then | ||||
| 	VERSION=$(cat main.go|grep "VERSION string"| awk -v FS="(\")" '{print $2}') | ||||
| else | ||||
|   VERSION=$1 | ||||
| fi | ||||
| echo Releasing $VERSION | ||||
| WINF=whatscli-$VERSION-windows.zip | ||||
| LINUXF=whatscli-$VERSION-linux.zip | ||||
| MACF=whatscli-$VERSION-macos.zip | ||||
| RASPIF=whatscli-$VERSION-raspberrypi.zip | ||||
|  | ||||
| # build zip files with binaries | ||||
| GOOS=darwin go build -o whatscli | ||||
| zip $MACF whatscli | ||||
| rm whatscli | ||||
| GOOS=windows go build -o whatscli.exe | ||||
| zip $WINF whatscli.exe | ||||
| rm whatscli.exe | ||||
| GOOS=linux go build -o whatscli | ||||
| zip $LINUXF whatscli | ||||
| rm whatscli | ||||
| GOOS=linux GOARCH=arm GOARM=5 go build -o whatscli | ||||
| zip $RASPIF whatscli | ||||
| rm whatscli | ||||
|  | ||||
| # publish to github | ||||
| git pull | ||||
| LASTTAG=$(git describe --tags --abbrev=0) | ||||
| git log $LASTTAG..HEAD --no-decorate --pretty=format:"- %s" --abbrev-commit > changes.txt | ||||
| vim changes.txt | ||||
| gh release create $VERSION $LINUXF $MACF $WINF $RASPIF -F changes.txt -t $VERSION | ||||
| rm changes.txt | ||||
| rm *.zip | ||||
|  | ||||
| # update homebrew tap | ||||
| URL="https://github.com/normen/whatscli/archive/$VERSION.tar.gz" | ||||
| wget $URL | ||||
| SHASUM=$(shasum -a 256 $VERSION.tar.gz|awk '{print$1}') | ||||
| rm $VERSION.tar.gz | ||||
| cd ../../BrewCode/homebrew-tap | ||||
| sed -i bak "s/sha256 \".*/sha256 \"$SHASUM\"/" Formula/whatscli.rb | ||||
| sed -i bak "s!url \".*!url \"$URL\"!" Formula/whatscli.rb | ||||
| rm Formula/whatscli.rbbak | ||||
| git add -A | ||||
| git commit -m "update whatscli to $VERSION" | ||||
| git push | ||||
		Reference in New Issue
	
	Block a user