Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae25296b9d | ||
|
|
020ccfbb4b | ||
|
|
aa53f24c39 | ||
|
|
b282213ca1 | ||
|
|
46f719c1f7 | ||
|
|
309eca5d00 | ||
|
|
c2e7d5d6b7 | ||
|
|
88cc6b09ae | ||
|
|
646fed75a6 | ||
|
|
0b5476a1c8 | ||
|
|
8510dd40ee | ||
|
|
ee5d675f98 | ||
|
|
2a3f53c4c3 | ||
|
|
cc71c296b2 | ||
|
|
b98af9f0c7 | ||
|
|
4fdcd70703 | ||
|
|
f690d798bc | ||
|
|
dd6e4f54b1 | ||
|
|
49718acc49 | ||
|
|
6a97d6dfe6 | ||
|
|
0bacea701a | ||
|
|
421139730c | ||
|
|
313b4606c5 | ||
|
|
f79e3e46b7 | ||
|
|
3053d15929 | ||
|
|
73cd749394 | ||
|
|
8e8f2da43f | ||
|
|
e8d9f266fa | ||
|
|
2763bb14b4 | ||
|
|
f91ffbef0f | ||
|
|
3521a3b6f5 | ||
|
|
a6d7954795 | ||
|
|
96e9d75810 | ||
|
|
b2904929b0 | ||
|
|
033e7aa1ac | ||
|
|
53e404dd55 | ||
|
|
8318d2e80f | ||
|
|
3af2a3738f | ||
|
|
e31c2c3e36 | ||
|
|
3f20981550 | ||
|
|
f8368e4998 | ||
|
|
121a73c312 | ||
|
|
86af0d82a4 | ||
|
|
b94129fb0e | ||
|
|
59a843bb8d | ||
|
|
a86aa3eec3 | ||
|
|
a20b4e3592 | ||
|
|
9e62295188 | ||
|
|
2e891d05ca | ||
|
|
bfbec54de3 | ||
|
|
c677bce14e | ||
|
|
6f30efeebe | ||
|
|
14a0e74b25 | ||
|
|
4951ca24da | ||
|
|
1cd6c25b02 | ||
|
|
d9a9e7f753 | ||
|
|
5388a1b408 | ||
|
|
7687af38e1 | ||
|
|
9b26641a0c | ||
|
|
e5ef667909 | ||
|
|
c39479f12b | ||
|
|
f1109f6465 | ||
|
|
127883701d | ||
|
|
5cacc3c5ea |
72
README.md
72
README.md
@@ -14,19 +14,20 @@ Things that work.
|
||||
- Allows downloading and opening image/video/audio/document attachments
|
||||
- Allows sending documents
|
||||
- Allows color customization
|
||||
- Allows basic group management
|
||||
- Supports desktop notifications
|
||||
- Binaries for Windows, Mac, Linux and RaspBerry Pi
|
||||
|
||||
### Caveats
|
||||
|
||||
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
|
||||
Heres some things you might expect to work that don't. Plus some other things I should mention.
|
||||
|
||||
- Only shows existing chats
|
||||
- No unread message count
|
||||
- No proper connection drop handling
|
||||
- No auto-reconnect when connection drops
|
||||
- No automation of messages, no sending of messages through shell commands
|
||||
- FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when FaceBook changes stuff in their web app
|
||||
|
||||
## Installation / Usage
|
||||
## Installation
|
||||
|
||||
How to get it running and how to use it
|
||||
|
||||
@@ -51,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.
|
||||
|
||||
@@ -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 {
|
||||
@@ -27,15 +25,20 @@ type General struct {
|
||||
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
|
||||
@@ -48,12 +51,13 @@ type Keymap struct {
|
||||
}
|
||||
|
||||
type Ui struct {
|
||||
ContactSidebarWidth int
|
||||
ChatSidebarWidth int
|
||||
}
|
||||
|
||||
type Colors struct {
|
||||
Background string
|
||||
Text string
|
||||
ForwardedText string
|
||||
ListHeader string
|
||||
ListContact string
|
||||
ListGroup string
|
||||
@@ -62,6 +66,7 @@ type Colors struct {
|
||||
Borders string
|
||||
InputBackground string
|
||||
InputText string
|
||||
UnreadCount string
|
||||
Positive string
|
||||
Negative string
|
||||
}
|
||||
@@ -73,14 +78,19 @@ var Config = IniFile{
|
||||
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+?",
|
||||
@@ -92,11 +102,12 @@ var Config = IniFile{
|
||||
MessageShow: "s",
|
||||
},
|
||||
&Ui{
|
||||
ContactSidebarWidth: 30,
|
||||
ChatSidebarWidth: 30,
|
||||
},
|
||||
&Colors{
|
||||
Background: "black",
|
||||
Text: "white",
|
||||
ForwardedText: "purple",
|
||||
ListHeader: "yellow",
|
||||
ListContact: "green",
|
||||
ListGroup: "blue",
|
||||
@@ -105,6 +116,7 @@ var Config = IniFile{
|
||||
Borders: "white",
|
||||
InputBackground: "blue",
|
||||
InputText: "white",
|
||||
UnreadCount: "yellow",
|
||||
Positive: "green",
|
||||
Negative: "red",
|
||||
},
|
||||
@@ -130,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
|
||||
@@ -160,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()
|
||||
|
||||
34
go.mod
34
go.mod
@@ -3,24 +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/gabriel-vasile/mimetype v1.1.2
|
||||
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
|
||||
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28
|
||||
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
|
||||
)
|
||||
|
||||
330
go.sum
330
go.sum
@@ -1,38 +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=
|
||||
@@ -42,106 +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=
|
||||
@@ -154,14 +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=
|
||||
|
||||
363
main.go
363
main.go
@@ -4,23 +4,24 @@ import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"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.9.0"
|
||||
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
|
||||
@@ -28,7 +29,7 @@ var textInput *tview.InputField
|
||||
var topBar *tview.TextView
|
||||
var infoBar *tview.TextView
|
||||
|
||||
var contactRoot *tview.TreeNode
|
||||
var chatRoot *tview.TreeNode
|
||||
var app *tview.Application
|
||||
|
||||
var sessionManager *messages.SessionManager
|
||||
@@ -45,7 +46,7 @@ func main() {
|
||||
|
||||
app = tview.NewApplication()
|
||||
|
||||
sideBarWidth := config.Config.Ui.ContactSidebarWidth
|
||||
sideBarWidth := config.Config.Ui.ChatSidebarWidth
|
||||
gridLayout := tview.NewGrid()
|
||||
gridLayout.SetRows(1, 0, 1)
|
||||
gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth)
|
||||
@@ -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
|
||||
}
|
||||
@@ -288,6 +309,7 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
|
||||
|
||||
// load the key map
|
||||
func LoadShortcuts() {
|
||||
// global bindings for app
|
||||
keyBindings = cbind.NewConfiguration()
|
||||
if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil {
|
||||
PrintErrorMsg("focus_messages:", err)
|
||||
@@ -295,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)
|
||||
}
|
||||
@@ -314,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)
|
||||
@@ -321,6 +353,12 @@ func LoadShortcuts() {
|
||||
if err := keysMessages.Set(config.Config.Keymap.MessageOpen, handleMessageCommand("open")); err != nil {
|
||||
PrintErrorMsg("message_open:", err)
|
||||
}
|
||||
if err := keysMessages.Set(config.Config.Keymap.Copyuser, handleCopyUser); err != nil {
|
||||
PrintErrorMsg("copyuser:", err)
|
||||
}
|
||||
if err := keysMessages.Set(config.Config.Keymap.Pasteuser, handlePasteUser); err != nil {
|
||||
PrintErrorMsg("pasteuser:", err)
|
||||
}
|
||||
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
|
||||
PrintErrorMsg("message_show:", err)
|
||||
}
|
||||
@@ -334,52 +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 to", config.Config.General.DownloadPath)
|
||||
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = download & open attachment in", config.Config.General.PreviewPath)
|
||||
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = download & show image using"+config.Config.General.ShowCommand, config.Config.General.PreviewPath+string(os.PathSeparator)+"filename.file")
|
||||
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, "[::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+"leave[::-] = leave group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"upload[::-] /path/to/file = upload any file as document to current chat")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendimage[::-] /path/to/file = send image as image message to current chat (also sendvideo/audio")
|
||||
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, "[::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, "")
|
||||
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
|
||||
@@ -390,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
|
||||
@@ -413,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("")
|
||||
@@ -428,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,54 +595,112 @@ func UpdateStatusBar(statusInfo messages.SessionStatus) {
|
||||
}
|
||||
out += ")[::-] "
|
||||
out += statusInfo.LastSeen
|
||||
go app.QueueUpdateDraw(func() {
|
||||
infoBar.SetText(out)
|
||||
})
|
||||
infoBar.SetText(out)
|
||||
//infoBar.SetText("🔋: ??%")
|
||||
}
|
||||
|
||||
// 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(sessionManager.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(sessionManager.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)
|
||||
}
|
||||
}
|
||||
@@ -581,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) {
|
||||
@@ -589,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 {
|
||||
|
||||
78
messages/messages.go
Normal file
78
messages/messages.go
Normal 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"
|
||||
@@ -2,123 +2,95 @@ package messages
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/rivo/tview"
|
||||
"mvdan.cc/xurls/v2"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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(sm.getTextMessageString(&msg), msg.Info.Id)
|
||||
sm.uiHandler.NewMessage(sm.createMessage(&msg))
|
||||
} else {
|
||||
screen, ids := sm.getMessagesString(sm.currentReceiver)
|
||||
sm.uiHandler.NewScreen(screen, ids)
|
||||
screen := sm.getMessages(sm.currentReceiver)
|
||||
sm.uiHandler.NewScreen(screen)
|
||||
}
|
||||
// notify if contact is in focus and we didn't send a message recently
|
||||
if config.Config.General.EnableNotifications {
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
|
||||
err := beeep.Notify(sm.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
|
||||
// notify if chat is in focus and we didn't send a message recently
|
||||
// TODO: move notify to UI
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
|
||||
sm.db.NewUnreadChat(msg.Info.RemoteJid)
|
||||
if !msg.Info.FromMe {
|
||||
err := notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text)
|
||||
if err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
@@ -126,19 +98,49 @@ func (sm *SessionManager) StartManager() error {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if config.Config.General.EnableNotifications {
|
||||
// notify if message is younger than 30 sec and not in focus
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
err := beeep.Notify(sm.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
|
||||
// notify if message is younger than 30 sec and not in focus
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
sm.db.NewUnreadChat(msg.Info.RemoteJid)
|
||||
if !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:
|
||||
@@ -147,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
|
||||
@@ -155,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")
|
||||
@@ -162,24 +172,29 @@ func (sm *SessionManager) StartManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the currently selected contact
|
||||
// set the currently selected chat
|
||||
func (sm *SessionManager) setCurrentReceiver(id string) {
|
||||
sm.currentReceiver = id
|
||||
screen, ids := sm.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 sm.connection == nil {
|
||||
wacc, err := whatsapp.NewConn(5 * time.Second)
|
||||
options := &whatsapp.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
LongClientName: "WhatsCLI Client",
|
||||
ShortClientName: "whatscli",
|
||||
}
|
||||
wacc, err := whatsapp.NewConnWithOptions(options)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
wac = wacc
|
||||
sm.connection = wac
|
||||
//wac.SetClientVersion(2, 2021, 4)
|
||||
wac.SetClientVersion(2, 2142, 12)
|
||||
} else {
|
||||
wac = sm.connection
|
||||
}
|
||||
@@ -195,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}
|
||||
@@ -226,7 +242,12 @@ func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving session: %v\n", err)
|
||||
}
|
||||
//<-time.After(3 * time.Second)
|
||||
//get initial battery state
|
||||
sm.BatteryChannel <- BatteryMsg{
|
||||
wac.Info.Battery,
|
||||
wac.Info.Plugged,
|
||||
false,
|
||||
}
|
||||
sm.StatusChannel <- StatusMsg{true, nil}
|
||||
return nil
|
||||
}
|
||||
@@ -244,6 +265,9 @@ func (sm *SessionManager) disconnect() error {
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -254,19 +278,19 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
default:
|
||||
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":
|
||||
@@ -281,19 +305,39 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
text := strings.Join(textParams, " ")
|
||||
sm.sendText(command.Params[0], text)
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]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("[" + config.Config.Colors.Negative + "]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("[" + config.Config.Colors.Negative + "]Usage:[-] info [message-id[]")
|
||||
sm.printCommandUsage("info", "[message-id[]")
|
||||
}
|
||||
case "download":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -303,7 +347,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintText("[::d] -> " + path + "[::-]")
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] download [message-id[]")
|
||||
sm.printCommandUsage("download", "[message-id[]")
|
||||
}
|
||||
case "open":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -313,7 +357,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] open [message-id[]")
|
||||
sm.printCommandUsage("open", "[message-id[]")
|
||||
}
|
||||
case "show":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -323,7 +367,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] show [message-id[]")
|
||||
sm.printCommandUsage("show", "[message-id[]")
|
||||
}
|
||||
case "url":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -335,23 +379,26 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] url [message-id[]")
|
||||
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 {
|
||||
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(),
|
||||
Content: file,
|
||||
Type: mime.String(),
|
||||
FileName: filepath.Base(file.Name()),
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
@@ -359,18 +406,21 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] upload /path/to/file")
|
||||
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 {
|
||||
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,
|
||||
@@ -384,18 +434,21 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] sendimage /path/to/file")
|
||||
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 {
|
||||
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,
|
||||
@@ -409,18 +462,21 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] sendvideo /path/to/file")
|
||||
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 {
|
||||
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,
|
||||
@@ -434,7 +490,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] sendaudio /path/to/file")
|
||||
sm.printCommandUsage("sendaudio", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "revoke":
|
||||
@@ -464,12 +520,13 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]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
|
||||
@@ -478,9 +535,147 @@ 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 {
|
||||
@@ -489,69 +684,40 @@ func checkParam(arr []string, length int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// gets a pretty name for a whatsapp id
|
||||
func (sm *SessionManager) GetIdName(id string) string {
|
||||
if val, ok := sm.getConnection().Store.Contacts[id]; ok {
|
||||
if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Notify != "" {
|
||||
return val.Notify
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(id, CONTACTSUFFIX)
|
||||
}
|
||||
|
||||
// gets a short name for a whatsapp id
|
||||
func (sm *SessionManager) GetIdShort(id string) string {
|
||||
if val, ok := sm.getConnection().Store.Contacts[id]; ok {
|
||||
if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Notify != "" {
|
||||
return val.Notify
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(id, CONTACTSUFFIX)
|
||||
}
|
||||
|
||||
// get a string representation of all messages for contact
|
||||
func (sm *SessionManager) getMessagesString(wid string) (string, []string) {
|
||||
out := ""
|
||||
ids := []string{}
|
||||
// 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 {
|
||||
out += sm.getTextMessageString(&msg)
|
||||
out += "\n"
|
||||
ids = append(ids, msg.Info.Id)
|
||||
ids = append(ids, sm.createMessage(&msg))
|
||||
}
|
||||
return out, ids
|
||||
return ids
|
||||
}
|
||||
|
||||
// create a formatted string with regions based on message ID from a text message
|
||||
// TODO: move message styling into UI
|
||||
func (sm *SessionManager) 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]" + sm.GetIdShort(userId) + ": [-::-]" + text
|
||||
} else { // message from others
|
||||
out += "[-::d](" + time + ") [" + colorContact + "::b]" + sm.GetIdShort(msg.Info.RemoteJid) + ": [-::-]" + text
|
||||
// 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)
|
||||
}
|
||||
out += "[\"\"]"
|
||||
return out
|
||||
return newMsg
|
||||
}
|
||||
|
||||
// load data for message specified by message id TODO: support types
|
||||
@@ -577,9 +743,9 @@ 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) {
|
||||
@@ -587,7 +753,9 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
case whatsapp.ImageMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
if len(exts) > 0 {
|
||||
fileName += exts[0]
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
@@ -601,7 +769,9 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
case whatsapp.DocumentMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
if len(exts) > 0 {
|
||||
fileName += exts[0]
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
@@ -615,7 +785,9 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
case whatsapp.AudioMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
if len(exts) > 0 {
|
||||
fileName += exts[0]
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
@@ -629,7 +801,9 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
case whatsapp.VideoMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
if len(exts) > 0 {
|
||||
fileName += exts[0]
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
@@ -657,13 +831,14 @@ func (sm *SessionManager) sendText(wid string, text string) {
|
||||
}
|
||||
|
||||
sm.lastSent = time.Now()
|
||||
_, err := sm.getConnection().Send(msg)
|
||||
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(sm.getTextMessageString(&msg), msg.Info.Id)
|
||||
sm.uiHandler.NewMessage(sm.createMessage(&msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -672,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("[" + config.Config.Colors.Negative + "]go-whatsapp reported an error:[-]")
|
||||
sm.uiHandler.PrintError(err)
|
||||
statusMsg := StatusMsg{false, err}
|
||||
sm.StatusChannel <- statusMsg
|
||||
@@ -748,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
|
||||
@@ -756,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)
|
||||
@@ -803,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, "")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package messages
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
)
|
||||
@@ -11,6 +12,8 @@ type MessageDatabase struct {
|
||||
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
|
||||
latestMessage map[string]uint64 // last message from RemoteJid
|
||||
otherMessages map[string]*interface{} // other non-text messages, stored by ID
|
||||
contacts map[string]Contact
|
||||
chats map[string]Chat
|
||||
}
|
||||
|
||||
// initialize the database
|
||||
@@ -20,6 +23,8 @@ func (db *MessageDatabase) Init() {
|
||||
db.messagesById = make(map[string]*whatsapp.TextMessage)
|
||||
db.otherMessages = make(map[string]*interface{})
|
||||
db.latestMessage = make(map[string]uint64)
|
||||
db.contacts = make(map[string]Contact)
|
||||
db.chats = make(map[string]Chat)
|
||||
}
|
||||
|
||||
// add a text message to the database, stored by RemoteJid
|
||||
@@ -36,6 +41,18 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
db.latestMessage[wid] = msg.Info.Timestamp
|
||||
didNew = true
|
||||
}
|
||||
//do we know this chat? if not add
|
||||
if _, ok := db.chats[msg.Info.RemoteJid]; !ok {
|
||||
//don't have this chat!
|
||||
isGroup := strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX)
|
||||
db.chats[msg.Info.RemoteJid] = Chat{
|
||||
msg.Info.RemoteJid,
|
||||
isGroup,
|
||||
db.GetIdName(msg.Info.RemoteJid),
|
||||
1,
|
||||
int64(msg.Info.Timestamp),
|
||||
}
|
||||
}
|
||||
//check if message exists, ignore otherwise
|
||||
if _, ok := db.messagesById[msg.Info.Id]; !ok {
|
||||
db.messagesById[msg.Info.Id] = msg
|
||||
@@ -47,6 +64,13 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
return didNew
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) NewUnreadChat(id string) {
|
||||
if chat, ok := db.chats[id]; ok {
|
||||
chat.Unread++
|
||||
db.chats[id] = chat
|
||||
}
|
||||
}
|
||||
|
||||
// add audio/video/image/doc message, stored by message id
|
||||
func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
|
||||
var id = ""
|
||||
@@ -66,21 +90,53 @@ func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) AddContact(contact Contact) {
|
||||
db.contacts[contact.Id] = contact
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) AddChat(chat Chat) {
|
||||
db.chats[chat.Id] = chat
|
||||
}
|
||||
|
||||
// get an array of all chat ids
|
||||
func (db *MessageDatabase) GetContactIds() []string {
|
||||
//var this = *db
|
||||
keys := make([]string, len(db.textMessages))
|
||||
func (db *MessageDatabase) GetChatIds() []Chat {
|
||||
keys := make([]Chat, len(db.chats))
|
||||
i := 0
|
||||
for k := range db.textMessages {
|
||||
for _, k := range db.chats {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return db.latestMessage[keys[i]] > db.latestMessage[keys[j]]
|
||||
return db.latestMessage[keys[i].Id] > db.latestMessage[keys[j].Id]
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// gets a pretty name for a whatsapp id
|
||||
func (sm *MessageDatabase) GetIdName(id string) string {
|
||||
if val, ok := sm.contacts[id]; ok {
|
||||
if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Short != "" {
|
||||
return val.Short
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
|
||||
}
|
||||
|
||||
// gets a short name for a whatsapp id
|
||||
func (sm *MessageDatabase) GetIdShort(id string) string {
|
||||
if val, ok := sm.contacts[id]; ok {
|
||||
//TODO val.notify from whatsapp??
|
||||
if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Name != "" {
|
||||
return val.Name
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) GetMessageInfo(id string) string {
|
||||
if _, ok := db.otherMessages[id]; ok {
|
||||
return "[yellow]OtherMessage[-]"
|
||||
|
||||
Reference in New Issue
Block a user