Compare commits

...

70 Commits

Author SHA1 Message Date
normen
3af2a3738f fix unread count not updating 2020-11-27 14:22:20 +01:00
normen
e31c2c3e36 get the battery state on connection 2020-11-27 03:30:13 +01:00
normen
3f20981550 cleaner logout 2020-11-27 03:10:25 +01:00
normen
f8368e4998 add /read command to set chat to read
- display connection status (connecting, connected, disconnected)
- allow setting unread count color
2020-11-27 00:11:59 +01:00
normen
121a73c312 add unread count (no reset yet) 2020-11-26 22:52:47 +01:00
normen
86af0d82a4 remove contact file name getter 2020-11-26 21:50:41 +01:00
normen
b94129fb0e rename focus_contacts and contact_sidebar_width - sorry! 2020-11-26 21:48:59 +01:00
normen
59a843bb8d use chat and contact list reported by go-whatsapp
Fixes #22
2020-11-26 21:43:59 +01:00
normen
a86aa3eec3 show contact name for status messages 2020-11-26 18:53:22 +01:00
normen
a20b4e3592 update dependencies 2020-11-26 18:45:23 +01:00
normen
9e62295188 unify variable names 2020-11-26 02:46:00 +01:00
normen
2e891d05ca don't start session manager twice 2020-11-26 02:17:44 +01:00
normen
bfbec54de3 remove number suffix for groups in fallback 2020-11-26 02:10:28 +01:00
normen
c677bce14e fix contact/group color mixup in contact list 2020-11-26 02:05:24 +01:00
normen
6f30efeebe abstract messages, move message styling to UI 2020-11-26 01:27:48 +01:00
normen
14a0e74b25 use pointer for database 2020-11-25 21:34:27 +01:00
normen
4951ca24da help screen cleanups 2020-11-25 03:19:28 +01:00
normen
1cd6c25b02 fix preview and download path mixup 2020-11-25 03:11:38 +01:00
normen
d9a9e7f753 separate messages and interfaces to separate file 2020-11-25 02:12:57 +01:00
normen
5388a1b408 cleanup help 2020-11-25 01:38:06 +01:00
normen
7687af38e1 set file name when uploading 2020-11-24 23:16:29 +01:00
normen
9b26641a0c don't swallow error messages on upload/send 2020-11-24 22:53:36 +01:00
normen
e5ef667909 show help and set no receiver on contact root 2020-11-24 22:29:50 +01:00
normen
c39479f12b fix notifications for own messages 2020-11-24 22:28:24 +01:00
normen
f1109f6465 add /colorlist command 2020-11-24 21:05:12 +01:00
normen
127883701d fix version snafu in 0.9.1 release 2020-11-24 20:52:07 +01:00
normen
5cacc3c5ea bump version 2020-11-24 20:46:17 +01:00
normen
65957ff732 use color_negative for all error messages 2020-11-24 20:38:04 +01:00
normen
7a096dbc94 allow configuring command for image to text conversion 2020-11-24 20:25:21 +01:00
normen
a7977959b5 help info update 2020-11-24 19:19:51 +01:00
normen
ea92d56426 allow sending image, video and audio messages
files have to be right format to be recognized by WhatsApp
2020-11-24 19:16:37 +01:00
normen
3c193d219e allow opening URLs in messages
Fixes #19
2020-11-24 18:47:58 +01:00
normen
f3a2bd3e88 add some code documentation 2020-11-24 17:37:07 +01:00
normen
f0488851ae remove local contacts feature creep
remove singleton connection
2020-11-24 17:30:06 +01:00
normen
c5276247b8 avoid sending notifications when loading backlog 2020-11-24 14:20:26 +01:00
normen
27d3a48d98 update readme 2020-11-24 14:03:52 +01:00
normen
9065248d1c add notification support 2020-11-24 13:58:31 +01:00
normen
6e0c150e26 fix creating file suffix for download 2020-11-24 13:03:47 +01:00
normen
489b23899e allow uploading of files as documents 2020-11-24 05:17:22 +01:00
normen
8f50aa02d6 visual fixes 2020-11-24 04:34:03 +01:00
normen
c3454e734f fix re-creating defaults 2020-11-24 04:22:34 +01:00
normen
2138e671c4 update dependecies 2020-11-24 02:47:31 +01:00
normen
0b6816f6e3 parse env variables in config 2020-11-24 02:46:28 +01:00
normen
b72a5e0cc6 add status bar, use config struct 2020-11-24 01:39:34 +01:00
normen
e4f1851b50 cleanup settings 2020-11-24 01:38:29 +01:00
normen
20f879271c fix config reset - still defaults don't come back 2020-11-24 00:31:00 +01:00
normen
d60e652d17 make default settings re-appear when deleted 2020-11-23 23:43:30 +01:00
normen
6f0d93e29e fix double default keymapping 2020-11-23 22:23:20 +01:00
normen
d7f8f0a918 allow revoking messages, allow leaving groups 2020-11-23 18:55:17 +01:00
normen
8d89a2a7f4 fix send command when called directly 2020-11-23 18:03:21 +01:00
normen
0e4a694d09 small cleanups 2020-11-23 17:18:33 +01:00
normen
754420e0c7 don't become unresponsive when login fails on startup 2020-11-23 17:15:04 +01:00
normen
ae29e13108 allow command parameters 2020-11-23 17:11:37 +01:00
normen
0e3811de44 print login errors 2020-11-23 16:48:19 +01:00
normen
d1c0e870f0 allow specifying command prefix, unify command calls 2020-11-23 16:40:44 +01:00
normen
795d8f7e63 show path again when downloading files 2020-11-23 14:43:18 +01:00
normen
5008ca46d8 fix info command, separate UI from connection 2020-11-23 14:28:11 +01:00
normen
e03e261d00 decouple UI from messaging library 2020-11-23 13:28:18 +01:00
normen
4a119a700f combine internal communication channels 2020-11-23 13:05:05 +01:00
normen
c1897b475a allow backlog to be read, improve code separation 2020-11-23 04:44:42 +01:00
normen
1672b42f7e avoid storing messages twice 2020-11-22 21:35:04 +01:00
normen
8a28ea47fd fix fallback value for contacts path 2020-11-22 17:20:07 +01:00
normen
3450fbc78f allow configuring input background and text color separately 2020-11-22 16:22:13 +01:00
normen
48ad9ce669 update links 2020-11-22 15:18:32 +01:00
normen
fd676f13cf - fix text input text color not being set
- Fixes #20
2020-11-22 15:02:38 +01:00
normen
63b5f3a604 update links 2020-11-22 14:54:53 +01:00
normen
b20031ff6a update links 2020-11-22 14:46:51 +01:00
normen
00516c3191 update links 2020-11-22 14:34:51 +01:00
Normen Hansen
76c4010ce2 Create FUNDING.yml 2020-11-22 14:03:54 +01:00
normen
0b8d265024 remove old keybindings 2020-11-22 13:21:38 +01:00
10 changed files with 1383 additions and 789 deletions

12
.github/FUNDING.yml vendored Normal file
View 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']

View File

@@ -8,10 +8,13 @@ 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
- Supports desktop notifications
- Binaries for Windows, Mac, Linux and RaspBerry Pi
### Caveats
@@ -19,10 +22,7 @@ Things that work.
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
- Only 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
- 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

View File

@@ -6,7 +6,6 @@ import (
"os/user"
"github.com/adrg/xdg"
"github.com/gdamore/tcell/v2"
"gitlab.com/tslocum/cbind"
"gopkg.in/ini.v1"
)
@@ -15,39 +14,138 @@ 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
NotificationTimeout int64
}
type Keymap struct {
SwitchPanels string
FocusMessages string
FocusInput string
FocusChats 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
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,
NotificationTimeout: 60,
},
&Keymap{
SwitchPanels: "Tab",
FocusMessages: "Ctrl+w",
FocusInput: "Ctrl+Space",
FocusChats: "Ctrl+e",
CommandBacklog: "Ctrl+b",
CommandRead: "Ctrl+n",
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",
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)
}
newCfg := ini.Empty()
if err = ini.ReflectFromWithMapper(newCfg, &Config, ini.TitleUnderscore); err == nil {
//TODO: only save if changes
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 +164,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()

9
go.mod
View File

@@ -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,11 @@ 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
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
)

35
go.sum
View File

@@ -15,11 +15,20 @@ 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.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591 h1:0WWUDZ1oxq7NxVyGo8M3KI5jbkiwNAdZFFzAdC68up4=
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
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 +47,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 +70,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 +81,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 +89,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=
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 +121,13 @@ 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 +159,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=

713
main.go
View File

@@ -8,7 +8,6 @@ import (
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/gdamore/tcell/v2"
"github.com/normen/whatscli/config"
"github.com/normen/whatscli/messages"
@@ -17,57 +16,53 @@ import (
"gitlab.com/tslocum/cbind"
)
type waMsg struct {
Wid string
Text string
}
var VERSION string = "v0.7.1"
var sendChannel chan waMsg
var textChannel chan whatsapp.TextMessage
var otherChannel chan interface{}
var contactChannel chan whatsapp.Contact
var VERSION string = "v0.9.9"
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
var textInput *tview.InputField
var topBar *tview.TextView
var infoBar *tview.TextView
//var infoBar *tview.TextView
var msgStore messages.MessageDatabase
var keysApp *cbind.Configuration
var contactRoot *tview.TreeNode
var handler textHandler
var chatRoot *tview.TreeNode
var app *tview.Application
var sessionManager *messages.SessionManager
var keyBindings *cbind.Configuration
var uiHandler messages.UiMessageHandler
func main() {
config.InitConfig()
msgStore = messages.MessageDatabase{}
msgStore.Init()
messages.LoadContacts()
uiHandler = UiHandler{}
sessionManager = &messages.SessionManager{}
sessionManager.Init(uiHandler)
app = tview.NewApplication()
sideBarWidth := config.GetIntSetting("ui", "contact_sidebar_width")
sideBarWidth := config.Config.Ui.ChatSidebarWidth
gridLayout := tview.NewGrid()
gridLayout.SetRows(1, 0, 1)
gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth)
gridLayout.SetBorders(true)
gridLayout.SetBackgroundColor(config.GetColor("background"))
gridLayout.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
gridLayout.SetBordersColor(tcell.ColorNames[config.Config.Colors.Borders])
cmdPrefix := config.Config.General.CmdPrefix
topBar = tview.NewTextView()
topBar.SetDynamicColors(true)
topBar.SetScrollable(false)
topBar.SetText("[::b] WhatsCLI " + VERSION + " [-::d]Type /help for help")
topBar.SetBackgroundColor(config.GetColor("background"))
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)
//infoBar.SetText("🔋: ??%")
infoBar = tview.NewTextView()
infoBar.SetDynamicColors(true)
textView = tview.NewTextView().
SetDynamicColors(true).
@@ -76,16 +71,15 @@ func main() {
SetChangedFunc(func() {
app.Draw()
})
textView.SetBackgroundColor(config.GetColor("background"))
textView.SetTextColor(config.GetColor("text"))
textView.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
textView.SetTextColor(tcell.ColorNames[config.Config.Colors.Text])
// TODO: add better way
messages.SetTextView(textView)
PrintHelp()
textInput = tview.NewInputField()
textInput.SetBackgroundColor(config.GetColor("background"))
textView.SetTextColor(config.GetColor("text"))
textInput.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
textInput.SetFieldBackgroundColor(tcell.ColorNames[config.Config.Colors.InputBackground])
textInput.SetFieldTextColor(tcell.ColorNames[config.Config.Colors.InputText])
textInput.SetChangedFunc(func(change string) {
sndTxt = change
})
@@ -119,67 +113,48 @@ func main() {
})
gridLayout.AddItem(topBar, 0, 0, 1, 4, 0, 0, false)
//gridLayout.AddItem(infoBar, 0, 0, 1, 1, 0, 0, false)
gridLayout.AddItem(MakeTree(), 1, 0, 2, 1, 0, 0, false)
gridLayout.AddItem(infoBar, 2, 0, 1, 1, 0, 0, false)
gridLayout.AddItem(MakeTree(), 1, 0, 1, 1, 0, 0, false)
gridLayout.AddItem(textView, 1, 1, 1, 3, 0, 0, false)
gridLayout.AddItem(textInput, 2, 1, 1, 3, 0, 0, false)
app.SetRoot(gridLayout, true)
app.EnableMouse(true)
app.SetFocus(textInput)
go func() {
if err := StartTextReceiver(); 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).
SetColor(config.GetColor("list_header"))
rootDir := "Chats"
chatRoot = tview.NewTreeNode(rootDir).
SetColor(tcell.ColorNames[config.Config.Colors.ListHeader])
treeView = tview.NewTreeView().
SetRoot(contactRoot).
SetCurrentNode(contactRoot)
treeView.SetBackgroundColor(config.GetColor("background"))
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())
}
})
treeView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyTab {
app.SetFocus(textInput)
return nil
}
if event.Key() == tcell.KeyCtrlSpace {
app.SetFocus(textInput)
return nil
}
if event.Key() == tcell.KeyCtrlW {
app.SetFocus(textView)
if curRegions != nil && len(curRegions) > 0 {
textView.Highlight(curRegions[len(curRegions)-1])
}
return nil
}
return event
})
return treeView
}
@@ -187,7 +162,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
@@ -219,14 +194,15 @@ func handleSwitchPanels(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
func handleConnect(ev *tcell.EventKey) *tcell.EventKey {
msgStore.Init()
messages.Login()
return nil
func handleCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey {
return func(ev *tcell.EventKey) *tcell.EventKey {
sessionManager.CommandChannel <- messages.Command{command, nil}
return nil
}
}
func handleQuit(ev *tcell.EventKey) *tcell.EventKey {
messages.Disconnect()
sessionManager.CommandChannel <- messages.Command{"disconnect", nil}
app.Stop()
return nil
}
@@ -236,44 +212,16 @@ func handleHelp(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
func handleDownload(ev *tcell.EventKey) *tcell.EventKey {
hls := textView.GetHighlights()
if len(hls) > 0 {
go DownloadMessageId(hls[0], false)
ResetMsgSelection()
app.SetFocus(textInput)
func handleMessageCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey {
return func(ev *tcell.EventKey) *tcell.EventKey {
hls := textView.GetHighlights()
if len(hls) > 0 {
sessionManager.CommandChannel <- messages.Command{command, []string{hls[0]}}
ResetMsgSelection()
app.SetFocus(textInput)
}
return nil
}
return nil
}
func handleOpen(ev *tcell.EventKey) *tcell.EventKey {
hls := textView.GetHighlights()
if len(hls) > 0 {
go DownloadMessageId(hls[0], true)
ResetMsgSelection()
app.SetFocus(textInput)
}
return nil
}
func handleShow(ev *tcell.EventKey) *tcell.EventKey {
hls := textView.GetHighlights()
if len(hls) > 0 {
go PrintImage(hls[0])
ResetMsgSelection()
app.SetFocus(textInput)
}
return nil
}
func handleInfo(ev *tcell.EventKey) *tcell.EventKey {
hls := textView.GetHighlights()
if len(hls) > 0 {
PrintText(msgStore.GetMessageInfo(hls[0]))
ResetMsgSelection()
app.SetFocus(textInput)
}
return nil
}
func handleMessagesUp(ev *tcell.EventKey) *tcell.EventKey {
@@ -287,7 +235,7 @@ func handleMessagesUp(ev *tcell.EventKey) *tcell.EventKey {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[len(curRegions)-1])
textView.Highlight(curRegions[len(curRegions)-1].Id)
}
textView.ScrollToHighlight()
return nil
@@ -304,17 +252,26 @@ func handleMessagesDown(ev *tcell.EventKey) *tcell.EventKey {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[0])
textView.Highlight(curRegions[0].Id)
}
textView.ScrollToHighlight()
return nil
}
func handleChatPanelUp(ev *tcell.EventKey) *tcell.EventKey {
//TODO: scroll selection in treeView? or chatRoot? How?
return ev
}
func handleChatPanelDown(ev *tcell.EventKey) *tcell.EventKey {
return ev
}
func handleMessagesLast(ev *tcell.EventKey) *tcell.EventKey {
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
}
@@ -323,7 +280,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
}
@@ -337,43 +294,56 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
// load the key map
func LoadShortcuts() {
keysApp = cbind.NewConfiguration()
if err := keysApp.Set(config.GetKey("focus_messages"), handleFocusMessage); err != nil {
keyBindings = cbind.NewConfiguration()
if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil {
PrintErrorMsg("focus_messages:", err)
}
if err := keysApp.Set(config.GetKey("focus_input"), handleFocusInput); err != nil {
if err := keyBindings.Set(config.Config.Keymap.FocusInput, handleFocusInput); err != nil {
PrintErrorMsg("focus_input:", err)
}
if err := keysApp.Set(config.GetKey("focus_contacts"), handleFocusContacts); err != nil {
if err := keyBindings.Set(config.Config.Keymap.FocusChats, handleFocusContacts); err != nil {
PrintErrorMsg("focus_contacts:", err)
}
if err := keysApp.Set(config.GetKey("switch_panels"), handleSwitchPanels); err != nil {
if err := keyBindings.Set(config.Config.Keymap.SwitchPanels, handleSwitchPanels); err != nil {
PrintErrorMsg("switch_panels:", err)
}
if err := keysApp.Set(config.GetKey("command_connect"), handleConnect); err != nil {
if err := keyBindings.Set(config.Config.Keymap.CommandRead, handleCommand("read")); err != nil {
PrintErrorMsg("command_read:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil {
PrintErrorMsg("command_backlog:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandConnect, handleCommand("login")); err != nil {
PrintErrorMsg("command_connect:", err)
}
if err := keysApp.Set(config.GetKey("command_quit"), handleQuit); err != nil {
if err := keyBindings.Set(config.Config.Keymap.CommandQuit, handleQuit); err != nil {
PrintErrorMsg("command_quit:", err)
}
if err := keysApp.Set(config.GetKey("command_help"), handleHelp); err != nil {
if err := keyBindings.Set(config.Config.Keymap.CommandHelp, handleHelp); err != nil {
PrintErrorMsg("command_help:", err)
}
app.SetInputCapture(keysApp.Capture)
app.SetInputCapture(keyBindings.Capture)
keysMessages := cbind.NewConfiguration()
if err := keysMessages.Set(config.GetKey("message_download"), handleDownload); err != nil {
if err := keysMessages.Set(config.Config.Keymap.MessageDownload, handleMessageCommand("download")); err != nil {
PrintErrorMsg("message_download:", err)
}
if err := keysMessages.Set(config.GetKey("message_open"), handleOpen); err != nil {
if err := keysMessages.Set(config.Config.Keymap.MessageOpen, handleMessageCommand("open")); err != nil {
PrintErrorMsg("message_open:", err)
}
if err := keysMessages.Set(config.GetKey("message_show"), handleShow); err != nil {
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
PrintErrorMsg("message_show:", err)
}
if err := keysMessages.Set(config.GetKey("message_info"), handleInfo); err != nil {
if err := keysMessages.Set(config.Config.Keymap.MessageUrl, handleMessageCommand("url")); err != nil {
PrintErrorMsg("message_url:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageInfo, handleMessageCommand("info")); err != nil {
PrintErrorMsg("message_info:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageRevoke, handleMessageCommand("revoke")); err != nil {
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)
@@ -382,37 +352,57 @@ func LoadShortcuts() {
keysMessages.SetRune(tcell.ModNone, 'g', handleMessagesFirst)
keysMessages.SetRune(tcell.ModNone, 'G', handleMessagesLast)
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.GetKey("switch_panels"), "[::-] = switch input/contacts")
fmt.Fprintln(textView, "[::b]", config.GetKey("focus_messages"), "[::-] = focus message panel")
fmt.Fprintln(textView, "[::b]", config.GetKey("focus_contacts"), "[::-] = focus contacts panel")
fmt.Fprintln(textView, "[::b]", config.GetKey("focus_input"), "[::-] = focus input")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Message panel focused:[-::-]")
fmt.Fprintln(textView, "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, "")
fmt.Fprintln(textView, "[-::-]Message panel[-::-]")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = select message")
fmt.Fprintln(textView, "[::b]", config.GetKey("message_download"), "[::-] = download attachment -> ", config.GetSetting("download_path"))
fmt.Fprintln(textView, "[::b]", config.GetKey("message_open"), "[::-] = download & open attachment -> ", config.GetSetting("preview_path"))
fmt.Fprintln(textView, "[::b]", config.GetKey("message_show"), "[::-] = download & show image using jp2a -> ", config.GetSetting("preview_path"))
fmt.Fprintln(textView, "[::b]", config.GetKey("message_info"), "[::-] = info about message")
fmt.Fprintln(textView, "[::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, "[-::u]Commands:[-::-]")
fmt.Fprintln(textView, "[::b] /connect[::-] = (re)connect in case the connection dropped ->[::b]", config.GetKey("command_connect"), "[::-]")
fmt.Fprintln(textView, "[::b] /help[::-] = show this help ->[::b]", config.GetKey("command_help"), "[::-]")
fmt.Fprintln(textView, "[::b] /quit[::-] = exit app ->[::b]", config.GetKey("command_quit"), "[::-]")
fmt.Fprintln(textView, "[::b] /disconnect[::-] = close the connection")
fmt.Fprintln(textView, "[::b] /logout[::-] = remove login data from computer (stays connected until app closes)")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Config file in \n-> ", config.GetConfigFilePath())
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 10 previous messages")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"read [::-]or[::b]", config.Config.Keymap.CommandRead, "[::-] = mark new messages in chat as read")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"upload[::-] /path/to/file = Upload any file as document")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendimage[::-] /path/to/file = Send image message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendvideo[::-] /path/to/file = Send video message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"sendaudio[::-] /path/to/file = Send audio message")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = Leave group")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Configuration:")
fmt.Fprintln(textView, " ->", config.GetConfigFilePath())
fmt.Fprintln(textView, "")
}
// called when text is entered by the user
// TODO: parse and map commands automatically
func EnterCommand(key tcell.Key) {
if sndTxt == "" {
return
@@ -421,83 +411,37 @@ func EnterCommand(key tcell.Key) {
textInput.SetText("")
return
}
if sndTxt == "/connect" {
//command
msgStore.Init()
messages.Login()
textInput.SetText("")
return
}
if sndTxt == "/disconnect" {
PrintError(messages.Disconnect())
textInput.SetText("")
return
}
if sndTxt == "/logout" {
//command
PrintError(messages.Logout())
textInput.SetText("")
return
}
if sndTxt == "/load" {
//command
LoadContacts()
textInput.SetText("")
return
}
if sndTxt == "/help" {
cmdPrefix := config.Config.General.CmdPrefix
if sndTxt == cmdPrefix+"help" {
//command
PrintHelp()
textInput.SetText("")
return
}
if sndTxt == "/quit" {
if sndTxt == cmdPrefix+"quit" {
//command
messages.Disconnect()
sessionManager.CommandChannel <- messages.Command{"disconnect", nil}
app.Stop()
return
}
if sndTxt == "/keys" {
//command
//config.PrintKeys(textView)
if strings.HasPrefix(sndTxt, cmdPrefix) {
cmd := strings.TrimPrefix(sndTxt, cmdPrefix)
var params []string
if strings.Index(cmd, " ") >= 0 {
cmdParts := strings.Split(cmd, " ")
cmd = cmdParts[0]
params = cmdParts[1:]
}
sessionManager.CommandChannel <- messages.Command{cmd, params}
textInput.SetText("")
return
}
if strings.Index(sndTxt, "/addname ") == 0 {
//command
parts := strings.Split(sndTxt, " ")
if len(parts) < 3 {
fmt.Fprintln(textView, "Use /addname 1234567 NewName")
return
}
contact := whatsapp.Contact{
Jid: parts[1] + messages.CONTACTSUFFIX,
Name: strings.TrimPrefix(sndTxt, "/addname "+parts[1]+" "),
}
contactChannel <- contact
textInput.SetText("")
return
// no command, send as message
msg := messages.Command{
Name: "send",
Params: []string{currentReceiver.Id, sndTxt},
}
if currentReceiver == "" {
fmt.Fprintln(textView, "[red]no contact selected[-]")
return
}
if strings.Index(sndTxt, "/name ") == 0 {
//command
contact := whatsapp.Contact{
Jid: currentReceiver,
Name: strings.TrimPrefix(sndTxt, "/name "),
}
contactChannel <- contact
textInput.SetText("")
return
}
// send message
msg := waMsg{
Wid: currentReceiver,
Text: sndTxt,
}
sendChannel <- msg
sessionManager.CommandChannel <- msg
textInput.SetText("")
}
@@ -507,17 +451,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
}
}
@@ -529,11 +473,6 @@ func ResetMsgSelection() {
textView.ScrollToEnd()
}
// prints a text message to the TextView
func PrintTextMessage(msg whatsapp.TextMessage) {
fmt.Fprintln(textView, messages.GetTextMessageString(&msg))
}
// prints text to the TextView
func PrintText(txt string) {
fmt.Fprintln(textView, txt)
@@ -544,7 +483,7 @@ func PrintError(err error) {
if err == nil {
return
}
fmt.Fprintln(textView, "[red]", err.Error(), "[-]")
fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", err.Error(), "[-]")
}
// prints an error to the TextView
@@ -552,230 +491,182 @@ func PrintErrorMsg(text string, err error) {
if err == nil {
return
}
fmt.Fprintln(textView, "[red]", text, err.Error(), "[-]")
fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", text, err.Error(), "[-]")
}
// prints an image attachment to the TextView (by message id)
func PrintImage(id string) {
func PrintImage(path string) {
var err error
var path string
PrintText("[::d]loading..[::-]")
if path, err = msgStore.DownloadMessage(id, true); err == nil {
cmd := exec.Command("jp2a", "--color", path)
var stdout io.ReadCloser
if stdout, err = cmd.StdoutPipe(); err == nil {
if err = cmd.Start(); err == nil {
reader := bufio.NewReader(stdout)
io.Copy(tview.ANSIWriter(textView), reader)
return
}
cmdParts := strings.Split(config.Config.General.ShowCommand, " ")
cmdParts = append(cmdParts, path)
var cmd *exec.Cmd
size := len(cmdParts)
if size > 1 {
cmd = exec.Command(cmdParts[0], cmdParts[1:]...)
} else if size > 0 {
cmd = exec.Command(cmdParts[0])
}
var stdout io.ReadCloser
if stdout, err = cmd.StdoutPipe(); err == nil {
if err = cmd.Start(); err == nil {
reader := bufio.NewReader(stdout)
io.Copy(tview.ANSIWriter(textView), reader)
return
}
}
PrintError(err)
}
// downloads a specific message attachment
func DownloadMessageId(id string, openIt bool) {
PrintText("[::d]loading..[::-]")
if result, err := msgStore.DownloadMessage(id, openIt); err == nil {
PrintText("[::d]Downloaded as [yellow]" + result + "[-::-]")
if openIt {
open.Run(result)
}
// updates the status bar
func UpdateStatusBar(statusInfo messages.SessionStatus) {
out := " "
if statusInfo.Connected {
out += "[" + config.Config.Colors.Positive + "]online[-]"
} else {
PrintError(err)
out += "[" + config.Config.Colors.Negative + "]offline[-]"
}
out += " "
out += "[::d] ("
out += fmt.Sprint(statusInfo.BatteryCharge)
out += "%"
if statusInfo.BatteryLoading {
out += " [" + config.Config.Colors.Positive + "]L[-]"
} else {
out += " [" + config.Config.Colors.Negative + "]l[-]"
}
if statusInfo.BatteryPowersave {
out += " [" + config.Config.Colors.Negative + "]S[-]"
} else {
out += " [" + config.Config.Colors.Positive + "]s[-]"
}
out += ")[::-] "
out += statusInfo.LastSeen
go app.QueueUpdateDraw(func() {
infoBar.SetText(out)
})
//infoBar.SetText("🔋: ??%")
}
// notifies about a new message if its recent
func NotifyMsg(msg whatsapp.TextMessage) {
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
//fmt.Print("\a")
//err := beeep.Notify(messages.GetIdName(msg.Info.RemoteJid), msg.Text, "")
//if err != nil {
// fmt.Fprintln(textView, "[red]error in notification[-]")
//}
}
}
// loads the contact data from storage to the TreeView
func LoadContacts() {
var ids = msgStore.GetContactIds()
contactRoot.ClearChildren()
for _, element := range ids {
node := tview.NewTreeNode(messages.GetIdName(element)).
SetReference(element).
SetSelectable(true)
if strings.Count(element, messages.CONTACTSUFFIX) > 0 {
node.SetColor(config.GetColor("list_contact"))
} else {
node.SetColor(config.GetColor("list_group"))
}
contactRoot.AddChild(node)
if element == currentReceiver {
treeView.SetCurrentNode(node)
}
}
}
// sets the current contact, loads text from storage to TextView
func SetDisplayedContact(wid string) {
// sets the current chat, loads text from storage to TextView
func SetDisplayedChat(wid messages.Chat) {
//TODO: how to get chat to set
currentReceiver = wid
textView.Clear()
textView.SetTitle(messages.GetIdName(wid))
msgTxt, regIds := msgStore.GetMessagesString(wid)
textView.SetText(msgTxt)
curRegions = regIds
textView.SetTitle(wid.Name)
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
}
// starts the receiver and message handling thread
func StartTextReceiver() error {
var wac = messages.GetConnection()
err := messages.LoginWithConnection(wac)
if err != nil {
return fmt.Errorf("%v\n", err)
// 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"
}
handler = textHandler{}
wac.AddHandler(handler)
sendChannel = make(chan waMsg)
textChannel = make(chan whatsapp.TextMessage)
otherChannel = make(chan interface{})
contactChannel = make(chan whatsapp.Contact)
for {
select {
case msg := <-sendChannel:
SendText(msg.Wid, msg.Text)
case rcvd := <-textChannel:
if msgStore.AddTextMessage(&rcvd) {
app.QueueUpdateDraw(LoadContacts)
}
case other := <-otherChannel:
msgStore.AddOtherMessage(&other)
case contact := <-contactChannel:
messages.SetIdName(contact.Jid, contact.Name)
app.QueueUpdateDraw(LoadContacts)
}
}
fmt.Fprintln(textView, "closing the receiver")
wac.Disconnect()
return nil
return out
}
// sends text to whatsapp id
func SendText(wid string, text string) {
msg := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: wid,
FromMe: true,
Timestamp: uint64(time.Now().Unix()),
},
Text: text,
}
_, err := messages.GetConnection().Send(msg)
if err != nil {
PrintError(err)
} else {
msgStore.AddTextMessage(&msg)
PrintTextMessage(msg)
// create a formatted string with regions based on message ID from a text message
func getTextMessageString(msg *messages.Message) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
out := ""
text := tview.Escape(msg.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
}
// handler struct for whatsapp callbacks
type textHandler struct{}
type UiHandler struct{}
// HandleError implements the error handler interface for go-whatsapp
func (t textHandler) HandleError(err error) {
PrintText("[red]go-whatsapp reported an error:[-]")
PrintError(err)
return
}
// HandleTextMessage implements the text message handler interface for go-whatsapp
func (t textHandler) HandleTextMessage(msg whatsapp.TextMessage) {
textChannel <- msg
if msg.Info.RemoteJid != currentReceiver {
NotifyMsg(msg)
return
}
PrintTextMessage(msg)
// add to regions if current window, otherwise its not selectable
id := msg.Info.Id
app.QueueUpdate(func() {
curRegions = append(curRegions, id)
func (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, msg)
PrintText(getTextMessageString(&msg))
})
}
// methods to convert messages to TextMessage
func (t textHandler) 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,
}
t.HandleTextMessage(msg)
otherChannel <- message
func (u UiHandler) NewScreen(msgs []messages.Message) {
go app.QueueUpdateDraw(func() {
textView.Clear()
screen := getMessagesString(msgs)
textView.SetText(screen)
curRegions = msgs
if screen == "" {
PrintHelp()
}
})
}
func (t textHandler) 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,
}
t.HandleTextMessage(msg)
otherChannel <- message
// loads the chat data from storage to the TreeView
func (u UiHandler) SetChats(ids []messages.Chat) {
go app.QueueUpdateDraw(func() {
chatRoot.ClearChildren()
oldId := currentReceiver.Id
for _, element := range ids {
name := element.Name
if name == "" {
name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX)
}
if element.Unread > 0 {
name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])"
//tim := time.Unix(element.LastMessage, 0)
//sin := time.Since(tim)
//since := fmt.Sprintf("%s", sin)
//time := tim.Format("02-01-06 15:04:05")
//name += since
}
node := tview.NewTreeNode(name).
SetReference(element).
SetSelectable(true)
if element.IsGroup {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup])
} else {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
}
// 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)
}
}
})
}
func (t textHandler) 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,
}
t.HandleTextMessage(msg)
otherChannel <- message
func (u UiHandler) PrintError(err error) {
PrintError(err)
}
func (t textHandler) 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]",
}
t.HandleTextMessage(msg)
otherChannel <- message
func (u UiHandler) PrintText(msg string) {
PrintText(msg)
}
// add contact info to database (not needed, internal db of connection is used)
func (t textHandler) HandleNewContact(contact whatsapp.Contact) {
// redundant, wac has contacts
//contactChannel <- contact
func (u UiHandler) PrintFile(path string) {
PrintImage(path)
}
// handle battery messages
//func (t textHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
// app.QueueUpdate(func() {
// infoBar.SetText("🔋: " + string(msg.Percentage) + "%")
// })
//}
func (u UiHandler) OpenFile(path string) {
open.Run(path)
}
func (u UiHandler) SetStatus(status messages.SessionStatus) {
UpdateStatusBar(status)
}
func (u UiHandler) GetWriter() io.Writer {
return textView
}

View File

@@ -1,99 +0,0 @@
package messages
import (
"encoding/gob"
"os"
"os/user"
"strings"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
)
var contacts map[string]string
var connection *whatsapp.Conn
// loads custom contacts from disk
func LoadContacts() {
contacts = make(map[string]string)
file, err := os.Open(config.GetContactsFilePath())
if err != nil {
// load old contacts file, re-save in new location if found
file, err = os.Open(GetHomeDir() + ".whatscli.contacts")
if err != nil {
return
} else {
os.Remove(GetHomeDir() + ".whatscli.contacts")
SaveContacts()
}
}
defer file.Close()
decoder := gob.NewDecoder(file)
err = decoder.Decode(&contacts)
if err != nil {
return
}
}
// saves custom contacts to disk
func SaveContacts() {
file, err := os.Open(config.GetContactsFilePath())
if err != nil {
return
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(contacts)
if err != nil {
return
}
return
}
// sets a new name for a whatsapp id
func SetIdName(id string, name string) {
contacts[id] = name
SaveContacts()
}
// gets a pretty name for a whatsapp id
func GetIdName(id string) string {
if _, ok := contacts[id]; ok {
return contacts[id]
}
if val, ok := connection.Store.Contacts[id]; ok {
if val.Name != "" {
return val.Name
} else if val.Short != "" {
return val.Short
} else if val.Notify != "" {
return val.Notify
}
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// gets a short name for a whatsapp id
func GetIdShort(id string) string {
if val, ok := connection.Store.Contacts[id]; ok {
if val.Short != "" {
return val.Short
} else if val.Name != "" {
return val.Name
} else if val.Notify != "" {
return val.Notify
}
}
if _, ok := contacts[id]; ok {
return contacts[id]
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// gets the OS home dir with a path separator at the end
func GetHomeDir() string {
usr, err := user.Current()
if err != nil {
}
return usr.HomeDir + string(os.PathSeparator)
}

77
messages/messages.go Normal file
View File

@@ -0,0 +1,77 @@
//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
Text string
}
// internal contact representation to abstract from message lib
type Chat struct {
Id string
IsGroup bool
Name string
Unread int
//TODO: convert to uint64
LastMessage int64
}
type Contact struct {
Id string
Name string
Short string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
const STATUSSUFFIX = "status@broadcast"

View File

@@ -2,58 +2,218 @@ package messages
import (
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"mime"
"os"
"sync"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/gdamore/tcell/v2"
"github.com/gen2brain/beeep"
"github.com/rivo/tview"
"mvdan.cc/xurls/v2"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
"github.com/normen/whatscli/qrcode"
)
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 config.Config.General.EnableNotifications && !msg.Info.FromMe {
err := beeep.Notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
} 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 config.Config.General.EnableNotifications && !msg.Info.FromMe {
err := beeep.Notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
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,590 @@ 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 := 10
if currentMsgs, ok := sm.db.textMessages[sm.currentReceiver]; ok {
if len(currentMsgs) > 0 {
firstMsg := currentMsgs[0]
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, firstMsg.Info.Id, firstMsg.Info.FromMe, false, sm)
}
} else {
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, "", false, false, sm)
}
} else {
sm.printCommandUsage("backlog", "-> only works in a chat")
}
case "login":
sm.uiHandler.PrintError(sm.login())
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 checkParam(command.Params, 1) {
groupId = command.Params[0]
}
wac := sm.getConnection()
var err error
_, err = wac.LeaveGroup(groupId)
if err == nil {
sm.uiHandler.PrintText("left group " + groupId)
}
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
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()
_, err := sm.getConnection().Send(msg)
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.PrintText("[" + config.Config.Colors.Negative + "]go-whatsapp reported an 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()

View File

@@ -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
}