Compare commits

...

76 Commits

Author SHA1 Message Date
normen
ae25296b9d bump version 2021-11-02 19:06:17 +01:00
Normen Hansen
020ccfbb4b Merge pull request #55 from dgrisham/master
set go-whatsapp client version to fix login error
2021-11-02 19:04:10 +01:00
dgrisham
aa53f24c39 set go-whatsapp client version to fix login error
Based on https://github.com/Rhymen/go-whatsapp/issues/626#issuecomment-957082821
2021-11-02 09:26:18 -06:00
normen
b282213ca1 - possible fix for #48 2021-07-12 21:24:10 +02:00
normen
46f719c1f7 - bump go-whatsapp library version
- fixes #46
2021-06-23 11:01:43 +02:00
normen
309eca5d00 update tview/tcell dependencies
- fixes mouse selection of messages
2021-05-01 23:16:08 +02:00
normen
c2e7d5d6b7 - fix string combination
- bump version
2021-04-10 12:31:00 +02:00
Normen Hansen
88cc6b09ae Merge pull request #42 from cetinajero/feature/customizable-backlog-msg-qty
Setting for the # of messages per backlog
2021-04-10 12:24:17 +02:00
Edgar Tinajero
646fed75a6 Fix indentation 2021-04-09 12:39:18 -06:00
Edgar Tinajero
0b5476a1c8 Setting for the # of messages per backlog
This adds a customizable setting to define how many messages to retrieve per backlog cmd execution
2021-04-09 12:36:08 -06:00
normen
8510dd40ee add license info 2021-02-20 17:03:12 +01:00
normen
ee5d675f98 bump version 2021-02-15 20:54:36 +01:00
Normen Hansen
2a3f53c4c3 Merge pull request #37 from keegancsmith/terminal-bell
add UseTerminalBell as an alternative to desktop notifications
2021-02-15 20:52:12 +01:00
Keegan Carruthers-Smith
cc71c296b2 add UseTerminalBell as an alternative to desktop notifications 2021-02-15 21:17:07 +02:00
normen
b98af9f0c7 document notifications 2020-12-08 16:59:51 +01:00
normen
4fdcd70703 cleanups 2020-12-07 23:02:44 +01:00
normen
f690d798bc more doc! 2020-12-07 22:54:09 +01:00
normen
dd6e4f54b1 Dr. Doc 🚑 2020-12-07 22:45:09 +01:00
normen
49718acc49 avoid trying to send message when no receiver is set
Fixes #26
2020-12-05 16:26:43 +01:00
normen
6a97d6dfe6 move more stuff to UI thread (image painting) 2020-11-30 19:45:58 +01:00
normen
0bacea701a fix own messages disappearing in UI 2020-11-29 19:05:36 +01:00
normen
421139730c don't save config on each start 2020-11-28 18:21:45 +01:00
normen
313b4606c5 remove unused variable/import 2020-11-28 15:44:51 +01:00
normen
f79e3e46b7 add development info 2020-11-28 15:37:09 +01:00
normen
3053d15929 add exit key binding to help screen 2020-11-28 15:33:26 +01:00
normen
73cd749394 move commands info into separate help screen 2020-11-28 15:28:49 +01:00
normen
8e8f2da43f allow paging when selecting messages 2020-11-28 14:56:58 +01:00
normen
e8d9f266fa allow pasting even if theres no system clipboard 2020-11-28 14:30:15 +01:00
normen
2763bb14b4 switch clipboard library 2020-11-28 04:04:06 +01:00
normen
f91ffbef0f colorize forwarded text differently 2020-11-28 02:24:32 +01:00
normen
3521a3b6f5 add /remove and /removeadmin commands 2020-11-28 00:23:20 +01:00
normen
a6d7954795 don't just print help when no messages are available 2020-11-27 23:23:36 +01:00
normen
96e9d75810 small fix in help screen 2020-11-27 21:48:04 +01:00
normen
b2904929b0 update README 2020-11-27 21:42:32 +01:00
normen
033e7aa1ac allow creating and managing groups 2020-11-27 21:33:40 +01:00
normen
53e404dd55 add command/binding to copy selected user id 2020-11-27 20:08:14 +01:00
normen
8318d2e80f remove redundant "reported an error" 2020-11-27 18:58:51 +01:00
normen
3af2a3738f fix unread count not updating 2020-11-27 14:22:20 +01:00
normen
e31c2c3e36 get the battery state on connection 2020-11-27 03:30:13 +01:00
normen
3f20981550 cleaner logout 2020-11-27 03:10:25 +01:00
normen
f8368e4998 add /read command to set chat to read
- display connection status (connecting, connected, disconnected)
- allow setting unread count color
2020-11-27 00:11:59 +01:00
normen
121a73c312 add unread count (no reset yet) 2020-11-26 22:52:47 +01:00
normen
86af0d82a4 remove contact file name getter 2020-11-26 21:50:41 +01:00
normen
b94129fb0e rename focus_contacts and contact_sidebar_width - sorry! 2020-11-26 21:48:59 +01:00
normen
59a843bb8d use chat and contact list reported by go-whatsapp
Fixes #22
2020-11-26 21:43:59 +01:00
normen
a86aa3eec3 show contact name for status messages 2020-11-26 18:53:22 +01:00
normen
a20b4e3592 update dependencies 2020-11-26 18:45:23 +01:00
normen
9e62295188 unify variable names 2020-11-26 02:46:00 +01:00
normen
2e891d05ca don't start session manager twice 2020-11-26 02:17:44 +01:00
normen
bfbec54de3 remove number suffix for groups in fallback 2020-11-26 02:10:28 +01:00
normen
c677bce14e fix contact/group color mixup in contact list 2020-11-26 02:05:24 +01:00
normen
6f30efeebe abstract messages, move message styling to UI 2020-11-26 01:27:48 +01:00
normen
14a0e74b25 use pointer for database 2020-11-25 21:34:27 +01:00
normen
4951ca24da help screen cleanups 2020-11-25 03:19:28 +01:00
normen
1cd6c25b02 fix preview and download path mixup 2020-11-25 03:11:38 +01:00
normen
d9a9e7f753 separate messages and interfaces to separate file 2020-11-25 02:12:57 +01:00
normen
5388a1b408 cleanup help 2020-11-25 01:38:06 +01:00
normen
7687af38e1 set file name when uploading 2020-11-24 23:16:29 +01:00
normen
9b26641a0c don't swallow error messages on upload/send 2020-11-24 22:53:36 +01:00
normen
e5ef667909 show help and set no receiver on contact root 2020-11-24 22:29:50 +01:00
normen
c39479f12b fix notifications for own messages 2020-11-24 22:28:24 +01:00
normen
f1109f6465 add /colorlist command 2020-11-24 21:05:12 +01:00
normen
127883701d fix version snafu in 0.9.1 release 2020-11-24 20:52:07 +01:00
normen
5cacc3c5ea bump version 2020-11-24 20:46:17 +01:00
normen
65957ff732 use color_negative for all error messages 2020-11-24 20:38:04 +01:00
normen
7a096dbc94 allow configuring command for image to text conversion 2020-11-24 20:25:21 +01:00
normen
a7977959b5 help info update 2020-11-24 19:19:51 +01:00
normen
ea92d56426 allow sending image, video and audio messages
files have to be right format to be recognized by WhatsApp
2020-11-24 19:16:37 +01:00
normen
3c193d219e allow opening URLs in messages
Fixes #19
2020-11-24 18:47:58 +01:00
normen
f3a2bd3e88 add some code documentation 2020-11-24 17:37:07 +01:00
normen
f0488851ae remove local contacts feature creep
remove singleton connection
2020-11-24 17:30:06 +01:00
normen
c5276247b8 avoid sending notifications when loading backlog 2020-11-24 14:20:26 +01:00
normen
27d3a48d98 update readme 2020-11-24 14:03:52 +01:00
normen
9065248d1c add notification support 2020-11-24 13:58:31 +01:00
normen
6e0c150e26 fix creating file suffix for download 2020-11-24 13:03:47 +01:00
normen
489b23899e allow uploading of files as documents 2020-11-24 05:17:22 +01:00
9 changed files with 1421 additions and 405 deletions

View File

@@ -8,24 +8,26 @@ A command line interface for whatsapp, based on [go-whatsapp](https://github.com
Things that work.
- Sending and receiving WhatsApp messages in a command line app
- 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
- 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
- Only fetches a few old messages
- No incoming message notification / count
- No proper connection drop handling
- No uploading of images/video/audio/data
- 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
## Installation
How to get it running and how to use it
@@ -50,3 +52,66 @@ Some ways to install via package managers are supported but the installed versio
- `https://aur.archlinux.org/packages/whatscli/`
## Usage
Most information, all commands and key bindings are availabe through the in-app help, simply type `/help` and/or `/commands`.
### Login
When starting up, whatscli will immediately try to connect to the WhatsApp server to log in. Keep your phone ready to scan the appearing QR code in WhatsApp on your Phone. If you don't manage to scan the code quick enough just restart the application. If you can not see the whole QR code, reduce the font size of your terminal or increase the window size.
After scanning the QR code the chats should be populated. After you have done this once, whatscli will be able to log into WhatsApp automatically on start. To log out of WhapsApp completely type `/logout`.
### Messaging / Commands
Select a chat on the left and start typing in the input field at the bottom to send messages. Switch between the chat list and the input fiel with `<Tab>`.
For issuing commands the same input field is used. By default commands are prefixed with `/`. You can for example use the `/sendimage /path/to/file.jpg` command to send images, see `/help` for more commands.
When paths are given for commands you don't need to surround the path in quotes, even if it contains spaces. Also don't prefix spaces with backslashes (as the copy-paste function of MacOS does for example).
### Messages
When pressing `Ctrl-w` (default mapping) you enter "message selection mode" which allows selecting a single message and performing operations on them. For example pressing `o` while a message is selected allows opening any attachments through an external application.
#### Image display
You can display images in whatscli using external programs that convert the image to UTF characters. I found that `jp2a` works well for jpeg images, it is available through package managers on most systems. However the "image quality" leaves a lot to be desired. The [PIXterm](https://github.com/eliukblau/pixterm) app allows displaying true-color versions of the images which are quite recognizable already.
To configure the used command and its parameters edit the `show_command` parameter in `whatscli.config`, see `/help` for the config file location.
#### Copy-Pasting User IDs
Some commands such as the `/add` and `/remove` require a "user id" as their input. You can copy the user ID of a selected chat or a selected message to the clipboard with `Ctrl-c` (default mapping) and easily append them to the current input using `Ctrl-v`.
### Notifications
The app supports basic desktop notifications through the `gen2brain/beeep` library, to enable it set `enable_notifications = true` in `whatscli.config`. Set `use_terminal_bell = true` to ring your terminal's bell instead of sending a desktop notification.
### Configuration
Most key bindings, colors and other options can be configured in the `whatscli.config` file, the `/help` command shows its location.
## 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.
## License
This software is released under MIT license. Remember that this gives you all freedom except for slapping your name on it.

View File

@@ -6,12 +6,10 @@ import (
"os/user"
"github.com/adrg/xdg"
"gitlab.com/tslocum/cbind"
"gopkg.in/ini.v1"
)
var configFilePath string
var keyConfig *cbind.Configuration
var cfg *ini.File
type IniFile struct {
@@ -22,34 +20,44 @@ type IniFile struct {
}
type General struct {
DownloadPath string
PreviewPath string
CmdPrefix string
DownloadPath string
PreviewPath string
CmdPrefix string
ShowCommand string
EnableNotifications bool
UseTerminalBell bool
NotificationTimeout int64
BacklogMsgQuantity int
}
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
MessageDownload string
MessageOpen string
MessageShow string
MessageUrl string
MessageInfo string
MessageRevoke string
}
type Ui struct {
ContactSidebarWidth int
ChatSidebarWidth int
}
type Colors struct {
Background string
Text string
ForwardedText string
ListHeader string
ListContact string
ListGroup string
@@ -58,37 +66,48 @@ type Colors struct {
Borders string
InputBackground string
InputText string
UnreadCount string
Positive string
Negative string
}
var Config = IniFile{
&General{
DownloadPath: GetHomeDir() + "Downloads",
PreviewPath: GetHomeDir() + "Downloads",
CmdPrefix: "/",
DownloadPath: GetHomeDir() + "Downloads",
PreviewPath: GetHomeDir() + "Downloads",
CmdPrefix: "/",
ShowCommand: "jp2a --color",
EnableNotifications: false,
UseTerminalBell: false,
NotificationTimeout: 60,
BacklogMsgQuantity: 10,
},
&Keymap{
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+?",
MessageDownload: "d",
MessageInfo: "i",
MessageOpen: "o",
MessageUrl: "u",
MessageRevoke: "r",
MessageShow: "s",
},
&Ui{
ContactSidebarWidth: 30,
ChatSidebarWidth: 30,
},
&Colors{
Background: "black",
Text: "white",
ForwardedText: "purple",
ListHeader: "yellow",
ListContact: "green",
ListGroup: "blue",
@@ -97,6 +116,7 @@ var Config = IniFile{
Borders: "white",
InputBackground: "blue",
InputText: "white",
UnreadCount: "yellow",
Positive: "green",
Negative: "red",
},
@@ -122,11 +142,11 @@ func InitConfig() {
if section, err := cfg.GetSection("colors"); err == nil {
section.MapTo(&Config.Colors)
}
newCfg := ini.Empty()
if err = ini.ReflectFromWithMapper(newCfg, &Config, ini.TitleUnderscore); err == nil {
//TODO: only save if changes
err = newCfg.SaveTo(configFilePath)
}
//TODO: only save if changes
//newCfg := ini.Empty()
//if err = ini.ReflectFromWithMapper(newCfg, &Config, ini.TitleUnderscore); err == nil {
//err = newCfg.SaveTo(configFilePath)
//}
} else {
cfg = ini.Empty()
cfg.NameMapper = ini.TitleUnderscore
@@ -152,13 +172,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()

33
go.mod
View File

@@ -3,21 +3,34 @@ module github.com/normen/whatscli
go 1.15
require (
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/golang/protobuf v1.4.3 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
code.rocketnine.space/tslocum/cbind v0.1.5
github.com/Rhymen/go-whatsapp v0.1.2-0.20210615184944-2b8a3e9b8aa2
github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d // indirect
github.com/adrg/xdg v0.3.3
github.com/gabriel-vasile/mimetype v1.3.0
github.com/gdamore/tcell/v2 v2.3.11
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/gopherjs/gopherjs v0.0.0-20210621113107-84c6004145de // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/mattn/go-colorable v0.1.8
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893
github.com/rivo/tview v0.0.0-20210608105643-d4fb0348227b
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/text v0.3.4 // indirect
google.golang.org/protobuf v1.25.0 // indirect
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
gopkg.in/ini.v1 v1.62.0
mvdan.cc/xurls/v2 v2.2.0
)

356
go.sum
View File

@@ -1,30 +1,103 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
code.rocketnine.space/tslocum/cbind v0.1.5 h1:i6NkeLLNPNMS4NWNi3302Ay3zSU6MrqOT+yJskiodxE=
code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0SXBJtxBObbfBWo/M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
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/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
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 v0.1.2-0.20210615184944-2b8a3e9b8aa2 h1:xEhWgQl3hCOtFph6nWpTjVgAdPmAk0zUTENPDb3HJ7Y=
github.com/Rhymen/go-whatsapp v0.1.2-0.20210615184944-2b8a3e9b8aa2/go.mod h1:DNSFRLFDFIqm2+0aJzSOVfn25020vldM4SRqz6YtLgI=
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/adrg/xdg v0.3.3 h1:s/tV7MdqQnzB1nKY8aqHvAMD+uCiuEDzVB5HLRY849U=
github.com/adrg/xdg v0.3.3/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
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/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.1.2 h1:gaPnPcNor5aZSVCJVSGipcpbgMWiAAj9z182ocSGbHU=
github.com/gabriel-vasile/mimetype v1.1.2/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
github.com/gabriel-vasile/mimetype v1.3.0 h1:4YOHITFLyYwF+iqG0ybSLGArRItynpfwdlWRmJnd75E=
github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
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/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.2.1 h1:Gt8wk0jd5pIK2CyXNo/fqwxNWf726j1lQjEDdfbnqTc=
github.com/gdamore/tcell/v2 v2.2.1/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gdamore/tcell/v2 v2.3.11 h1:ECO6WqHGbKZ3HrSL7bG/zArMCmLaNr5vcjjMVnLHpzc=
github.com/gdamore/tcell/v2 v2.3.11/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
@@ -34,93 +107,349 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
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/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20210621113107-84c6004145de h1:q+tIagAgwJBQPaeOPvis2b+cxfaZ5HNkzoctl7SMxDQ=
github.com/gopherjs/gopherjs v0.0.0-20210621113107-84c6004145de/go.mod h1:MtKwTfDNYAP5EtbQSMYjTSqvj1aXJKQRASWq3bwaP+g=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
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/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/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/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
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/tview v0.0.0-20210427112837-09cec83b1732 h1:sY3Egm/ld9GcxAUs5zBPo1lEzC15ZteptMJmd8Ltc6Q=
github.com/rivo/tview v0.0.0-20210427112837-09cec83b1732/go.mod h1:Uajo+DndkMDGPzEw/Z92AgJrpx6Rvy2HRo/xP1YbXAI=
github.com/rivo/tview v0.0.0-20210608105643-d4fb0348227b h1:VRivPtgGaL9sjudoQUyHpKWJeDIFYUYbJ+AF03NEvLI=
github.com/rivo/tview v0.0.0-20210608105643-d4fb0348227b/go.mod h1:IxQujbYMAh4trWr0Dwa8jfciForjVmxyHpskZX6aydQ=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
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/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
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/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
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-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
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.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
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=
@@ -133,9 +462,28 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
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=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

390
main.go
View File

@@ -6,20 +6,22 @@ import (
"io"
"os/exec"
"strings"
"time"
"code.rocketnine.space/tslocum/cbind"
"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"
"github.com/zyedidia/clipboard"
)
var VERSION string = "v0.8.4"
var VERSION string = "v1.0.11"
var sndTxt string = ""
var currentReceiver string = ""
var curRegions []string
var currentReceiver messages.Chat = messages.Chat{}
var curRegions []messages.Message
var textView *tview.TextView
var treeView *tview.TreeView
@@ -27,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
@@ -41,11 +43,10 @@ func main() {
uiHandler = UiHandler{}
sessionManager = &messages.SessionManager{}
sessionManager.Init(uiHandler)
messages.LoadContacts()
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)
@@ -59,10 +60,10 @@ func main() {
topBar.SetScrollable(false)
topBar.SetText("[::b] WhatsCLI " + VERSION + " [-::d]Type " + cmdPrefix + "help or press " + config.Config.Keymap.CommandHelp + " for help")
topBar.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
UpdateStatusBar(messages.SessionStatus{})
infoBar = tview.NewTextView()
infoBar.SetDynamicColors(true)
UpdateStatusBar(messages.SessionStatus{})
textView = tview.NewTextView().
SetDynamicColors(true).
@@ -121,36 +122,35 @@ func main() {
app.SetRoot(gridLayout, true)
app.EnableMouse(true)
app.SetFocus(textInput)
go func() {
if err := sessionManager.StartManager(); err != nil {
PrintError(err)
}
}()
if err := sessionManager.StartManager(); err != nil {
PrintError(err)
}
LoadShortcuts()
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 {
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.(string)
SetDisplayedContact(recv)
recv := reference.(messages.Chat)
SetDisplayedChat(recv)
} else {
// Collapse if visible, expand if collapsed.
node.SetExpanded(!node.IsExpanded())
@@ -163,7 +163,7 @@ 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])
textView.Highlight(curRegions[len(curRegions)-1].Id)
}
}
return nil
@@ -202,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()
@@ -225,45 +248,43 @@ 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])
}
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 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 {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
textView.Highlight(curRegions[len(curRegions)-1])
textView.Highlight(curRegions[len(curRegions)-1].Id)
textView.ScrollToHighlight()
return nil
}
@@ -272,7 +293,7 @@ func handleMessagesFirst(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
textView.Highlight(curRegions[0])
textView.Highlight(curRegions[0].Id)
textView.ScrollToHighlight()
return nil
}
@@ -286,7 +307,9 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
// 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,9 +353,18 @@ 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)
}
if err := keysMessages.Set(config.Config.Keymap.MessageUrl, handleMessageCommand("url")); err != nil {
PrintErrorMsg("message_url:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageInfo, handleMessageCommand("info")); err != nil {
PrintErrorMsg("message_info:", err)
}
@@ -330,49 +372,83 @@ 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, "[::b] Up/Down[::-] = scroll history/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = switch input/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusMessages, "[::-] = focus message panel")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusContacts, "[::-] = focus contacts panel")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusInput, "[::-] = focus input")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Message panel focused:[-::-]")
fmt.Fprintln(textView, "Global")
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")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageDownload, "[::-] = download attachment -> ", config.Config.General.DownloadPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = download & open attachment -> ", config.Config.General.PreviewPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = download & show image using jp2a -> ", config.Config.General.PreviewPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageRevoke, "[::-] = revoke message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageInfo, "[::-] = info about message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageDownload, "[::-] = Download attachment")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = Download & open attachment")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = Download & show image using", config.Config.General.ShowCommand)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageUrl, "[::-] = Find URL in message and open it")
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, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next 10 older messages for current chat")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.Config.Keymap.CommandConnect, "[::-] = (re)connect in case the connection dropped")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"help [::-]or[::b]", config.Config.Keymap.CommandHelp, "[::-] = show this help")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.Config.Keymap.CommandQuit, "[::-] = exit app")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = leave group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"disconnect[::-] = close the connection")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"logout[::-] = remove login data from computer (stays connected until app closes)")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Config file in \n-> ", config.GetConfigFilePath())
fmt.Fprintln(textView, "[-::-]Global[-::-]")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.Config.Keymap.CommandConnect, "[::-] = (Re)Connect to server")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"disconnect[::-] = Close the connection")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"logout[::-] = Remove login data from computer")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.Config.Keymap.CommandQuit, "[::-] = Exit app")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Chat[-::-]")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next", config.Config.General.BacklogMsgQuantity, "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, "")
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
@@ -383,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
@@ -406,10 +485,15 @@ func EnterCommand(key tcell.Key) {
textInput.SetText("")
return
}
if currentReceiver.Id == "" {
PrintText("no receiver")
textInput.SetText("")
return
}
// no command, send as message
msg := messages.Command{
Name: "send",
Params: []string{currentReceiver, sndTxt},
Params: []string{currentReceiver.Id, sndTxt},
}
sessionManager.CommandChannel <- msg
textInput.SetText("")
@@ -421,17 +505,17 @@ func GetOffsetMsgId(curId string, offset int) string {
return ""
}
for idx, val := range curRegions {
if val == curId {
if val.Id == curId {
arrPos := idx + offset
if len(curRegions) > arrPos && arrPos >= 0 {
return curRegions[arrPos]
return curRegions[arrPos].Id
}
}
}
if offset > 0 {
return curRegions[0]
return curRegions[0].Id
} else {
return curRegions[len(curRegions)-1]
return curRegions[len(curRegions)-1].Id
}
}
@@ -453,7 +537,7 @@ func PrintError(err error) {
if err == nil {
return
}
fmt.Fprintln(textView, "[red]", err.Error(), "[-]")
fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", err.Error(), "[-]")
}
// prints an error to the TextView
@@ -461,13 +545,21 @@ func PrintErrorMsg(text string, err error) {
if err == nil {
return
}
fmt.Fprintln(textView, "[red]", text, err.Error(), "[-]")
fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", text, err.Error(), "[-]")
}
// prints an image attachment to the TextView (by message id)
func PrintImage(path string) {
var err error
cmd := exec.Command("jp2a", "--color", path)
cmdParts := strings.Split(config.Config.General.ShowCommand, " ")
cmdParts = append(cmdParts, path)
var cmd *exec.Cmd
size := len(cmdParts)
if size > 1 {
cmd = exec.Command(cmdParts[0], cmdParts[1:]...)
} else if size > 0 {
cmd = exec.Command(cmdParts[0])
}
var stdout io.ReadCloser
if stdout, err = cmd.StdoutPipe(); err == nil {
if err = cmd.Start(); err == nil {
@@ -479,6 +571,7 @@ func PrintImage(path string) {
PrintError(err)
}
// updates the status bar
func UpdateStatusBar(statusInfo messages.SessionStatus) {
out := " "
if statusInfo.Connected {
@@ -502,65 +595,112 @@ func UpdateStatusBar(statusInfo messages.SessionStatus) {
}
out += ")[::-] "
out += statusInfo.LastSeen
go app.QueueUpdateDraw(func() {
infoBar.SetText(out)
})
infoBar.SetText(out)
//infoBar.SetText("🔋: ??%")
}
// 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[-]")
//}
//}
//}
// sets the current contact, loads text from storage to TextView
func SetDisplayedContact(wid string) {
// 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(messages.GetIdName(wid))
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver}}
textView.SetTitle(wid.Name)
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
}
// get a string representation of all messages for chat
func getMessagesString(msgs []messages.Message) string {
out := ""
for _, msg := range msgs {
out += getTextMessageString(&msg)
out += "\n"
}
return out
}
// 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 += "[\""
out += msg.Id
out += "\"]"
if msg.FromMe { //msg from me
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
} else { // message from others
out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.ContactShort + ": [-::-]" + text
}
out += "[\"\"]"
return out
}
type UiHandler struct{}
func (u UiHandler) NewMessage(msg string, id string) {
func (u UiHandler) NewMessage(msg messages.Message) {
//TODO: its stupid to "go" this as its supposed to run
//on the ui thread anyway. But QueueUpdate blocks...?
go app.QueueUpdateDraw(func() {
curRegions = append(curRegions, id)
PrintText(msg)
curRegions = append(curRegions, msg)
PrintText(getTextMessageString(&msg))
})
}
func (u UiHandler) NewScreen(screen string, ids []string) {
func (u UiHandler) NewScreen(msgs []messages.Message) {
go app.QueueUpdateDraw(func() {
textView.Clear()
screen := getMessagesString(msgs)
textView.SetText(screen)
curRegions = ids
curRegions = msgs
if screen == "" {
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 []string) {
// 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(messages.GetIdName(element)).
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 strings.Count(element, messages.CONTACTSUFFIX) > 0 {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
} else {
if element.IsGroup {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup])
} 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)
}
}
@@ -576,7 +716,9 @@ func (u UiHandler) PrintText(msg string) {
}
func (u UiHandler) PrintFile(path string) {
PrintImage(path)
go app.QueueUpdateDraw(func() {
PrintImage(path)
})
}
func (u UiHandler) OpenFile(path string) {
@@ -584,7 +726,9 @@ func (u UiHandler) OpenFile(path string) {
}
func (u UiHandler) SetStatus(status messages.SessionStatus) {
UpdateStatusBar(status)
go app.QueueUpdateDraw(func() {
UpdateStatusBar(status)
})
}
func (u UiHandler) GetWriter() io.Writer {

View File

@@ -1,99 +0,0 @@
package messages
import (
"encoding/gob"
"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(config.GetContactsFilePath())
if err != nil {
// 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)
err = decoder.Decode(&contacts)
if err != nil {
return
}
}
// saves custom contacts to disk
func SaveContacts() {
file, err := os.Open(config.GetContactsFilePath())
if err != nil {
return
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(contacts)
if err != nil {
return
}
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 {
}
return usr.HomeDir + string(os.PathSeparator)
}

78
messages/messages.go Normal file
View File

@@ -0,0 +1,78 @@
//this package manages the messages
package messages
import "io"
// TODO: move these funcs/interface to channels
type UiMessageHandler interface {
NewMessage(Message)
NewScreen([]Message)
SetChats([]Chat)
PrintError(error)
PrintText(string)
PrintFile(string)
SetStatus(SessionStatus)
OpenFile(string)
GetWriter() io.Writer
}
// data struct for current session status
type SessionStatus struct {
BatteryCharge int
BatteryLoading bool
BatteryPowersave bool
Connected bool
LastSeen string
}
// message struct for battery messages
type BatteryMsg struct {
charge int
loading bool
powersave bool
}
// message struct for status messages
type StatusMsg struct {
connected bool
err error
}
// message object for commands
type Command struct {
Name string
Params []string
}
// internal message representation to abstract from message lib
type Message struct {
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 Chat struct {
Id string
IsGroup bool
Name string
Unread int
//TODO: convert to uint64
LastMessage int64
}
type Contact struct {
Id string
Name string
Short string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
const STATUSSUFFIX = "status@broadcast"

View File

@@ -2,109 +2,145 @@ package messages
import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/rivo/tview"
"github.com/Rhymen/go-whatsapp"
"github.com/gabriel-vasile/mimetype"
"github.com/gdamore/tcell/v2"
"github.com/gen2brain/beeep"
"github.com/normen/whatscli/config"
"github.com/normen/whatscli/qrcode"
"github.com/rivo/tview"
"mvdan.cc/xurls/v2"
)
// TODO: move message styling and ordering into UI, don't use strings
// move these funcs/interface to channels
type UiMessageHandler interface {
NewMessage(string, string)
NewScreen(string, []string)
SetContacts([]string)
PrintError(error)
PrintText(string)
PrintFile(string)
SetStatus(SessionStatus)
OpenFile(string)
GetWriter() io.Writer
}
type SessionStatus struct {
BatteryCharge int
BatteryLoading bool
BatteryPowersave bool
Connected bool
LastSeen string
}
type BatteryMsg struct {
charge int
loading bool
powersave bool
}
type StatusMsg struct {
connected bool
err error
}
type Command struct {
Name string
Params []string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
// 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
db *MessageDatabase
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
lastSent time.Time
started bool
}
// initialize the SessionManager
func (sm *SessionManager) Init(handler UiMessageHandler) {
sm.db = MessageDatabase{}
sm.db = &MessageDatabase{}
sm.db.Init()
sm.uiHandler = handler
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)
}
// starts the receiver and message handling thread
// TODO: can't be stopped, can only be called once!
// starts the receiver and message handling go routine
func (sm *SessionManager) StartManager() error {
if sm.started {
return errors.New("session manager running, send commands to control")
}
sm.started = true
go sm.runManager()
return nil
}
func (sm *SessionManager) runManager() error {
var wac = sm.getConnection()
err := sm.loginWithConnection(wac)
if err != nil {
sm.uiHandler.PrintError(err)
}
wac.AddHandler(sm)
for {
for sm.started == true {
select {
case msg := <-sm.TextChannel:
didNew := sm.db.AddTextMessage(&msg)
if msg.Info.RemoteJid == sm.currentReceiver {
if didNew {
sm.uiHandler.NewMessage(getTextMessageString(&msg), msg.Info.Id)
sm.uiHandler.NewMessage(sm.createMessage(&msg))
} else {
screen, ids := sm.db.GetMessagesString(sm.currentReceiver)
sm.uiHandler.NewScreen(screen, ids)
screen := sm.getMessages(sm.currentReceiver)
sm.uiHandler.NewScreen(screen)
}
// 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 !msg.Info.FromMe {
err := notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text)
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
} else {
// 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 !msg.Info.FromMe {
err := notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text)
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
sm.uiHandler.SetContacts(sm.db.GetContactIds())
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:
@@ -113,6 +149,7 @@ func (sm *SessionManager) StartManager() 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
@@ -121,6 +158,13 @@ func (sm *SessionManager) StartManager() 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")
@@ -128,25 +172,31 @@ func (sm *SessionManager) StartManager() error {
return nil
}
// set the currently selected chat
func (sm *SessionManager) setCurrentReceiver(id string) {
sm.currentReceiver = id
screen, ids := sm.db.GetMessagesString(id)
sm.uiHandler.NewScreen(screen, ids)
screen := sm.getMessages(id)
sm.uiHandler.NewScreen(screen)
}
// gets an existing connection or creates one
func (sm *SessionManager) getConnection() *whatsapp.Conn {
var wac *whatsapp.Conn
if connection == nil {
wacc, err := whatsapp.NewConn(5 * time.Second)
if sm.connection == nil {
options := &whatsapp.Options{
Timeout: 5 * time.Second,
LongClientName: "WhatsCLI Client",
ShortClientName: "whatscli",
}
wacc, err := whatsapp.NewConnWithOptions(options)
if err != nil {
return nil
}
wac = wacc
connection = wac
//wac.SetClientVersion(2, 2021, 4)
sm.connection = wac
wac.SetClientVersion(2, 2142, 12)
} else {
wac = connection
wac = sm.connection
}
return wac
}
@@ -160,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}
@@ -191,11 +242,17 @@ 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
}
// disconnects the session
func (sm *SessionManager) disconnect() error {
wac := sm.getConnection()
var err error
@@ -206,30 +263,34 @@ func (sm *SessionManager) disconnect() error {
return err
}
// logout logs out the user.
// logout logs out the user, deletes session file
func (ub *SessionManager) logout() error {
err := ub.getConnection().Logout()
ub.StatusChannel <- StatusMsg{false, err}
ub.uiHandler.PrintText("removing login data..")
return removeSession()
}
// executes a command
func (sm *SessionManager) execCommand(command Command) {
cmd := command.Name
switch cmd {
default:
sm.uiHandler.PrintText("[red]Unknown command: [-]" + cmd)
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Unknown command: [-]" + cmd)
case "backlog":
//command
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 := config.Config.General.BacklogMsgQuantity
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")
}
//FullChatHistory(currentReceiver, 20, 100000, handler)
//messages.GetConnection().LoadFullChatHistory(currentReceiver, 20, 100000, handler)
case "login":
sm.uiHandler.PrintError(sm.login())
case "connect":
@@ -244,19 +305,39 @@ func (sm *SessionManager) execCommand(command Command) {
text := strings.Join(textParams, " ")
sm.sendText(command.Params[0], text)
} else {
sm.uiHandler.PrintText("[red]Usage:[-] 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.uiHandler.PrintText("[red]Usage:[-] 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) {
sm.uiHandler.PrintText(sm.db.GetMessageInfo(command.Params[0]))
} else {
sm.uiHandler.PrintText("[red]Usage:[-] info [message-id[]")
sm.printCommandUsage("info", "[message-id[]")
}
case "download":
if checkParam(command.Params, 1) {
@@ -266,7 +347,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintText("[::d] -> " + path + "[::-]")
}
} else {
sm.uiHandler.PrintText("[red]Usage:[-] download [message-id[]")
sm.printCommandUsage("download", "[message-id[]")
}
case "open":
if checkParam(command.Params, 1) {
@@ -276,7 +357,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
}
} else {
sm.uiHandler.PrintText("[red]Usage:[-] open [message-id[]")
sm.printCommandUsage("open", "[message-id[]")
}
case "show":
if checkParam(command.Params, 1) {
@@ -286,8 +367,132 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
}
} else {
sm.uiHandler.PrintText("[red]Usage:[-] show [message-id[]")
sm.printCommandUsage("show", "[message-id[]")
}
case "url":
if checkParam(command.Params, 1) {
if msg, ok := sm.db.messagesById[command.Params[0]]; ok {
urlParser := xurls.Relaxed()
url := urlParser.FindString(msg.Text)
if url != "" {
sm.uiHandler.OpenFile(url)
}
}
} else {
sm.printCommandUsage("url", "[message-id[]")
}
case "upload":
if sm.currentReceiver == "" {
sm.printCommandUsage("upload", "-> only works in a chat")
return
}
var err error
var mime *mimetype.MIME
var file *os.File
if checkParam(command.Params, 1) {
path := strings.Join(command.Params, " ")
if mime, err = mimetype.DetectFile(path); err == nil {
if file, err = os.Open(path); err == nil {
msg := whatsapp.DocumentMessage{
Info: whatsapp.MessageInfo{
RemoteJid: sm.currentReceiver,
},
Type: mime.String(),
FileName: filepath.Base(file.Name()),
}
wac := sm.getConnection()
sm.lastSent = time.Now()
_, err = wac.Send(msg)
}
}
} else {
sm.printCommandUsage("upload", "/path/to/file")
}
sm.uiHandler.PrintError(err)
case "sendimage":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendimage", "-> only works in a chat")
return
}
var err error
var mime *mimetype.MIME
var file *os.File
if checkParam(command.Params, 1) {
path := strings.Join(command.Params, " ")
if mime, err = mimetype.DetectFile(path); err == nil {
if file, err = os.Open(path); err == nil {
msg := whatsapp.ImageMessage{
Info: whatsapp.MessageInfo{
RemoteJid: sm.currentReceiver,
},
Type: mime.String(),
Content: file,
}
wac := sm.getConnection()
sm.lastSent = time.Now()
_, err = wac.Send(msg)
}
}
} else {
sm.printCommandUsage("sendimage", "/path/to/file")
}
sm.uiHandler.PrintError(err)
case "sendvideo":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendvideo", "-> only works in a chat")
return
}
var err error
var mime *mimetype.MIME
var file *os.File
if checkParam(command.Params, 1) {
path := strings.Join(command.Params, " ")
if mime, err = mimetype.DetectFile(path); err == nil {
if file, err = os.Open(path); err == nil {
msg := whatsapp.VideoMessage{
Info: whatsapp.MessageInfo{
RemoteJid: sm.currentReceiver,
},
Type: mime.String(),
Content: file,
}
wac := sm.getConnection()
sm.lastSent = time.Now()
_, err = wac.Send(msg)
}
}
} else {
sm.printCommandUsage("sendvideo", "/path/to/file")
}
sm.uiHandler.PrintError(err)
case "sendaudio":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendaudio", "-> only works in a chat")
return
}
var err error
var mime *mimetype.MIME
var file *os.File
if checkParam(command.Params, 1) {
path := strings.Join(command.Params, " ")
if mime, err = mimetype.DetectFile(path); err == nil {
if file, err = os.Open(path); err == nil {
msg := whatsapp.AudioMessage{
Info: whatsapp.MessageInfo{
RemoteJid: sm.currentReceiver,
},
Type: mime.String(),
Content: file,
}
wac := sm.getConnection()
sm.lastSent = time.Now()
_, err = wac.Send(msg)
}
}
} else {
sm.printCommandUsage("sendaudio", "/path/to/file")
}
sm.uiHandler.PrintError(err)
case "revoke":
if checkParam(command.Params, 1) {
wac := sm.getConnection()
@@ -315,12 +520,13 @@ func (sm *SessionManager) execCommand(command Command) {
}
sm.uiHandler.PrintError(err)
} else {
sm.uiHandler.PrintText("[red]Usage:[-] revoke [message-id[]")
sm.printCommandUsage("revoke", "[message-id[]")
}
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
@@ -329,9 +535,148 @@ 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 {
out = out + "[" + idx + "]" + idx + "[-]\n"
}
sm.uiHandler.PrintText(out)
}
}
// helper for built-in command help
func (sm *SessionManager) printCommandUsage(command string, usage string) {
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] " + command + " " + usage)
}
// check if parameters for command are okay
func checkParam(arr []string, length int) bool {
if arr == nil || len(arr) < length {
return false
@@ -339,6 +684,42 @@ func checkParam(arr []string, length int) bool {
return true
}
// get all messages for one chat id
func (sm *SessionManager) getMessages(wid string) []Message {
msgs := sm.db.GetMessages(wid)
ids := []Message{}
for _, msg := range msgs {
ids = append(ids, sm.createMessage(&msg))
}
return ids
}
// 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.ChatId = msg.Info.RemoteJid
newMsg.FromMe = msg.Info.FromMe
newMsg.Timestamp = msg.Info.Timestamp
newMsg.Text = msg.Text
newMsg.Forwarded = msg.ContextInfo.IsForwarded
if strings.Contains(msg.Info.RemoteJid, STATUSSUFFIX) {
newMsg.ContactId = 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.ContactName = sm.db.GetIdName(msg.Info.RemoteJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.RemoteJid)
}
return newMsg
}
// load data for message specified by message id TODO: support types
func (sm *SessionManager) loadMessageData(wid string) ([]byte, error) {
if msg, ok := sm.db.otherMessages[wid]; ok {
@@ -362,15 +743,20 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
if msg, ok := sm.db.otherMessages[wid]; ok {
var fileName string = ""
if preview {
fileName += config.Config.General.DownloadPath
} else {
fileName += config.Config.General.PreviewPath
} else {
fileName += config.Config.General.DownloadPath
}
fileName += string(os.PathSeparator)
switch v := (*msg).(type) {
default:
case whatsapp.ImageMessage:
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "image/")
fileName += v.Info.Id
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
if len(exts) > 0 {
fileName += exts[0]
}
}
if _, err := os.Stat(fileName); err == nil {
return fileName, err
} else if os.IsNotExist(err) {
@@ -381,7 +767,12 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
}
}
case whatsapp.DocumentMessage:
fileName += v.Info.Id + "." + strings.TrimPrefix(strings.TrimPrefix(v.Type, "application/"), "document/")
fileName += v.Info.Id
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
if len(exts) > 0 {
fileName += exts[0]
}
}
if _, err := os.Stat(fileName); err == nil {
return fileName, err
} else if os.IsNotExist(err) {
@@ -392,7 +783,12 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
}
}
case whatsapp.AudioMessage:
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "audio/")
fileName += v.Info.Id
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
if len(exts) > 0 {
fileName += exts[0]
}
}
if _, err := os.Stat(fileName); err == nil {
return fileName, err
} else if os.IsNotExist(err) {
@@ -403,7 +799,12 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
}
}
case whatsapp.VideoMessage:
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "video/")
fileName += v.Info.Id
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
if len(exts) > 0 {
fileName += exts[0]
}
}
if _, err := os.Stat(fileName); err == nil {
return fileName, err
} else if os.IsNotExist(err) {
@@ -419,7 +820,7 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
}
// sends text to whatsapp id
func (sm SessionManager) sendText(wid string, text string) {
func (sm *SessionManager) sendText(wid string, text string) {
msg := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: wid,
@@ -429,13 +830,15 @@ func (sm SessionManager) sendText(wid string, text string) {
Text: text,
}
_, err := sm.getConnection().Send(msg)
sm.lastSent = time.Now()
newid, err := sm.getConnection().Send(msg)
msg.Info.Id = newid
if err != nil {
sm.uiHandler.PrintError(err)
} else {
sm.db.AddTextMessage(&msg)
if sm.currentReceiver == wid {
sm.uiHandler.NewMessage(getTextMessageString(&msg), msg.Info.Id)
sm.uiHandler.NewMessage(sm.createMessage(&msg))
}
}
}
@@ -444,7 +847,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("[red]go-whatsapp reported an error:[-]")
sm.uiHandler.PrintError(err)
statusMsg := StatusMsg{false, err}
sm.StatusChannel <- statusMsg
@@ -520,7 +922,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
@@ -528,6 +930,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)
@@ -540,11 +958,11 @@ func readSession() (whatsapp.Session, error) {
file, err := os.Open(config.GetSessionFilePath())
if err != nil {
// load old session file, delete if found
file, err = os.Open(GetHomeDir() + ".whatscli.session")
file, err = os.Open(config.GetHomeDir() + ".whatscli.session")
if err != nil {
return session, err
} else {
os.Remove(GetHomeDir() + ".whatscli.session")
os.Remove(config.GetHomeDir() + ".whatscli.session")
}
}
defer file.Close()
@@ -575,3 +993,15 @@ func writeSession(session whatsapp.Session) error {
func removeSession() error {
return os.Remove(config.GetSessionFilePath())
}
// notify will send a notification via beeep if EnableNotification is true. If
// UseTerminalBell is true it will send a terminal bell instead.
func notify(title, message string) error {
if !config.Config.General.EnableNotifications {
return nil
} else if config.Config.General.UseTerminalBell {
_, err := fmt.Printf("\a")
return err
}
return beeep.Notify(title, message, "")
}

View File

@@ -3,11 +3,8 @@ package messages
import (
"sort"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
"github.com/rivo/tview"
)
type MessageDatabase struct {
@@ -15,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
@@ -24,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
@@ -40,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
@@ -51,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 = ""
@@ -70,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[-]"
@@ -102,38 +154,10 @@ func (db *MessageDatabase) GetMessageInfo(id string) string {
}
// get a string containing all messages for a chat by chat id
func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) {
//var this = *db
var out = ""
var arr = []string{}
func (db *MessageDatabase) GetMessages(wid string) []whatsapp.TextMessage {
var arr = []whatsapp.TextMessage{}
for _, element := range db.textMessages[wid] {
out += getTextMessageString(element)
out += "\n"
arr = append(arr, element.Info.Id)
arr = append(arr, *element)
}
return out, arr
}
// create a formatted string with regions based on message ID from a text message
// TODO: move message styling into UI
func getTextMessageString(msg *whatsapp.TextMessage) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
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
return arr
}