Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
65957ff732 | ||
|
|
7a096dbc94 | ||
|
|
a7977959b5 | ||
|
|
ea92d56426 | ||
|
|
3c193d219e | ||
|
|
f3a2bd3e88 | ||
|
|
f0488851ae | ||
|
|
c5276247b8 | ||
|
|
27d3a48d98 | ||
|
|
9065248d1c | ||
|
|
6e0c150e26 | ||
|
|
489b23899e | ||
|
|
8f50aa02d6 | ||
|
|
c3454e734f | ||
|
|
2138e671c4 | ||
|
|
0b6816f6e3 | ||
|
|
b72a5e0cc6 | ||
|
|
e4f1851b50 | ||
|
|
20f879271c | ||
|
|
d60e652d17 | ||
|
|
6f0d93e29e | ||
|
|
d7f8f0a918 | ||
|
|
8d89a2a7f4 | ||
|
|
0e4a694d09 | ||
|
|
754420e0c7 | ||
|
|
ae29e13108 | ||
|
|
0e3811de44 | ||
|
|
d1c0e870f0 | ||
|
|
795d8f7e63 | ||
|
|
5008ca46d8 | ||
|
|
e03e261d00 | ||
|
|
4a119a700f | ||
|
|
c1897b475a | ||
|
|
1672b42f7e | ||
|
|
8a28ea47fd | ||
|
|
3450fbc78f | ||
|
|
48ad9ce669 | ||
|
|
fd676f13cf | ||
|
|
63b5f3a604 | ||
|
|
b20031ff6a | ||
|
|
00516c3191 | ||
|
|
76c4010ce2 | ||
|
|
0b8d265024 |
12
.github/FUNDING.yml
vendored
Normal file
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
open_collective: #normen
|
||||
ko_fi: normen
|
||||
patreon: #normen
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
81
README.md
81
README.md
@@ -8,24 +8,26 @@ A command line interface for whatsapp, based on [go-whatsapp](https://github.com
|
||||
|
||||
Things that work.
|
||||
|
||||
- Sending and receiving WhatsApp messages in a command line app
|
||||
- Connects through the Web App API without a browser
|
||||
- Allows sending and receiving WhatsApp messages in a command line app
|
||||
- Allows downloading and opening image/video/audio/document attachments
|
||||
- Uses QR code for simple setup
|
||||
- Allows downloading and opening image/video/audio/document attachments
|
||||
- Allows sending documents
|
||||
- Allows color customization
|
||||
- Allows basic group management
|
||||
- Supports desktop notifications
|
||||
- Binaries for Windows, Mac, Linux and RaspBerry Pi
|
||||
|
||||
### Caveats
|
||||
|
||||
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
|
||||
Heres some things you might expect to work that don't. Plus some other things I should mention.
|
||||
|
||||
- Only shows existing chats
|
||||
- Only fetches a few old messages
|
||||
- No incoming message notification / count
|
||||
- No proper connection drop handling
|
||||
- No uploading of images/video/audio/data
|
||||
- No auto-reconnect when connection drops
|
||||
- No automation of messages, no sending of messages through shell commands
|
||||
- FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when FaceBook changes stuff in their web app
|
||||
|
||||
## Installation / Usage
|
||||
## Installation
|
||||
|
||||
How to get it running and how to use it
|
||||
|
||||
@@ -50,3 +52,66 @@ Some ways to install via package managers are supported but the installed versio
|
||||
|
||||
- `https://aur.archlinux.org/packages/whatscli/`
|
||||
|
||||
## Usage
|
||||
|
||||
Most information, all commands and key bindings are availabe through the in-app help, simply type `/help` and/or `/commands`.
|
||||
|
||||
### Login
|
||||
|
||||
When starting up, whatscli will immediately try to connect to the WhatsApp server to log in. Keep your phone ready to scan the appearing QR code in WhatsApp on your Phone. If you don't manage to scan the code quick enough just restart the application. If you can not see the whole QR code, reduce the font size of your terminal or increase the window size.
|
||||
|
||||
After scanning the QR code the chats should be populated. After you have done this once, whatscli will be able to log into WhatsApp automatically on start. To log out of WhapsApp completely type `/logout`.
|
||||
|
||||
### Messaging / Commands
|
||||
|
||||
Select a chat on the left and start typing in the input field at the bottom to send messages. Switch between the chat list and the input fiel with `<Tab>`.
|
||||
|
||||
For issuing commands the same input field is used. By default commands are prefixed with `/`. You can for example use the `/sendimage /path/to/file.jpg` command to send images, see `/help` for more commands.
|
||||
|
||||
When paths are given for commands you don't need to surround the path in quotes, even if it contains spaces. Also don't prefix spaces with backslashes (as the copy-paste function of MacOS does for example).
|
||||
|
||||
### Messages
|
||||
|
||||
When pressing `Ctrl-w` (default mapping) you enter "message selection mode" which allows selecting a single message and performing operations on them. For example pressing `o` while a message is selected allows opening any attachments through an external application.
|
||||
|
||||
#### Image display
|
||||
|
||||
You can display images in whatscli using external programs that convert the image to UTF characters. I found that `jp2a` works well for jpeg images, it is available through package managers on most systems. However the "image quality" leaves a lot to be desired. The [PIXterm](https://github.com/eliukblau/pixterm) app allows displaying true-color versions of the images which are quite recognizable already.
|
||||
|
||||
To configure the used command and its parameters edit the `show_command` parameter in `whatscli.config`, see `/help` for the config file location.
|
||||
|
||||
#### Copy-Pasting User IDs
|
||||
|
||||
Some commands such as the `/add` and `/remove` require a "user id" as their input. You can copy the user ID of a selected chat or a selected message to the clipboard with `Ctrl-c` (default mapping) and easily append them to the current input using `Ctrl-v`.
|
||||
|
||||
### Notifications
|
||||
|
||||
The app supports basic desktop notifications through the `gen2brain/beeep` library, to enable it set `enable_notifications = true` in `whatscli.config`. Set `use_terminal_bell = true` to ring your terminal's bell instead of sending a desktop notification.
|
||||
|
||||
### Configuration
|
||||
|
||||
Most key bindings, colors and other options can be configured in the `whatscli.config` file, the `/help` command shows its location.
|
||||
|
||||
## Development
|
||||
|
||||
This app started as my first attempt at writing something in go. Some areas that are marked with `TODO` can still be improved but work mostly. If you want to contribute features or improve the code thats great, send a PR and we can discuss.
|
||||
|
||||
### Building
|
||||
|
||||
Using a recent version of go, building should be straightforward. Either use `go build`, `go run` etc. or use the included Makefile.
|
||||
|
||||
### Structure Overview
|
||||
|
||||
The `main.go` contains most UI elements which are based around a tview app running on the main routine. It uses a keymap configuration based on the tslocum/cbind library. Apart from that it mostly manages the selection of messages in the current chat as well as displaying the messages and chat list that the session manager sends.
|
||||
|
||||
The `messages/session_manager.go` runs a separate go routine to receive messages from the Rhymen/go-whatsapp library which in turn runs the websocket connection to the whatsapp server. The session manager receives the messages from go-whatsapp and the commands from the UI via channels that it drains on its main routine. It then updates the UI accordingly using the UiMessageHandler interface. This ensures "thread safe" management of the connection and data while both UI and network connection run separately.
|
||||
|
||||
Session manager is designed "object like", the MessageDatabase in `messages/storage.go` is similar and somewhat linked to the session manager. In theory the session manager could be run multiple times (multiple accounts) or a different implementation of a session manager could connect to a different service like e.g. Telegram.
|
||||
|
||||
In `messages/messages.go` most interfaces and data structures for communication are kept.
|
||||
|
||||
The `config/settings.go` keeps a singleton `Config` struct with the config that is loaded via the gopkg.in/ini.v1 library when the app starts. This makes it easy to quickly add new configuration items with default values that can be used across the app.
|
||||
|
||||
## License
|
||||
|
||||
This software is released under MIT license. Remember that this gives you all freedom except for slapping your name on it.
|
||||
|
||||
@@ -6,48 +6,154 @@ import (
|
||||
"os/user"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"gitlab.com/tslocum/cbind"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
var configFilePath string
|
||||
var keyConfig *cbind.Configuration
|
||||
var cfg *ini.File
|
||||
|
||||
type IniFile struct {
|
||||
*General
|
||||
*Keymap
|
||||
*Ui
|
||||
*Colors
|
||||
}
|
||||
|
||||
type General struct {
|
||||
DownloadPath string
|
||||
PreviewPath string
|
||||
CmdPrefix string
|
||||
ShowCommand string
|
||||
EnableNotifications bool
|
||||
UseTerminalBell bool
|
||||
NotificationTimeout int64
|
||||
BacklogMsgQuantity int
|
||||
}
|
||||
|
||||
type Keymap struct {
|
||||
SwitchPanels string
|
||||
FocusMessages string
|
||||
FocusInput string
|
||||
FocusChats string
|
||||
Copyuser string
|
||||
Pasteuser string
|
||||
CommandBacklog string
|
||||
CommandRead string
|
||||
CommandConnect string
|
||||
CommandQuit string
|
||||
CommandHelp string
|
||||
MessageDownload string
|
||||
MessageOpen string
|
||||
MessageShow string
|
||||
MessageUrl string
|
||||
MessageInfo string
|
||||
MessageRevoke string
|
||||
}
|
||||
|
||||
type Ui struct {
|
||||
ChatSidebarWidth int
|
||||
}
|
||||
|
||||
type Colors struct {
|
||||
Background string
|
||||
Text string
|
||||
ForwardedText string
|
||||
ListHeader string
|
||||
ListContact string
|
||||
ListGroup string
|
||||
ChatContact string
|
||||
ChatMe string
|
||||
Borders string
|
||||
InputBackground string
|
||||
InputText string
|
||||
UnreadCount string
|
||||
Positive string
|
||||
Negative string
|
||||
}
|
||||
|
||||
var Config = IniFile{
|
||||
&General{
|
||||
DownloadPath: GetHomeDir() + "Downloads",
|
||||
PreviewPath: GetHomeDir() + "Downloads",
|
||||
CmdPrefix: "/",
|
||||
ShowCommand: "jp2a --color",
|
||||
EnableNotifications: false,
|
||||
UseTerminalBell: false,
|
||||
NotificationTimeout: 60,
|
||||
BacklogMsgQuantity: 10,
|
||||
},
|
||||
&Keymap{
|
||||
SwitchPanels: "Tab",
|
||||
FocusMessages: "Ctrl+w",
|
||||
FocusInput: "Ctrl+Space",
|
||||
FocusChats: "Ctrl+e",
|
||||
CommandBacklog: "Ctrl+b",
|
||||
CommandRead: "Ctrl+n",
|
||||
Copyuser: "Ctrl+c",
|
||||
Pasteuser: "Ctrl+v",
|
||||
CommandConnect: "Ctrl+r",
|
||||
CommandQuit: "Ctrl+q",
|
||||
CommandHelp: "Ctrl+?",
|
||||
MessageDownload: "d",
|
||||
MessageInfo: "i",
|
||||
MessageOpen: "o",
|
||||
MessageUrl: "u",
|
||||
MessageRevoke: "r",
|
||||
MessageShow: "s",
|
||||
},
|
||||
&Ui{
|
||||
ChatSidebarWidth: 30,
|
||||
},
|
||||
&Colors{
|
||||
Background: "black",
|
||||
Text: "white",
|
||||
ForwardedText: "purple",
|
||||
ListHeader: "yellow",
|
||||
ListContact: "green",
|
||||
ListGroup: "blue",
|
||||
ChatContact: "green",
|
||||
ChatMe: "blue",
|
||||
Borders: "white",
|
||||
InputBackground: "blue",
|
||||
InputText: "white",
|
||||
UnreadCount: "yellow",
|
||||
Positive: "green",
|
||||
Negative: "red",
|
||||
},
|
||||
}
|
||||
|
||||
func InitConfig() {
|
||||
var err error
|
||||
if configFilePath, err = xdg.ConfigFile("whatscli/whatscli.config"); err == nil {
|
||||
// add any new values
|
||||
var cfg *ini.File
|
||||
if cfg, err = ini.Load(configFilePath); err == nil {
|
||||
//TODO: check config for new parameters
|
||||
cfg.NameMapper = ini.TitleUnderscore
|
||||
cfg.ValueMapper = os.ExpandEnv
|
||||
if section, err := cfg.GetSection("general"); err == nil {
|
||||
section.MapTo(&Config.General)
|
||||
}
|
||||
if section, err := cfg.GetSection("keymap"); err == nil {
|
||||
section.MapTo(&Config.Keymap)
|
||||
}
|
||||
if section, err := cfg.GetSection("ui"); err == nil {
|
||||
section.MapTo(&Config.Ui)
|
||||
}
|
||||
if section, err := cfg.GetSection("colors"); err == nil {
|
||||
section.MapTo(&Config.Colors)
|
||||
}
|
||||
//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.NewSection("general")
|
||||
cfg.Section("general").NewKey("download_path", GetHomeDir()+"Downloads")
|
||||
cfg.Section("general").NewKey("preview_path", GetHomeDir()+"Downloads")
|
||||
cfg.NewSection("keymap")
|
||||
cfg.Section("keymap").NewKey("switch_panels", "Tab")
|
||||
cfg.Section("keymap").NewKey("focus_messages", "Ctrl+w")
|
||||
cfg.Section("keymap").NewKey("focus_input", "Ctrl+Space")
|
||||
cfg.Section("keymap").NewKey("focus_contacts", "Ctrl+e")
|
||||
cfg.Section("keymap").NewKey("command_connect", "Ctrl+r")
|
||||
cfg.Section("keymap").NewKey("command_quit", "Ctrl+q")
|
||||
cfg.Section("keymap").NewKey("command_help", "Ctrl+?")
|
||||
cfg.Section("keymap").NewKey("message_download", "d")
|
||||
cfg.Section("keymap").NewKey("message_open", "o")
|
||||
cfg.Section("keymap").NewKey("message_show", "s")
|
||||
cfg.Section("keymap").NewKey("message_info", "i")
|
||||
cfg.NewSection("ui")
|
||||
cfg.Section("ui").NewKey("contact_sidebar_width", "30")
|
||||
cfg.NewSection("colors")
|
||||
cfg.Section("colors").NewKey("background", "black")
|
||||
cfg.Section("colors").NewKey("text", "white")
|
||||
cfg.Section("colors").NewKey("list_header", "yellow")
|
||||
cfg.Section("colors").NewKey("list_contact", "green")
|
||||
cfg.Section("colors").NewKey("list_group", "blue")
|
||||
cfg.Section("colors").NewKey("chat_contact", "green")
|
||||
cfg.Section("colors").NewKey("chat_me", "blue")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
cfg.NameMapper = ini.TitleUnderscore
|
||||
cfg.ValueMapper = os.ExpandEnv
|
||||
if err = ini.ReflectFromWithMapper(cfg, &Config, ini.TitleUnderscore); err == nil {
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -66,59 +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.session"
|
||||
}
|
||||
|
||||
func GetKey(name string) string {
|
||||
if sec, err := cfg.GetSection("keymap"); err == nil {
|
||||
if key, err := sec.GetKey(name); err == nil {
|
||||
return key.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetColorName(key string) string {
|
||||
if sec, err := cfg.GetSection("colors"); err == nil {
|
||||
if key, err := sec.GetKey(key); err == nil {
|
||||
return key.String()
|
||||
}
|
||||
}
|
||||
return "white"
|
||||
}
|
||||
|
||||
func GetColor(key string) tcell.Color {
|
||||
name := GetColorName(key)
|
||||
if color, ok := tcell.ColorNames[name]; ok {
|
||||
return color
|
||||
}
|
||||
return tcell.ColorWhite
|
||||
}
|
||||
|
||||
func GetSetting(name string) string {
|
||||
if sec, err := cfg.GetSection("general"); err == nil {
|
||||
if key, err := sec.GetKey(name); err == nil {
|
||||
return key.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func GetIntSetting(section string, name string) int {
|
||||
if sec, err := cfg.GetSection(section); err == nil {
|
||||
if key, err := sec.GetKey(name); err == nil {
|
||||
if val, err := key.Int(); err == nil {
|
||||
return val
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// gets the OS home dir with a path separator at the end
|
||||
func GetHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
|
||||
10
go.mod
10
go.mod
@@ -5,7 +5,9 @@ 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
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
@@ -14,10 +16,12 @@ require (
|
||||
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
gitlab.com/tslocum/cbind v0.1.3
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
github.com/zyedidia/clipboard v1.0.3
|
||||
gitlab.com/tslocum/cbind v0.1.4
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 // indirect
|
||||
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
mvdan.cc/xurls/v2 v2.2.0
|
||||
)
|
||||
|
||||
40
go.sum
40
go.sum
@@ -15,11 +15,19 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/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/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.1-0.20201017141208-acf90d56d591 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4=
|
||||
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
|
||||
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
|
||||
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/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -38,11 +46,18 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/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/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/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/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
@@ -54,6 +69,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
|
||||
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/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/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=
|
||||
@@ -63,6 +80,7 @@ github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893 h1:24As98PZlIdjZn6V4wUu
|
||||
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
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=
|
||||
@@ -70,12 +88,16 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gitlab.com/tslocum/cbind v0.1.3 h1:FT/fTQ4Yj3eo5021lB3IbkIt8eVtYGhrw/xur+cjvUU=
|
||||
gitlab.com/tslocum/cbind v0.1.3/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
|
||||
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
|
||||
github.com/zyedidia/clipboard v1.0.3 h1:F/nCDVYMdbDWTmY8s8cJl0tnwX32q96IF09JHM14bUI=
|
||||
github.com/zyedidia/clipboard v1.0.3/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA=
|
||||
gitlab.com/tslocum/cbind v0.1.4 h1:cbZXPPcieXspk8cShoT6efz7HAT8yMNQcofYWNizis4=
|
||||
gitlab.com/tslocum/cbind v0.1.4/go.mod h1:RvwYE3auSjBNlCmWeGspzn+jdLUVQ8C2QGC+0nP9ChI=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -98,10 +120,11 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 h1:WFCmm2Hi9I2gYf1kv7LQ8ajKA5x9heC2v9xuUKwvf68=
|
||||
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -133,8 +156,13 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
|
||||
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"os"
|
||||
"os/user"
|
||||
"strings"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/normen/whatscli/config"
|
||||
)
|
||||
|
||||
var contacts map[string]string
|
||||
var connection *whatsapp.Conn
|
||||
|
||||
// loads custom contacts from disk
|
||||
func LoadContacts() {
|
||||
contacts = make(map[string]string)
|
||||
file, err := os.Open(config.GetContactsFilePath())
|
||||
if err != nil {
|
||||
// load old contacts file, re-save in new location if found
|
||||
file, err = os.Open(GetHomeDir() + ".whatscli.contacts")
|
||||
if err != nil {
|
||||
return
|
||||
} else {
|
||||
os.Remove(GetHomeDir() + ".whatscli.contacts")
|
||||
SaveContacts()
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
decoder := gob.NewDecoder(file)
|
||||
err = decoder.Decode(&contacts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// saves custom contacts to disk
|
||||
func SaveContacts() {
|
||||
file, err := os.Open(config.GetContactsFilePath())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
encoder := gob.NewEncoder(file)
|
||||
err = encoder.Encode(contacts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// sets a new name for a whatsapp id
|
||||
func SetIdName(id string, name string) {
|
||||
contacts[id] = name
|
||||
SaveContacts()
|
||||
}
|
||||
|
||||
// gets a pretty name for a whatsapp id
|
||||
func GetIdName(id string) string {
|
||||
if _, ok := contacts[id]; ok {
|
||||
return contacts[id]
|
||||
}
|
||||
if val, ok := connection.Store.Contacts[id]; ok {
|
||||
if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Notify != "" {
|
||||
return val.Notify
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(id, CONTACTSUFFIX)
|
||||
}
|
||||
|
||||
// gets a short name for a whatsapp id
|
||||
func GetIdShort(id string) string {
|
||||
if val, ok := connection.Store.Contacts[id]; ok {
|
||||
if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Notify != "" {
|
||||
return val.Notify
|
||||
}
|
||||
}
|
||||
if _, ok := contacts[id]; ok {
|
||||
return contacts[id]
|
||||
}
|
||||
return strings.TrimSuffix(id, CONTACTSUFFIX)
|
||||
}
|
||||
|
||||
// gets the OS home dir with a path separator at the end
|
||||
func GetHomeDir() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
}
|
||||
return usr.HomeDir + string(os.PathSeparator)
|
||||
}
|
||||
78
messages/messages.go
Normal file
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,58 +2,218 @@ package messages
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"sync"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/normen/whatscli/config"
|
||||
"github.com/normen/whatscli/qrcode"
|
||||
"github.com/rivo/tview"
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
var textView *tview.TextView
|
||||
var connMutex sync.Mutex
|
||||
// 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 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
|
||||
}
|
||||
|
||||
// TODO: remove this circular dependeny in favor of a better way
|
||||
func SetTextView(tv *tview.TextView) {
|
||||
textView = tv
|
||||
// initialize the SessionManager
|
||||
func (sm *SessionManager) Init(handler UiMessageHandler) {
|
||||
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 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 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.createMessage(&msg))
|
||||
} else {
|
||||
screen := sm.getMessages(sm.currentReceiver)
|
||||
sm.uiHandler.NewScreen(screen)
|
||||
}
|
||||
// notify if chat is in focus and we didn't send a message recently
|
||||
// TODO: move notify to UI
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
|
||||
sm.db.NewUnreadChat(msg.Info.RemoteJid)
|
||||
if !msg.Info.FromMe {
|
||||
err := notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text)
|
||||
if err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// notify if message is younger than 30 sec and not in focus
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
sm.db.NewUnreadChat(msg.Info.RemoteJid)
|
||||
if !msg.Info.FromMe {
|
||||
err := notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text)
|
||||
if err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sm.uiHandler.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:
|
||||
sm.statusInfo.BatteryLoading = batteryMsg.loading
|
||||
sm.statusInfo.BatteryPowersave = batteryMsg.powersave
|
||||
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
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
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")
|
||||
wac.Disconnect()
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the currently selected chat
|
||||
func (sm *SessionManager) setCurrentReceiver(id string) {
|
||||
sm.currentReceiver = id
|
||||
screen := sm.getMessages(id)
|
||||
sm.uiHandler.NewScreen(screen)
|
||||
}
|
||||
|
||||
// gets an existing connection or creates one
|
||||
func GetConnection() *whatsapp.Conn {
|
||||
connMutex.Lock()
|
||||
defer connMutex.Unlock()
|
||||
func (sm *SessionManager) getConnection() *whatsapp.Conn {
|
||||
var wac *whatsapp.Conn
|
||||
if connection == nil {
|
||||
wacc, err := whatsapp.NewConn(5 * time.Second)
|
||||
if sm.connection == nil {
|
||||
options := &whatsapp.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
LongClientName: "WhatsCLI Client",
|
||||
ShortClientName: "whatscli",
|
||||
}
|
||||
wacc, err := whatsapp.NewConnWithOptions(options)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
wac = wacc
|
||||
connection = wac
|
||||
sm.connection = wac
|
||||
//wac.SetClientVersion(2, 2021, 4)
|
||||
} else {
|
||||
wac = connection
|
||||
wac = sm.connection
|
||||
}
|
||||
return wac
|
||||
}
|
||||
|
||||
// Login logs in the user. It ries to see if a session already exists. If not, tries to create a
|
||||
// login logs in the user. It ries to see if a session already exists. If not, tries to create a
|
||||
// new one using qr scanned on the terminal.
|
||||
func Login() error {
|
||||
return LoginWithConnection(GetConnection())
|
||||
func (sm *SessionManager) login() error {
|
||||
return sm.loginWithConnection(sm.getConnection())
|
||||
}
|
||||
|
||||
// LoginWithConnection logs in the user using a provided connection. It ries to see if a session already exists. If not, tries to create a
|
||||
// loginWithConnection logs in the user using a provided connection. It ries to see if a session already exists. If not, tries to create a
|
||||
// new one using qr scanned on the terminal.
|
||||
func LoginWithConnection(wac *whatsapp.Conn) error {
|
||||
connMutex.Lock()
|
||||
defer connMutex.Unlock()
|
||||
func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
|
||||
sm.uiHandler.PrintText("connecting..")
|
||||
if wac != nil && wac.GetConnected() {
|
||||
wac.Disconnect()
|
||||
sm.StatusChannel <- StatusMsg{false, nil}
|
||||
}
|
||||
//load saved session
|
||||
session, err := readSession()
|
||||
@@ -68,7 +228,7 @@ func LoginWithConnection(wac *whatsapp.Conn) error {
|
||||
qr := make(chan string)
|
||||
go func() {
|
||||
terminal := qrcode.New()
|
||||
terminal.SetOutput(tview.ANSIWriter(textView))
|
||||
terminal.SetOutput(tview.ANSIWriter(sm.uiHandler.GetWriter()))
|
||||
terminal.Get(<-qr).Print()
|
||||
}()
|
||||
session, err = wac.Login(qr)
|
||||
@@ -82,37 +242,719 @@ func LoginWithConnection(wac *whatsapp.Conn) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving session: %v\n", err)
|
||||
}
|
||||
//<-time.After(3 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Disconnect() error {
|
||||
wac := GetConnection()
|
||||
if wac != nil && wac.GetConnected() {
|
||||
_, err := wac.Disconnect()
|
||||
return err
|
||||
//get initial battery state
|
||||
sm.BatteryChannel <- BatteryMsg{
|
||||
wac.Info.Battery,
|
||||
wac.Info.Plugged,
|
||||
false,
|
||||
}
|
||||
sm.StatusChannel <- StatusMsg{true, nil}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Logout logs out the user.
|
||||
func Logout() error {
|
||||
connMutex.Lock()
|
||||
defer connMutex.Unlock()
|
||||
// disconnects the session
|
||||
func (sm *SessionManager) disconnect() error {
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
if wac != nil && wac.GetConnected() {
|
||||
_, err = wac.Disconnect()
|
||||
}
|
||||
sm.StatusChannel <- StatusMsg{false, err}
|
||||
return err
|
||||
}
|
||||
|
||||
// logout logs out the user, deletes session file
|
||||
func (ub *SessionManager) logout() error {
|
||||
err := ub.getConnection().Logout()
|
||||
ub.StatusChannel <- StatusMsg{false, err}
|
||||
ub.uiHandler.PrintText("removing login data..")
|
||||
return removeSession()
|
||||
}
|
||||
|
||||
// executes a command
|
||||
func (sm *SessionManager) execCommand(command Command) {
|
||||
cmd := command.Name
|
||||
switch cmd {
|
||||
default:
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Unknown command: [-]" + cmd)
|
||||
case "backlog":
|
||||
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")
|
||||
}
|
||||
case "login":
|
||||
sm.uiHandler.PrintError(sm.login())
|
||||
case "connect":
|
||||
sm.uiHandler.PrintError(sm.login())
|
||||
case "disconnect":
|
||||
sm.uiHandler.PrintError(sm.disconnect())
|
||||
case "logout":
|
||||
sm.uiHandler.PrintError(sm.logout())
|
||||
case "send":
|
||||
if checkParam(command.Params, 2) {
|
||||
textParams := command.Params[1:]
|
||||
text := strings.Join(textParams, " ")
|
||||
sm.sendText(command.Params[0], text)
|
||||
} else {
|
||||
sm.printCommandUsage("send", "[chat-id[] [message text[]")
|
||||
}
|
||||
case "select":
|
||||
if checkParam(command.Params, 1) {
|
||||
sm.setCurrentReceiver(command.Params[0])
|
||||
} else {
|
||||
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.printCommandUsage("info", "[message-id[]")
|
||||
}
|
||||
case "download":
|
||||
if checkParam(command.Params, 1) {
|
||||
if path, err := sm.downloadMessage(command.Params[0], false); err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[::d] -> " + path + "[::-]")
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("download", "[message-id[]")
|
||||
}
|
||||
case "open":
|
||||
if checkParam(command.Params, 1) {
|
||||
if path, err := sm.downloadMessage(command.Params[0], true); err == nil {
|
||||
sm.uiHandler.OpenFile(path)
|
||||
} else {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("open", "[message-id[]")
|
||||
}
|
||||
case "show":
|
||||
if checkParam(command.Params, 1) {
|
||||
if path, err := sm.downloadMessage(command.Params[0], true); err == nil {
|
||||
sm.uiHandler.PrintFile(path)
|
||||
} else {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("show", "[message-id[]")
|
||||
}
|
||||
case "url":
|
||||
if checkParam(command.Params, 1) {
|
||||
if msg, ok := sm.db.messagesById[command.Params[0]]; ok {
|
||||
urlParser := xurls.Relaxed()
|
||||
url := urlParser.FindString(msg.Text)
|
||||
if url != "" {
|
||||
sm.uiHandler.OpenFile(url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("url", "[message-id[]")
|
||||
}
|
||||
case "upload":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("upload", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.DocumentMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
FileName: filepath.Base(file.Name()),
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("upload", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendimage":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendimage", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.ImageMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendimage", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendvideo":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendvideo", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.VideoMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendvideo", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendaudio":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendaudio", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.AudioMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendaudio", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "revoke":
|
||||
if checkParam(command.Params, 1) {
|
||||
wac := sm.getConnection()
|
||||
var revId string
|
||||
var err error
|
||||
if msgg, ok := sm.db.otherMessages[command.Params[0]]; ok {
|
||||
switch msg := (*msgg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
revId, err = wac.RevokeMessage(msg.Info.RemoteJid, msg.Info.Id, msg.Info.FromMe)
|
||||
case whatsapp.DocumentMessage:
|
||||
revId, err = wac.RevokeMessage(msg.Info.RemoteJid, msg.Info.Id, msg.Info.FromMe)
|
||||
case whatsapp.AudioMessage:
|
||||
revId, err = wac.RevokeMessage(msg.Info.RemoteJid, msg.Info.Id, msg.Info.FromMe)
|
||||
case whatsapp.VideoMessage:
|
||||
revId, err = wac.RevokeMessage(msg.Info.RemoteJid, msg.Info.Id, msg.Info.FromMe)
|
||||
}
|
||||
} else {
|
||||
if msg, ok := sm.db.messagesById[command.Params[0]]; ok {
|
||||
revId, err = wac.RevokeMessage(msg.Info.RemoteJid, msg.Info.Id, msg.Info.FromMe)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("revoked: " + revId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
} else {
|
||||
sm.printCommandUsage("revoke", "[message-id[]")
|
||||
}
|
||||
case "leave":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.LeaveGroup(groupId)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("left group " + groupId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "create":
|
||||
if !checkParam(command.Params, 1) {
|
||||
sm.printCommandUsage("create", "[user-id[] [user-id[] New Group Subject")
|
||||
sm.printCommandUsage("create", "New Group Subject")
|
||||
return
|
||||
}
|
||||
// first params are users if ending in CONTACTSUFFIX, rest is name
|
||||
users := []string{}
|
||||
idx := 0
|
||||
size := len(command.Params)
|
||||
for idx = 0; idx < size && strings.Index(command.Params[idx], CONTACTSUFFIX) > 0; idx++ {
|
||||
users = append(users, command.Params[idx])
|
||||
}
|
||||
name := ""
|
||||
if len(command.Params) > idx {
|
||||
name = strings.Join(command.Params[idx:], " ")
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
var groupId <-chan string
|
||||
groupId, err = wac.CreateGroup(name, users)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("creating new group " + name)
|
||||
resultInfo := <-groupId
|
||||
//{"status":200,"gid":"491600000009-0606000436@g.us","participants":[{"491700000000@c.us":{"code":"200"}},{"4917600000001@c.us":{"code": "200"}}]}
|
||||
var result map[string]interface{}
|
||||
json.Unmarshal([]byte(resultInfo), &result)
|
||||
newChatId := result["gid"].(string)
|
||||
sm.uiHandler.PrintText("got new Id " + newChatId)
|
||||
newChat := Chat{}
|
||||
newChat.Id = newChatId
|
||||
newChat.Name = name
|
||||
newChat.IsGroup = true
|
||||
sm.db.chats[newChatId] = newChat
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "add":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
if !checkParam(command.Params, 1) {
|
||||
sm.printCommandUsage("add", "[user-id[]")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.AddMember(groupId, command.Params)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("added new members for " + groupId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "remove":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
if !checkParam(command.Params, 1) {
|
||||
sm.printCommandUsage("remove", "[user-id[]")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.RemoveMember(groupId, command.Params)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("removed from " + groupId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "removeadmin":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
if !checkParam(command.Params, 1) {
|
||||
sm.printCommandUsage("removeadmin", "[user-id[]")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.RemoveAdmin(groupId, command.Params)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("removed admin for " + groupId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "admin":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
if !checkParam(command.Params, 1) {
|
||||
sm.printCommandUsage("admin", "[user-id[]")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.SetAdmin(groupId, command.Params)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("added admin for " + groupId)
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "subject":
|
||||
groupId := sm.currentReceiver
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
if !checkParam(command.Params, 1) || groupId == "" {
|
||||
sm.printCommandUsage("subject", "new-subject -> in group chat")
|
||||
return
|
||||
}
|
||||
name := strings.Join(command.Params, " ")
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.UpdateGroupSubject(name, groupId)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("updated subject for " + groupId)
|
||||
}
|
||||
newChat := sm.db.chats[groupId]
|
||||
newChat.Name = name
|
||||
sm.db.chats[groupId] = newChat
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "colorlist":
|
||||
out := ""
|
||||
for idx, _ := range tcell.ColorNames {
|
||||
out = out + "[" + idx + "]" + idx + "[-]\n"
|
||||
}
|
||||
sm.uiHandler.PrintText(out)
|
||||
}
|
||||
}
|
||||
|
||||
// helper for built-in command help
|
||||
func (sm *SessionManager) printCommandUsage(command string, usage string) {
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] " + command + " " + usage)
|
||||
}
|
||||
|
||||
// check if parameters for command are okay
|
||||
func checkParam(arr []string, length int) bool {
|
||||
if arr == nil || len(arr) < length {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// get all messages for one chat id
|
||||
func (sm *SessionManager) getMessages(wid string) []Message {
|
||||
msgs := sm.db.GetMessages(wid)
|
||||
ids := []Message{}
|
||||
for _, msg := range msgs {
|
||||
ids = append(ids, sm.createMessage(&msg))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// create internal message from whatsapp message
|
||||
// TODO: store these instead of generating each time
|
||||
func (sm *SessionManager) createMessage(msg *whatsapp.TextMessage) Message {
|
||||
newMsg := Message{}
|
||||
newMsg.Id = msg.Info.Id
|
||||
newMsg.ChatId = msg.Info.RemoteJid
|
||||
newMsg.FromMe = msg.Info.FromMe
|
||||
newMsg.Timestamp = msg.Info.Timestamp
|
||||
newMsg.Text = msg.Text
|
||||
newMsg.Forwarded = msg.ContextInfo.IsForwarded
|
||||
if strings.Contains(msg.Info.RemoteJid, STATUSSUFFIX) {
|
||||
newMsg.ContactId = msg.Info.SenderJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
|
||||
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) {
|
||||
newMsg.ContactId = msg.Info.SenderJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
|
||||
} else {
|
||||
newMsg.ContactId = msg.Info.RemoteJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.RemoteJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.RemoteJid)
|
||||
}
|
||||
return newMsg
|
||||
}
|
||||
|
||||
// load data for message specified by message id TODO: support types
|
||||
func (sm *SessionManager) loadMessageData(wid string) ([]byte, error) {
|
||||
if msg, ok := sm.db.otherMessages[wid]; ok {
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
return v.Download()
|
||||
case whatsapp.DocumentMessage:
|
||||
//return v.Download()
|
||||
case whatsapp.AudioMessage:
|
||||
//return v.Download()
|
||||
case whatsapp.VideoMessage:
|
||||
//return v.Download()
|
||||
}
|
||||
}
|
||||
return []byte{}, errors.New("This is not an image message")
|
||||
}
|
||||
|
||||
// attempts to download a messages attachments, returns path or error message
|
||||
func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, error) {
|
||||
if msg, ok := sm.db.otherMessages[wid]; ok {
|
||||
var fileName string = ""
|
||||
if preview {
|
||||
fileName += config.Config.General.PreviewPath
|
||||
} else {
|
||||
fileName += config.Config.General.DownloadPath
|
||||
}
|
||||
fileName += string(os.PathSeparator)
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.DocumentMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.AudioMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.VideoMessage:
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("No attachments found")
|
||||
}
|
||||
|
||||
// sends text to whatsapp id
|
||||
func (sm *SessionManager) sendText(wid string, text string) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: wid,
|
||||
FromMe: true,
|
||||
Timestamp: uint64(time.Now().Unix()),
|
||||
},
|
||||
Text: text,
|
||||
}
|
||||
|
||||
sm.lastSent = time.Now()
|
||||
newid, err := sm.getConnection().Send(msg)
|
||||
msg.Info.Id = newid
|
||||
if err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
} else {
|
||||
sm.db.AddTextMessage(&msg)
|
||||
if sm.currentReceiver == wid {
|
||||
sm.uiHandler.NewMessage(sm.createMessage(&msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handler struct for whatsapp callbacks
|
||||
|
||||
// HandleError implements the error handler interface for go-whatsapp
|
||||
func (sm *SessionManager) HandleError(err error) {
|
||||
sm.uiHandler.PrintError(err)
|
||||
statusMsg := StatusMsg{false, err}
|
||||
sm.StatusChannel <- statusMsg
|
||||
return
|
||||
}
|
||||
|
||||
// HandleTextMessage implements the text message handler interface for go-whatsapp
|
||||
func (sm *SessionManager) HandleTextMessage(msg whatsapp.TextMessage) {
|
||||
sm.TextChannel <- msg
|
||||
}
|
||||
|
||||
// methods to convert messages to TextMessage
|
||||
func (sm *SessionManager) HandleImageMessage(message whatsapp.ImageMessage) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: message.Info.RemoteJid,
|
||||
SenderJid: message.Info.SenderJid,
|
||||
FromMe: message.Info.FromMe,
|
||||
Timestamp: message.Info.Timestamp,
|
||||
Id: message.Info.Id,
|
||||
},
|
||||
Text: "[IMAGE] " + message.Caption,
|
||||
}
|
||||
sm.HandleTextMessage(msg)
|
||||
sm.OtherChannel <- message
|
||||
}
|
||||
|
||||
func (sm *SessionManager) HandleDocumentMessage(message whatsapp.DocumentMessage) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: message.Info.RemoteJid,
|
||||
SenderJid: message.Info.SenderJid,
|
||||
FromMe: message.Info.FromMe,
|
||||
Timestamp: message.Info.Timestamp,
|
||||
Id: message.Info.Id,
|
||||
},
|
||||
Text: "[DOCUMENT] " + message.Title,
|
||||
}
|
||||
sm.HandleTextMessage(msg)
|
||||
sm.OtherChannel <- message
|
||||
}
|
||||
|
||||
func (sm *SessionManager) HandleVideoMessage(message whatsapp.VideoMessage) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: message.Info.RemoteJid,
|
||||
SenderJid: message.Info.SenderJid,
|
||||
FromMe: message.Info.FromMe,
|
||||
Timestamp: message.Info.Timestamp,
|
||||
Id: message.Info.Id,
|
||||
},
|
||||
Text: "[VIDEO] " + message.Caption,
|
||||
}
|
||||
sm.HandleTextMessage(msg)
|
||||
sm.OtherChannel <- message
|
||||
}
|
||||
|
||||
func (sm *SessionManager) HandleAudioMessage(message whatsapp.AudioMessage) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: message.Info.RemoteJid,
|
||||
SenderJid: message.Info.SenderJid,
|
||||
FromMe: message.Info.FromMe,
|
||||
Timestamp: message.Info.Timestamp,
|
||||
Id: message.Info.Id,
|
||||
},
|
||||
Text: "[AUDIO]",
|
||||
}
|
||||
sm.HandleTextMessage(msg)
|
||||
sm.OtherChannel <- message
|
||||
}
|
||||
|
||||
// add contact info to database (not needed, internal db of connection is used)
|
||||
func (sm *SessionManager) HandleNewContact(contact whatsapp.Contact) {
|
||||
// redundant, wac has contacts
|
||||
sm.ContactChannel <- contact
|
||||
}
|
||||
|
||||
// handle battery messages
|
||||
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)
|
||||
return path, err
|
||||
}
|
||||
|
||||
// reads the session file from disk
|
||||
func readSession() (whatsapp.Session, error) {
|
||||
session := whatsapp.Session{}
|
||||
file, err := os.Open(config.GetSessionFilePath())
|
||||
if err != nil {
|
||||
// load old session file, delete if found
|
||||
file, err = os.Open(GetHomeDir() + ".whatscli.session")
|
||||
file, err = os.Open(config.GetHomeDir() + ".whatscli.session")
|
||||
if err != nil {
|
||||
return session, err
|
||||
} else {
|
||||
os.Remove(GetHomeDir() + ".whatscli.session")
|
||||
os.Remove(config.GetHomeDir() + ".whatscli.session")
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -143,3 +985,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, "")
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
package messages
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/normen/whatscli/config"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const GROUPSUFFIX = "@g.us"
|
||||
const CONTACTSUFFIX = "@s.whatsapp.net"
|
||||
|
||||
type MessageDatabase struct {
|
||||
textMessages map[string][]*whatsapp.TextMessage // text messages stored by RemoteJid
|
||||
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
|
||||
latestMessage map[string]uint64 // last message from RemoteJid
|
||||
otherMessages map[string]*interface{} // other non-text messages, stored by ID
|
||||
mutex sync.Mutex
|
||||
contacts map[string]Contact
|
||||
chats map[string]Chat
|
||||
}
|
||||
|
||||
// initialize the database
|
||||
@@ -32,12 +23,12 @@ 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
|
||||
func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
//var this = *db
|
||||
var didNew = false
|
||||
var wid = msg.Info.RemoteJid
|
||||
@@ -50,18 +41,38 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
db.latestMessage[wid] = msg.Info.Timestamp
|
||||
didNew = true
|
||||
}
|
||||
db.textMessages[wid] = append(db.textMessages[wid], msg)
|
||||
db.messagesById[msg.Info.Id] = msg
|
||||
sort.Slice(db.textMessages[wid], func(i, j int) bool {
|
||||
return db.textMessages[wid][i].Info.Timestamp < db.textMessages[wid][j].Info.Timestamp
|
||||
})
|
||||
//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
|
||||
db.textMessages[wid] = append(db.textMessages[wid], msg)
|
||||
sort.Slice(db.textMessages[wid], func(i, j int) bool {
|
||||
return db.textMessages[wid][i].Info.Timestamp < db.textMessages[wid][j].Info.Timestamp
|
||||
})
|
||||
}
|
||||
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{}) {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
var id = ""
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
@@ -79,26 +90,54 @@ 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 {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
//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 {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
if _, ok := db.otherMessages[id]; ok {
|
||||
return "[yellow]OtherMessage[-]"
|
||||
}
|
||||
@@ -115,128 +154,10 @@ func (db *MessageDatabase) GetMessageInfo(id string) string {
|
||||
}
|
||||
|
||||
// get a string containing all messages for a chat by chat id
|
||||
func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
//var this = *db
|
||||
var out = ""
|
||||
var arr = []string{}
|
||||
func (db *MessageDatabase) GetMessages(wid string) []whatsapp.TextMessage {
|
||||
var arr = []whatsapp.TextMessage{}
|
||||
for _, element := range db.textMessages[wid] {
|
||||
out += GetTextMessageString(element)
|
||||
out += "\n"
|
||||
arr = append(arr, element.Info.Id)
|
||||
arr = append(arr, *element)
|
||||
}
|
||||
return out, arr
|
||||
}
|
||||
|
||||
// load data for message specified by message id TODO: support types
|
||||
func (db *MessageDatabase) LoadMessageData(wid string) ([]byte, error) {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
if msg, ok := db.otherMessages[wid]; ok {
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
return v.Download()
|
||||
case whatsapp.DocumentMessage:
|
||||
//return v.Download()
|
||||
case whatsapp.AudioMessage:
|
||||
//return v.Download()
|
||||
case whatsapp.VideoMessage:
|
||||
//return v.Download()
|
||||
}
|
||||
}
|
||||
return []byte{}, errors.New("This is not an image message")
|
||||
}
|
||||
|
||||
// attempts to download a messages attachments, returns path or error message
|
||||
func (db *MessageDatabase) DownloadMessage(wid string, preview bool) (string, error) {
|
||||
db.mutex.Lock()
|
||||
defer db.mutex.Unlock()
|
||||
if msg, ok := db.otherMessages[wid]; ok {
|
||||
var fileName string = ""
|
||||
if preview {
|
||||
fileName += config.GetSetting("download_path")
|
||||
} else {
|
||||
fileName += config.GetSetting("preview_path")
|
||||
}
|
||||
fileName += string(os.PathSeparator)
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "image/")
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.DocumentMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(strings.TrimPrefix(v.Type, "application/"), "document/")
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.AudioMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "audio/")
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
case whatsapp.VideoMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "video/")
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
if data, err := v.Download(); err == nil {
|
||||
return saveAttachment(data, fileName)
|
||||
} else {
|
||||
return fileName, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", errors.New("No attachments found")
|
||||
}
|
||||
|
||||
// create a formatted string with regions based on message ID from a text message
|
||||
func GetTextMessageString(msg *whatsapp.TextMessage) string {
|
||||
colorMe := config.GetColorName("chat_me")
|
||||
colorContact := config.GetColorName("chat_contact")
|
||||
out := ""
|
||||
text := tview.Escape(msg.Text)
|
||||
tim := time.Unix(int64(msg.Info.Timestamp), 0)
|
||||
time := tim.Format("02-01-06 15:04:05")
|
||||
out += "[\""
|
||||
out += msg.Info.Id
|
||||
out += "\"]"
|
||||
if msg.Info.FromMe { //msg from me
|
||||
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
|
||||
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) { // group msg
|
||||
userId := msg.Info.SenderJid
|
||||
out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(userId) + ": [-::-]" + text
|
||||
} else { // message from others
|
||||
out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(msg.Info.RemoteJid) + ": [-::-]" + text
|
||||
}
|
||||
out += "[\"\"]"
|
||||
return out
|
||||
}
|
||||
|
||||
// helper to save an attachment and open it if specified
|
||||
func saveAttachment(data []byte, path string) (string, error) {
|
||||
err := ioutil.WriteFile(path, data, 0644)
|
||||
return path, err
|
||||
return arr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user