Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
16
README.md
16
README.md
@@ -8,21 +8,23 @@ 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
|
||||
|
||||
@@ -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,75 +14,142 @@ 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
|
||||
CommandCopyuser 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,
|
||||
NotificationTimeout: 60,
|
||||
},
|
||||
&Keymap{
|
||||
SwitchPanels: "Tab",
|
||||
FocusMessages: "Ctrl+w",
|
||||
FocusInput: "Ctrl+Space",
|
||||
FocusChats: "Ctrl+e",
|
||||
CommandBacklog: "Ctrl+b",
|
||||
CommandRead: "Ctrl+n",
|
||||
CommandCopyuser: "Ctrl+c",
|
||||
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 {
|
||||
if _, err := section.GetKey("cmd_prefix"); err != nil {
|
||||
section.NewKey("cmd_prefix", "/")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
section.MapTo(&Config.General)
|
||||
}
|
||||
if section, err := cfg.GetSection("keymap"); err == nil {
|
||||
if _, err := section.GetKey("command_backlog"); err != nil {
|
||||
section.NewKey("command_backlog", "Ctrl+b")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
if _, err := section.GetKey("message_revoke"); err != nil {
|
||||
section.NewKey("message_revoke", "r")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
section.MapTo(&Config.Keymap)
|
||||
}
|
||||
if section, err := cfg.GetSection("ui"); err == nil {
|
||||
section.MapTo(&Config.Ui)
|
||||
}
|
||||
if section, err := cfg.GetSection("colors"); err == nil {
|
||||
if _, err := section.GetKey("borders"); err != nil {
|
||||
section.NewKey("borders", "white")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
if _, err := section.GetKey("input_background"); err != nil {
|
||||
section.NewKey("input_background", "blue")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
if _, err := section.GetKey("input_text"); err != nil {
|
||||
section.NewKey("input_text", "white")
|
||||
err = cfg.SaveTo(configFilePath)
|
||||
}
|
||||
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.Section("general").NewKey("cmd_prefix", "/")
|
||||
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_backlog", "Ctrl+b")
|
||||
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.Section("keymap").NewKey("message_revoke", "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")
|
||||
cfg.Section("colors").NewKey("borders", "white")
|
||||
cfg.Section("colors").NewKey("input_background", "blue")
|
||||
cfg.Section("colors").NewKey("input_text", "white")
|
||||
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 {
|
||||
@@ -102,59 +168,6 @@ func GetSessionFilePath() string {
|
||||
return GetHomeDir() + ".whatscli.session"
|
||||
}
|
||||
|
||||
func GetContactsFilePath() string {
|
||||
if sessionFilePath, err := xdg.ConfigFile("whatscli/contacts"); err == nil {
|
||||
return sessionFilePath
|
||||
}
|
||||
return GetHomeDir() + ".whatscli.contacts"
|
||||
}
|
||||
|
||||
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,10 @@ go 1.15
|
||||
require (
|
||||
github.com/Rhymen/go-whatsapp v0.1.1
|
||||
github.com/adrg/xdg v0.2.3
|
||||
github.com/atotto/clipboard v0.1.2
|
||||
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 +17,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
|
||||
)
|
||||
|
||||
37
go.sum
37
go.sum
@@ -10,16 +10,27 @@ github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf
|
||||
github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM=
|
||||
github.com/adrg/xdg v0.2.3 h1:GxXngdYxNDkoUvZXjNJGwqZxWXi43MKbOOlA/00qZi4=
|
||||
github.com/adrg/xdg v0.2.3/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
|
||||
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
|
||||
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/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 +49,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 +72,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 +83,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 +91,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 +123,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 +161,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=
|
||||
|
||||
374
main.go
374
main.go
@@ -6,6 +6,7 @@ import (
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/normen/whatscli/config"
|
||||
@@ -15,22 +16,25 @@ import (
|
||||
"gitlab.com/tslocum/cbind"
|
||||
)
|
||||
|
||||
var VERSION string = "v0.8.2"
|
||||
var VERSION string = "v1.0.1"
|
||||
|
||||
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 chatRoot *tview.TreeNode
|
||||
var app *tview.Application
|
||||
|
||||
//var infoBar *tview.TextView
|
||||
var sessionManager *messages.SessionManager
|
||||
|
||||
var keyBindings *cbind.Configuration
|
||||
|
||||
var contactRoot *tview.TreeNode
|
||||
var app *tview.Application
|
||||
var uiHandler messages.UiMessageHandler
|
||||
|
||||
func main() {
|
||||
@@ -38,27 +42,27 @@ func main() {
|
||||
uiHandler = UiHandler{}
|
||||
sessionManager = &messages.SessionManager{}
|
||||
sessionManager.Init(uiHandler)
|
||||
messages.LoadContacts()
|
||||
|
||||
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.SetBordersColor(config.GetColor("borders"))
|
||||
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).
|
||||
@@ -67,15 +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])
|
||||
|
||||
PrintHelp()
|
||||
|
||||
textInput = tview.NewInputField()
|
||||
textInput.SetBackgroundColor(config.GetColor("background"))
|
||||
textInput.SetFieldBackgroundColor(config.GetColor("input_background"))
|
||||
textInput.SetFieldTextColor(config.GetColor("input_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
|
||||
})
|
||||
@@ -109,44 +113,43 @@ 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 := sessionManager.StartManager(); err != nil {
|
||||
PrintError(err)
|
||||
}
|
||||
}()
|
||||
if err := sessionManager.StartManager(); err != nil {
|
||||
PrintError(err)
|
||||
}
|
||||
LoadShortcuts()
|
||||
app.Run()
|
||||
}
|
||||
|
||||
// creates the TreeView for contacts
|
||||
// creates the TreeView for chats
|
||||
func MakeTree() *tview.TreeView {
|
||||
rootDir := "Contacts"
|
||||
contactRoot = tview.NewTreeNode(rootDir).
|
||||
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())
|
||||
@@ -159,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
|
||||
@@ -198,6 +201,16 @@ func handleCommand(command string) func(ev *tcell.EventKey) *tcell.EventKey {
|
||||
}
|
||||
}
|
||||
|
||||
func handleCopyUser(ev *tcell.EventKey) *tcell.EventKey {
|
||||
if hls := textView.GetHighlights(); len(hls) > 0 {
|
||||
sessionManager.CommandChannel <- messages.Command{"copyuser", []string{hls[0]}}
|
||||
ResetMsgSelection()
|
||||
} else if currentReceiver.Id != "" {
|
||||
sessionManager.CommandChannel <- messages.Command{"copyuser", nil}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleQuit(ev *tcell.EventKey) *tcell.EventKey {
|
||||
sessionManager.CommandChannel <- messages.Command{"disconnect", nil}
|
||||
app.Stop()
|
||||
@@ -232,7 +245,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
|
||||
@@ -249,17 +262,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
|
||||
}
|
||||
@@ -268,7 +290,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
|
||||
}
|
||||
@@ -282,47 +304,60 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
|
||||
return nil
|
||||
}
|
||||
|
||||
// load the key map
|
||||
func LoadShortcuts() {
|
||||
keyBindings = cbind.NewConfiguration()
|
||||
if err := keyBindings.Set(config.GetKey("focus_messages"), handleFocusMessage); err != nil {
|
||||
if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil {
|
||||
PrintErrorMsg("focus_messages:", err)
|
||||
}
|
||||
if err := keyBindings.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 := keyBindings.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 := keyBindings.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 := keyBindings.Set(config.GetKey("command_backlog"), handleCommand("backlog")); 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.CommandCopyuser, handleCopyUser); err != nil {
|
||||
PrintErrorMsg("command_copyuser:", err)
|
||||
}
|
||||
if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil {
|
||||
PrintErrorMsg("command_backlog:", err)
|
||||
}
|
||||
if err := keyBindings.Set(config.GetKey("command_connect"), handleCommand("login")); err != nil {
|
||||
if err := keyBindings.Set(config.Config.Keymap.CommandConnect, handleCommand("login")); err != nil {
|
||||
PrintErrorMsg("command_connect:", err)
|
||||
}
|
||||
if err := keyBindings.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 := keyBindings.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(keyBindings.Capture)
|
||||
keysMessages := cbind.NewConfiguration()
|
||||
if err := keysMessages.Set(config.GetKey("message_download"), handleMessageCommand("download")); 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"), handleMessageCommand("open")); 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"), handleMessageCommand("show")); err != nil {
|
||||
if err := keysMessages.Set(config.Config.Keymap.CommandCopyuser, handleCopyUser); err != nil {
|
||||
PrintErrorMsg("command_copyuser:", err)
|
||||
}
|
||||
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
|
||||
PrintErrorMsg("message_show:", err)
|
||||
}
|
||||
if err := keysMessages.Set(config.GetKey("message_info"), handleMessageCommand("info")); 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.GetKey("message_revoke"), handleMessageCommand("revoke")); err != nil {
|
||||
if err := keysMessages.Set(config.Config.Keymap.MessageRevoke, handleMessageCommand("revoke")); err != nil {
|
||||
PrintErrorMsg("message_revoke:", err)
|
||||
}
|
||||
keysMessages.SetKey(tcell.ModNone, tcell.KeyEscape, handleExitMessages)
|
||||
@@ -333,38 +368,62 @@ 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.GetSetting("cmd_prefix")
|
||||
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_revoke"), "[::-] = revoke message")
|
||||
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] "+cmdPrefix+"backlog [::-]or[::b]", config.GetKey("command_backlog"), "[::-] = load next 10 older messages for current chat")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.GetKey("command_connect"), "[::-] = (re)connect in case the connection dropped")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"help [::-]or[::b]", config.GetKey("command_help"), "[::-] = show this help")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.GetKey("command_quit"), "[::-] = exit app")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = leave group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"disconnect[::-] = close the connection")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"logout[::-] = remove login data from computer (stays connected until app closes)")
|
||||
fmt.Fprintln(textView, "")
|
||||
fmt.Fprintln(textView, "Config file in \n-> ", config.GetConfigFilePath())
|
||||
fmt.Fprintln(textView, "[-::-]Global[-::-]")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.Config.Keymap.CommandConnect, "[::-] = (Re)Connect to server")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"disconnect[::-] = Close the connection")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"logout[::-] = Remove login data from computer")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.Config.Keymap.CommandQuit, "[::-] = Exit app")
|
||||
fmt.Fprintln(textView, "")
|
||||
fmt.Fprintln(textView, "[-::-]Chat[-::-]")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next 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, "")
|
||||
fmt.Fprintln(textView, "[-::-]Groups[-::-]")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = Leave group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"create[::-] [user-id[] [user-id[] Group Subject = Create group with users")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"subject[::-] New Subject = Change subject of group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"add[::-] [user-id[] = Add user to group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"remove[::-] [user-id[] = Remove user from group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"admin[::-] [user-id[] = Set admin role for user in group")
|
||||
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"removeadmin[::-] [user-id[] = Remove admin role for user in group")
|
||||
fmt.Fprintln(textView, "Use[::b]", config.Config.Keymap.CommandCopyuser, "[::-]to copy a selected user id")
|
||||
fmt.Fprintln(textView, "")
|
||||
fmt.Fprintln(textView, "Configuration:")
|
||||
fmt.Fprintln(textView, " ->", config.GetConfigFilePath())
|
||||
fmt.Fprintln(textView, "")
|
||||
}
|
||||
|
||||
// called when text is entered by the user
|
||||
@@ -377,7 +436,7 @@ func EnterCommand(key tcell.Key) {
|
||||
textInput.SetText("")
|
||||
return
|
||||
}
|
||||
cmdPrefix := config.GetSetting("cmd_prefix")
|
||||
cmdPrefix := config.Config.General.CmdPrefix
|
||||
if sndTxt == cmdPrefix+"help" {
|
||||
//command
|
||||
PrintHelp()
|
||||
@@ -405,7 +464,7 @@ func EnterCommand(key tcell.Key) {
|
||||
// no command, send as message
|
||||
msg := messages.Command{
|
||||
Name: "send",
|
||||
Params: []string{currentReceiver, sndTxt},
|
||||
Params: []string{currentReceiver.Id, sndTxt},
|
||||
}
|
||||
sessionManager.CommandChannel <- msg
|
||||
textInput.SetText("")
|
||||
@@ -417,17 +476,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +508,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
|
||||
@@ -457,13 +516,21 @@ func PrintErrorMsg(text string, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(textView, "[red]", text, err.Error(), "[-]")
|
||||
fmt.Fprintln(textView, "["+config.Config.Colors.Negative+"]", text, err.Error(), "[-]")
|
||||
}
|
||||
|
||||
// prints an image attachment to the TextView (by message id)
|
||||
func PrintImage(path string) {
|
||||
var err error
|
||||
cmd := exec.Command("jp2a", "--color", path)
|
||||
cmdParts := strings.Split(config.Config.General.ShowCommand, " ")
|
||||
cmdParts = append(cmdParts, path)
|
||||
var cmd *exec.Cmd
|
||||
size := len(cmdParts)
|
||||
if size > 1 {
|
||||
cmd = exec.Command(cmdParts[0], cmdParts[1:]...)
|
||||
} else if size > 0 {
|
||||
cmd = exec.Command(cmdParts[0])
|
||||
}
|
||||
var stdout io.ReadCloser
|
||||
if stdout, err = cmd.StdoutPipe(); err == nil {
|
||||
if err = cmd.Start(); err == nil {
|
||||
@@ -475,59 +542,138 @@ func PrintImage(path string) {
|
||||
PrintError(err)
|
||||
}
|
||||
|
||||
// notifies about a new message if its recent
|
||||
//func NotifyMsg(msg whatsapp.TextMessage) {
|
||||
//if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
//fmt.Print("\a")
|
||||
//err := beeep.Notify(messages.GetIdName(msg.Info.RemoteJid), msg.Text, "")
|
||||
//if err != nil {
|
||||
// fmt.Fprintln(textView, "[red]error in notification[-]")
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
// updates the status bar
|
||||
func UpdateStatusBar(statusInfo messages.SessionStatus) {
|
||||
out := " "
|
||||
if statusInfo.Connected {
|
||||
out += "[" + config.Config.Colors.Positive + "]online[-]"
|
||||
} else {
|
||||
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("🔋: ??%")
|
||||
}
|
||||
|
||||
// sets the current contact, loads text from storage to TextView
|
||||
func SetDisplayedContact(wid string) {
|
||||
// sets the current chat, loads text from storage to TextView
|
||||
func SetDisplayedChat(wid messages.Chat) {
|
||||
//TODO: how to get chat to set
|
||||
currentReceiver = wid
|
||||
textView.Clear()
|
||||
textView.SetTitle(messages.GetIdName(wid))
|
||||
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver}}
|
||||
textView.SetTitle(wid.Name)
|
||||
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
|
||||
}
|
||||
|
||||
// get a string representation of all messages for chat
|
||||
func getMessagesString(msgs []messages.Message) string {
|
||||
out := ""
|
||||
for _, msg := range msgs {
|
||||
out += getTextMessageString(&msg)
|
||||
out += "\n"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// create a formatted string with regions based on message ID from a text message
|
||||
//TODO: optimize, use Sprintf etc
|
||||
func getTextMessageString(msg *messages.Message) string {
|
||||
colorMe := config.Config.Colors.ChatMe
|
||||
colorContact := config.Config.Colors.ChatContact
|
||||
out := ""
|
||||
text := tview.Escape(msg.Text)
|
||||
if msg.Forwarded {
|
||||
text = "[" + config.Config.Colors.ForwardedText + "]" + text + "[-]"
|
||||
}
|
||||
tim := time.Unix(int64(msg.Timestamp), 0)
|
||||
time := tim.Format("02-01-06 15:04:05")
|
||||
out += "[\""
|
||||
out += msg.Id
|
||||
out += "\"]"
|
||||
if msg.FromMe { //msg from me
|
||||
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
|
||||
} else { // message from others
|
||||
out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.ContactShort + ": [-::-]" + text
|
||||
}
|
||||
out += "[\"\"]"
|
||||
return out
|
||||
}
|
||||
|
||||
type UiHandler struct{}
|
||||
|
||||
func (u UiHandler) NewMessage(msg string, id string) {
|
||||
func (u UiHandler) NewMessage(msg messages.Message) {
|
||||
//TODO: its stupid to "go" this as its supposed to run
|
||||
//on the ui thread anyway. But QueueUpdate blocks...?
|
||||
go app.QueueUpdateDraw(func() {
|
||||
curRegions = append(curRegions, id)
|
||||
PrintText(msg)
|
||||
curRegions = append(curRegions, msg)
|
||||
PrintText(getTextMessageString(&msg))
|
||||
})
|
||||
}
|
||||
|
||||
func (u UiHandler) NewScreen(screen string, ids []string) {
|
||||
func (u UiHandler) NewScreen(msgs []messages.Message) {
|
||||
go app.QueueUpdateDraw(func() {
|
||||
textView.Clear()
|
||||
screen := getMessagesString(msgs)
|
||||
textView.SetText(screen)
|
||||
curRegions = ids
|
||||
curRegions = msgs
|
||||
if screen == "" {
|
||||
if currentReceiver.Id == "" {
|
||||
PrintHelp()
|
||||
} else {
|
||||
PrintText("[::d] ~~~ no messages, press " + config.Config.Keymap.CommandBacklog + " to load backlog if available ~~~[::-]")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// loads the contact data from storage to the TreeView
|
||||
func (u UiHandler) SetContacts(ids []string) {
|
||||
// loads the chat data from storage to the TreeView
|
||||
func (u UiHandler) SetChats(ids []messages.Chat) {
|
||||
go app.QueueUpdateDraw(func() {
|
||||
contactRoot.ClearChildren()
|
||||
chatRoot.ClearChildren()
|
||||
oldId := currentReceiver.Id
|
||||
for _, element := range ids {
|
||||
node := tview.NewTreeNode(messages.GetIdName(element)).
|
||||
name := element.Name
|
||||
if name == "" {
|
||||
name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX)
|
||||
}
|
||||
if element.Unread > 0 {
|
||||
name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])"
|
||||
//tim := time.Unix(element.LastMessage, 0)
|
||||
//sin := time.Since(tim)
|
||||
//since := fmt.Sprintf("%s", sin)
|
||||
//time := tim.Format("02-01-06 15:04:05")
|
||||
//name += since
|
||||
}
|
||||
node := tview.NewTreeNode(name).
|
||||
SetReference(element).
|
||||
SetSelectable(true)
|
||||
if strings.Count(element, messages.CONTACTSUFFIX) > 0 {
|
||||
node.SetColor(config.GetColor("list_contact"))
|
||||
if element.IsGroup {
|
||||
node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup])
|
||||
} else {
|
||||
node.SetColor(config.GetColor("list_group"))
|
||||
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
|
||||
}
|
||||
contactRoot.AddChild(node)
|
||||
if element == currentReceiver {
|
||||
// store new currentReceiver, else the selection on the left goes off
|
||||
if element.Id == oldId {
|
||||
currentReceiver = element
|
||||
}
|
||||
chatRoot.AddChild(node)
|
||||
if element.Id == currentReceiver.Id {
|
||||
treeView.SetCurrentNode(node)
|
||||
}
|
||||
}
|
||||
@@ -550,6 +696,10 @@ 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
|
||||
}
|
||||
|
||||
@@ -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,86 +2,170 @@ package messages
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rivo/tview"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/atotto/clipboard"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/gen2brain/beeep"
|
||||
"github.com/normen/whatscli/config"
|
||||
"github.com/normen/whatscli/qrcode"
|
||||
"github.com/rivo/tview"
|
||||
"mvdan.cc/xurls/v2"
|
||||
)
|
||||
|
||||
// TODO: move message styling and ordering into UI, don't use strings
|
||||
// move these funcs/interface to channels
|
||||
type UiMessageHandler interface {
|
||||
NewMessage(string, string)
|
||||
NewScreen(string, []string)
|
||||
SetContacts([]string)
|
||||
PrintError(error)
|
||||
PrintText(string)
|
||||
PrintFile(string)
|
||||
OpenFile(string)
|
||||
GetWriter() io.Writer
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
Params []string
|
||||
}
|
||||
|
||||
const GROUPSUFFIX = "@g.us"
|
||||
const CONTACTSUFFIX = "@s.whatsapp.net"
|
||||
|
||||
// SessionManager deals with the connection and receives commands from the UI
|
||||
// it updates the UI accordingly
|
||||
type SessionManager struct {
|
||||
db MessageDatabase
|
||||
currentReceiver string // currently selected contact for message handling
|
||||
db *MessageDatabase
|
||||
currentReceiver string // currently selected chat for message handling
|
||||
uiHandler UiMessageHandler
|
||||
connection *whatsapp.Conn
|
||||
BatteryChannel chan BatteryMsg
|
||||
StatusChannel chan StatusMsg
|
||||
CommandChannel chan Command
|
||||
ChatChannel chan whatsapp.Chat
|
||||
ContactChannel chan whatsapp.Contact
|
||||
TextChannel chan whatsapp.TextMessage
|
||||
OtherChannel chan interface{}
|
||||
statusInfo SessionStatus
|
||||
lastSent time.Time
|
||||
started bool
|
||||
}
|
||||
|
||||
// initialize the SessionManager
|
||||
func (sm *SessionManager) Init(handler UiMessageHandler) {
|
||||
sm.db = MessageDatabase{}
|
||||
sm.db = &MessageDatabase{}
|
||||
sm.db.Init()
|
||||
sm.uiHandler = handler
|
||||
sm.BatteryChannel = make(chan BatteryMsg, 10)
|
||||
sm.StatusChannel = make(chan StatusMsg, 10)
|
||||
sm.CommandChannel = make(chan Command, 10)
|
||||
sm.ChatChannel = make(chan whatsapp.Chat, 10)
|
||||
sm.ContactChannel = make(chan whatsapp.Contact, 10)
|
||||
sm.TextChannel = make(chan whatsapp.TextMessage, 10)
|
||||
sm.OtherChannel = make(chan interface{}, 10)
|
||||
}
|
||||
|
||||
// starts the receiver and message handling thread
|
||||
// TODO: can't be stopped, can only be called once!
|
||||
// starts the receiver and message handling go routine
|
||||
func (sm *SessionManager) StartManager() error {
|
||||
if sm.started {
|
||||
return errors.New("session manager running, send commands to control")
|
||||
}
|
||||
sm.started = true
|
||||
go sm.runManager()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *SessionManager) runManager() error {
|
||||
var wac = sm.getConnection()
|
||||
err := sm.loginWithConnection(wac)
|
||||
if err != nil {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
wac.AddHandler(sm)
|
||||
for {
|
||||
for sm.started == true {
|
||||
select {
|
||||
case msg := <-sm.TextChannel:
|
||||
didNew := sm.db.AddTextMessage(&msg)
|
||||
if msg.Info.RemoteJid == sm.currentReceiver {
|
||||
if didNew {
|
||||
sm.uiHandler.NewMessage(getTextMessageString(&msg), msg.Info.Id)
|
||||
sm.uiHandler.NewMessage(sm.createMessage(&msg))
|
||||
} else {
|
||||
screen, ids := sm.db.GetMessagesString(sm.currentReceiver)
|
||||
sm.uiHandler.NewScreen(screen, ids)
|
||||
screen := sm.getMessages(sm.currentReceiver)
|
||||
sm.uiHandler.NewScreen(screen)
|
||||
}
|
||||
// notify if chat is in focus and we didn't send a message recently
|
||||
// TODO: move notify to UI
|
||||
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
|
||||
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
|
||||
sm.db.NewUnreadChat(msg.Info.RemoteJid)
|
||||
if 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.SetContacts(sm.db.GetContactIds())
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
case other := <-sm.OtherChannel:
|
||||
sm.db.AddOtherMessage(&other)
|
||||
case c := <-sm.ContactChannel:
|
||||
contact := Contact{
|
||||
c.Jid,
|
||||
c.Name,
|
||||
c.Short,
|
||||
}
|
||||
if contact.Name == "" && c.Notify != "" {
|
||||
contact.Name = c.Notify
|
||||
}
|
||||
if contact.Short == "" && c.Notify != "" {
|
||||
contact.Short = c.Notify
|
||||
}
|
||||
sm.db.AddContact(contact)
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
case c := <-sm.ChatChannel:
|
||||
if c.IsMarkedSpam == "false" {
|
||||
isGroup := strings.Contains(c.Jid, GROUPSUFFIX)
|
||||
unread, _ := strconv.ParseInt(c.Unread, 10, 0)
|
||||
last, _ := strconv.ParseInt(c.LastMessageTime, 10, 64)
|
||||
chat := Chat{
|
||||
c.Jid,
|
||||
isGroup,
|
||||
c.Name,
|
||||
int(unread),
|
||||
last,
|
||||
}
|
||||
sm.db.AddChat(chat)
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
}
|
||||
case command := <-sm.CommandChannel:
|
||||
sm.execCommand(command)
|
||||
case batteryMsg := <-sm.BatteryChannel:
|
||||
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")
|
||||
@@ -89,25 +173,31 @@ func (sm *SessionManager) StartManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// set the currently selected chat
|
||||
func (sm *SessionManager) setCurrentReceiver(id string) {
|
||||
sm.currentReceiver = id
|
||||
screen, ids := sm.db.GetMessagesString(id)
|
||||
sm.uiHandler.NewScreen(screen, ids)
|
||||
screen := sm.getMessages(id)
|
||||
sm.uiHandler.NewScreen(screen)
|
||||
}
|
||||
|
||||
// gets an existing connection or creates one
|
||||
func (sm *SessionManager) getConnection() *whatsapp.Conn {
|
||||
var wac *whatsapp.Conn
|
||||
if connection == nil {
|
||||
wacc, err := whatsapp.NewConn(5 * time.Second)
|
||||
if sm.connection == nil {
|
||||
options := &whatsapp.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
LongClientName: "WhatsCLI Client",
|
||||
ShortClientName: "whatscli",
|
||||
}
|
||||
wacc, err := whatsapp.NewConnWithOptions(options)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
wac = wacc
|
||||
connection = wac
|
||||
sm.connection = wac
|
||||
//wac.SetClientVersion(2, 2021, 4)
|
||||
} else {
|
||||
wac = connection
|
||||
wac = sm.connection
|
||||
}
|
||||
return wac
|
||||
}
|
||||
@@ -121,8 +211,10 @@ func (sm *SessionManager) login() error {
|
||||
// loginWithConnection logs in the user using a provided connection. It ries to see if a session already exists. If not, tries to create a
|
||||
// new one using qr scanned on the terminal.
|
||||
func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
|
||||
sm.uiHandler.PrintText("connecting..")
|
||||
if wac != nil && wac.GetConnected() {
|
||||
wac.Disconnect()
|
||||
sm.StatusChannel <- StatusMsg{false, nil}
|
||||
}
|
||||
//load saved session
|
||||
session, err := readSession()
|
||||
@@ -151,43 +243,55 @@ func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving session: %v\n", err)
|
||||
}
|
||||
//<-time.After(3 * time.Second)
|
||||
//get initial battery state
|
||||
sm.BatteryChannel <- BatteryMsg{
|
||||
wac.Info.Battery,
|
||||
wac.Info.Plugged,
|
||||
false,
|
||||
}
|
||||
sm.StatusChannel <- StatusMsg{true, nil}
|
||||
return nil
|
||||
}
|
||||
|
||||
// disconnects the session
|
||||
func (sm *SessionManager) disconnect() error {
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
if wac != nil && wac.GetConnected() {
|
||||
_, err := wac.Disconnect()
|
||||
return err
|
||||
_, err = wac.Disconnect()
|
||||
}
|
||||
return nil
|
||||
sm.StatusChannel <- StatusMsg{false, err}
|
||||
return err
|
||||
}
|
||||
|
||||
// logout logs out the user.
|
||||
// logout logs out the user, deletes session file
|
||||
func (ub *SessionManager) logout() error {
|
||||
err := ub.getConnection().Logout()
|
||||
ub.StatusChannel <- StatusMsg{false, err}
|
||||
ub.uiHandler.PrintText("removing login data..")
|
||||
return removeSession()
|
||||
}
|
||||
|
||||
// executes a command
|
||||
func (sm *SessionManager) execCommand(command Command) {
|
||||
cmd := command.Name
|
||||
switch cmd {
|
||||
default:
|
||||
sm.uiHandler.PrintText("[red]Unknown command: [-]" + cmd)
|
||||
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Unknown command: [-]" + cmd)
|
||||
case "backlog":
|
||||
//command
|
||||
if sm.currentReceiver == "" {
|
||||
return
|
||||
}
|
||||
count := 10
|
||||
if currentMsgs, ok := sm.db.textMessages[sm.currentReceiver]; ok {
|
||||
if len(currentMsgs) > 0 {
|
||||
firstMsg := currentMsgs[0]
|
||||
go sm.getConnection().LoadChatMessages(sm.currentReceiver, count, firstMsg.Info.Id, firstMsg.Info.FromMe, false, sm)
|
||||
if sm.currentReceiver != "" {
|
||||
count := 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")
|
||||
}
|
||||
//FullChatHistory(currentReceiver, 20, 100000, handler)
|
||||
//messages.GetConnection().LoadFullChatHistory(currentReceiver, 20, 100000, handler)
|
||||
case "login":
|
||||
sm.uiHandler.PrintError(sm.login())
|
||||
case "connect":
|
||||
@@ -202,19 +306,59 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
text := strings.Join(textParams, " ")
|
||||
sm.sendText(command.Params[0], text)
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] send [user-id[] [message text[]")
|
||||
sm.printCommandUsage("send", "[chat-id[] [message text[]")
|
||||
}
|
||||
case "select":
|
||||
if checkParam(command.Params, 1) {
|
||||
sm.setCurrentReceiver(command.Params[0])
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] select [user-id[]")
|
||||
sm.printCommandUsage("select", "[chat-id[]")
|
||||
}
|
||||
case "read":
|
||||
if sm.currentReceiver != "" {
|
||||
// need to send message id, so get all (unread count)
|
||||
// recent messages and send "read"
|
||||
if chat, ok := sm.db.chats[sm.currentReceiver]; ok {
|
||||
count := chat.Unread
|
||||
msgs := sm.db.GetMessages(chat.Id)
|
||||
length := len(msgs)
|
||||
for idx, msg := range msgs {
|
||||
if idx >= length-count {
|
||||
sm.getConnection().Read(chat.Id, msg.Info.Id)
|
||||
}
|
||||
}
|
||||
chat.Unread = 0
|
||||
sm.db.chats[sm.currentReceiver] = chat
|
||||
sm.uiHandler.SetChats(sm.db.GetChatIds())
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("read", "-> only works in a chat")
|
||||
}
|
||||
case "info":
|
||||
if checkParam(command.Params, 1) {
|
||||
sm.uiHandler.PrintText(sm.db.GetMessageInfo(command.Params[0]))
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] info [message-id[]")
|
||||
sm.printCommandUsage("info", "[message-id[]")
|
||||
}
|
||||
case "copyuser":
|
||||
if checkParam(command.Params, 1) {
|
||||
if msg, ok := sm.db.messagesById[command.Params[0]]; ok {
|
||||
if msg.Info.SenderJid != "" {
|
||||
clipboard.WriteAll(msg.Info.SenderJid)
|
||||
sm.uiHandler.PrintText("copied " + sm.db.GetIdShort(msg.Info.SenderJid))
|
||||
} else {
|
||||
clipboard.WriteAll(msg.Info.RemoteJid)
|
||||
sm.uiHandler.PrintText("copied " + sm.db.GetIdShort(msg.Info.RemoteJid))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("copyuser", "[message-id[]")
|
||||
sm.printCommandUsage("copyuser", "-> in chat")
|
||||
} else {
|
||||
clipboard.WriteAll(sm.currentReceiver)
|
||||
sm.uiHandler.PrintText("copied " + sm.db.GetIdShort(sm.currentReceiver))
|
||||
}
|
||||
}
|
||||
case "download":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -224,7 +368,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintText("[::d] -> " + path + "[::-]")
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] download [message-id[]")
|
||||
sm.printCommandUsage("download", "[message-id[]")
|
||||
}
|
||||
case "open":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -234,7 +378,7 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] open [message-id[]")
|
||||
sm.printCommandUsage("open", "[message-id[]")
|
||||
}
|
||||
case "show":
|
||||
if checkParam(command.Params, 1) {
|
||||
@@ -244,8 +388,132 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
sm.uiHandler.PrintError(err)
|
||||
}
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] show [message-id[]")
|
||||
sm.printCommandUsage("show", "[message-id[]")
|
||||
}
|
||||
case "url":
|
||||
if checkParam(command.Params, 1) {
|
||||
if msg, ok := sm.db.messagesById[command.Params[0]]; ok {
|
||||
urlParser := xurls.Relaxed()
|
||||
url := urlParser.FindString(msg.Text)
|
||||
if url != "" {
|
||||
sm.uiHandler.OpenFile(url)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("url", "[message-id[]")
|
||||
}
|
||||
case "upload":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("upload", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.DocumentMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
FileName: filepath.Base(file.Name()),
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("upload", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendimage":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendimage", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.ImageMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendimage", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendvideo":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendvideo", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.VideoMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendvideo", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "sendaudio":
|
||||
if sm.currentReceiver == "" {
|
||||
sm.printCommandUsage("sendaudio", "-> only works in a chat")
|
||||
return
|
||||
}
|
||||
var err error
|
||||
var mime *mimetype.MIME
|
||||
var file *os.File
|
||||
if checkParam(command.Params, 1) {
|
||||
path := strings.Join(command.Params, " ")
|
||||
if mime, err = mimetype.DetectFile(path); err == nil {
|
||||
if file, err = os.Open(path); err == nil {
|
||||
msg := whatsapp.AudioMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: sm.currentReceiver,
|
||||
},
|
||||
Type: mime.String(),
|
||||
Content: file,
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
sm.lastSent = time.Now()
|
||||
_, err = wac.Send(msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sm.printCommandUsage("sendaudio", "/path/to/file")
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
case "revoke":
|
||||
if checkParam(command.Params, 1) {
|
||||
wac := sm.getConnection()
|
||||
@@ -273,23 +541,163 @@ func (sm *SessionManager) execCommand(command Command) {
|
||||
}
|
||||
sm.uiHandler.PrintError(err)
|
||||
} else {
|
||||
sm.uiHandler.PrintText("[red]Usage:[-] revoke [message-id[]")
|
||||
sm.printCommandUsage("revoke", "[message-id[]")
|
||||
}
|
||||
case "leave":
|
||||
groupId := sm.currentReceiver
|
||||
if checkParam(command.Params, 1) {
|
||||
groupId = command.Params[0]
|
||||
if strings.Index(groupId, GROUPSUFFIX) < 0 {
|
||||
sm.uiHandler.PrintText("not a group")
|
||||
return
|
||||
}
|
||||
wac := sm.getConnection()
|
||||
var err error
|
||||
_, err = wac.LeaveGroup(groupId)
|
||||
if err == nil {
|
||||
sm.uiHandler.PrintText("left group")
|
||||
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
|
||||
@@ -297,6 +705,42 @@ func checkParam(arr []string, length int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// get all messages for one chat id
|
||||
func (sm *SessionManager) getMessages(wid string) []Message {
|
||||
msgs := sm.db.GetMessages(wid)
|
||||
ids := []Message{}
|
||||
for _, msg := range msgs {
|
||||
ids = append(ids, sm.createMessage(&msg))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
// create internal message from whatsapp message
|
||||
// TODO: store these instead of generating each time
|
||||
func (sm *SessionManager) createMessage(msg *whatsapp.TextMessage) Message {
|
||||
newMsg := Message{}
|
||||
newMsg.Id = msg.Info.Id
|
||||
newMsg.ChatId = msg.Info.RemoteJid
|
||||
newMsg.FromMe = msg.Info.FromMe
|
||||
newMsg.Timestamp = msg.Info.Timestamp
|
||||
newMsg.Text = msg.Text
|
||||
newMsg.Forwarded = msg.ContextInfo.IsForwarded
|
||||
if strings.Contains(msg.Info.RemoteJid, STATUSSUFFIX) {
|
||||
newMsg.ContactId = msg.Info.SenderJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
|
||||
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) {
|
||||
newMsg.ContactId = msg.Info.SenderJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
|
||||
} else {
|
||||
newMsg.ContactId = msg.Info.RemoteJid
|
||||
newMsg.ContactName = sm.db.GetIdName(msg.Info.RemoteJid)
|
||||
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.RemoteJid)
|
||||
}
|
||||
return newMsg
|
||||
}
|
||||
|
||||
// load data for message specified by message id TODO: support types
|
||||
func (sm *SessionManager) loadMessageData(wid string) ([]byte, error) {
|
||||
if msg, ok := sm.db.otherMessages[wid]; ok {
|
||||
@@ -320,15 +764,18 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
if msg, ok := sm.db.otherMessages[wid]; ok {
|
||||
var fileName string = ""
|
||||
if preview {
|
||||
fileName += config.GetSetting("download_path")
|
||||
fileName += config.Config.General.PreviewPath
|
||||
} else {
|
||||
fileName += config.GetSetting("preview_path")
|
||||
fileName += config.Config.General.DownloadPath
|
||||
}
|
||||
fileName += string(os.PathSeparator)
|
||||
switch v := (*msg).(type) {
|
||||
default:
|
||||
case whatsapp.ImageMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "image/")
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
@@ -339,7 +786,10 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
}
|
||||
}
|
||||
case whatsapp.DocumentMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(strings.TrimPrefix(v.Type, "application/"), "document/")
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
@@ -350,7 +800,10 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
}
|
||||
}
|
||||
case whatsapp.AudioMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "audio/")
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
@@ -361,7 +814,10 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
}
|
||||
}
|
||||
case whatsapp.VideoMessage:
|
||||
fileName += v.Info.Id + "." + strings.TrimPrefix(v.Type, "video/")
|
||||
fileName += v.Info.Id
|
||||
if exts, err := mime.ExtensionsByType(v.Type); err == nil {
|
||||
fileName += exts[0]
|
||||
}
|
||||
if _, err := os.Stat(fileName); err == nil {
|
||||
return fileName, err
|
||||
} else if os.IsNotExist(err) {
|
||||
@@ -377,7 +833,7 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
|
||||
}
|
||||
|
||||
// sends text to whatsapp id
|
||||
func (sm SessionManager) sendText(wid string, text string) {
|
||||
func (sm *SessionManager) sendText(wid string, text string) {
|
||||
msg := whatsapp.TextMessage{
|
||||
Info: whatsapp.MessageInfo{
|
||||
RemoteJid: wid,
|
||||
@@ -387,13 +843,14 @@ func (sm SessionManager) sendText(wid string, text string) {
|
||||
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(getTextMessageString(&msg), msg.Info.Id)
|
||||
sm.uiHandler.NewMessage(sm.createMessage(&msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,8 +859,9 @@ func (sm SessionManager) sendText(wid string, text string) {
|
||||
|
||||
// HandleError implements the error handler interface for go-whatsapp
|
||||
func (sm *SessionManager) HandleError(err error) {
|
||||
sm.uiHandler.PrintText("[red]go-whatsapp reported an error:[-]")
|
||||
sm.uiHandler.PrintError(err)
|
||||
statusMsg := StatusMsg{false, err}
|
||||
sm.StatusChannel <- statusMsg
|
||||
return
|
||||
}
|
||||
|
||||
@@ -476,15 +934,29 @@ func (sm *SessionManager) HandleAudioMessage(message whatsapp.AudioMessage) {
|
||||
// add contact info to database (not needed, internal db of connection is used)
|
||||
func (sm *SessionManager) HandleNewContact(contact whatsapp.Contact) {
|
||||
// redundant, wac has contacts
|
||||
//contactChannel <- contact
|
||||
sm.ContactChannel <- contact
|
||||
}
|
||||
|
||||
// handle battery messages
|
||||
//func (t textHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
|
||||
// app.QueueUpdate(func() {
|
||||
// infoBar.SetText("🔋: " + string(msg.Percentage) + "%")
|
||||
// })
|
||||
//}
|
||||
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) {
|
||||
@@ -498,11 +970,11 @@ func readSession() (whatsapp.Session, error) {
|
||||
file, err := os.Open(config.GetSessionFilePath())
|
||||
if err != nil {
|
||||
// load old session file, delete if found
|
||||
file, err = os.Open(GetHomeDir() + ".whatscli.session")
|
||||
file, err = os.Open(config.GetHomeDir() + ".whatscli.session")
|
||||
if err != nil {
|
||||
return session, err
|
||||
} else {
|
||||
os.Remove(GetHomeDir() + ".whatscli.session")
|
||||
os.Remove(config.GetHomeDir() + ".whatscli.session")
|
||||
}
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
@@ -3,11 +3,8 @@ package messages
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rhymen/go-whatsapp"
|
||||
"github.com/normen/whatscli/config"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
type MessageDatabase struct {
|
||||
@@ -15,6 +12,8 @@ type MessageDatabase struct {
|
||||
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
|
||||
latestMessage map[string]uint64 // last message from RemoteJid
|
||||
otherMessages map[string]*interface{} // other non-text messages, stored by ID
|
||||
contacts map[string]Contact
|
||||
chats map[string]Chat
|
||||
}
|
||||
|
||||
// initialize the database
|
||||
@@ -24,6 +23,8 @@ func (db *MessageDatabase) Init() {
|
||||
db.messagesById = make(map[string]*whatsapp.TextMessage)
|
||||
db.otherMessages = make(map[string]*interface{})
|
||||
db.latestMessage = make(map[string]uint64)
|
||||
db.contacts = make(map[string]Contact)
|
||||
db.chats = make(map[string]Chat)
|
||||
}
|
||||
|
||||
// add a text message to the database, stored by RemoteJid
|
||||
@@ -40,6 +41,18 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
db.latestMessage[wid] = msg.Info.Timestamp
|
||||
didNew = true
|
||||
}
|
||||
//do we know this chat? if not add
|
||||
if _, ok := db.chats[msg.Info.RemoteJid]; !ok {
|
||||
//don't have this chat!
|
||||
isGroup := strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX)
|
||||
db.chats[msg.Info.RemoteJid] = Chat{
|
||||
msg.Info.RemoteJid,
|
||||
isGroup,
|
||||
db.GetIdName(msg.Info.RemoteJid),
|
||||
1,
|
||||
int64(msg.Info.Timestamp),
|
||||
}
|
||||
}
|
||||
//check if message exists, ignore otherwise
|
||||
if _, ok := db.messagesById[msg.Info.Id]; !ok {
|
||||
db.messagesById[msg.Info.Id] = msg
|
||||
@@ -51,6 +64,13 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
|
||||
return didNew
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) NewUnreadChat(id string) {
|
||||
if chat, ok := db.chats[id]; ok {
|
||||
chat.Unread++
|
||||
db.chats[id] = chat
|
||||
}
|
||||
}
|
||||
|
||||
// add audio/video/image/doc message, stored by message id
|
||||
func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
|
||||
var id = ""
|
||||
@@ -70,21 +90,53 @@ func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) AddContact(contact Contact) {
|
||||
db.contacts[contact.Id] = contact
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) AddChat(chat Chat) {
|
||||
db.chats[chat.Id] = chat
|
||||
}
|
||||
|
||||
// get an array of all chat ids
|
||||
func (db *MessageDatabase) GetContactIds() []string {
|
||||
//var this = *db
|
||||
keys := make([]string, len(db.textMessages))
|
||||
func (db *MessageDatabase) GetChatIds() []Chat {
|
||||
keys := make([]Chat, len(db.chats))
|
||||
i := 0
|
||||
for k := range db.textMessages {
|
||||
for _, k := range db.chats {
|
||||
keys[i] = k
|
||||
i++
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return db.latestMessage[keys[i]] > db.latestMessage[keys[j]]
|
||||
return db.latestMessage[keys[i].Id] > db.latestMessage[keys[j].Id]
|
||||
})
|
||||
return keys
|
||||
}
|
||||
|
||||
// gets a pretty name for a whatsapp id
|
||||
func (sm *MessageDatabase) GetIdName(id string) string {
|
||||
if val, ok := sm.contacts[id]; ok {
|
||||
if val.Name != "" {
|
||||
return val.Name
|
||||
} else if val.Short != "" {
|
||||
return val.Short
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
|
||||
}
|
||||
|
||||
// gets a short name for a whatsapp id
|
||||
func (sm *MessageDatabase) GetIdShort(id string) string {
|
||||
if val, ok := sm.contacts[id]; ok {
|
||||
//TODO val.notify from whatsapp??
|
||||
if val.Short != "" {
|
||||
return val.Short
|
||||
} else if val.Name != "" {
|
||||
return val.Name
|
||||
}
|
||||
}
|
||||
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
|
||||
}
|
||||
|
||||
func (db *MessageDatabase) GetMessageInfo(id string) string {
|
||||
if _, ok := db.otherMessages[id]; ok {
|
||||
return "[yellow]OtherMessage[-]"
|
||||
@@ -102,38 +154,10 @@ func (db *MessageDatabase) GetMessageInfo(id string) string {
|
||||
}
|
||||
|
||||
// get a string containing all messages for a chat by chat id
|
||||
func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) {
|
||||
//var this = *db
|
||||
var out = ""
|
||||
var arr = []string{}
|
||||
func (db *MessageDatabase) GetMessages(wid string) []whatsapp.TextMessage {
|
||||
var arr = []whatsapp.TextMessage{}
|
||||
for _, element := range db.textMessages[wid] {
|
||||
out += getTextMessageString(element)
|
||||
out += "\n"
|
||||
arr = append(arr, element.Info.Id)
|
||||
arr = append(arr, *element)
|
||||
}
|
||||
return out, arr
|
||||
}
|
||||
|
||||
// create a formatted string with regions based on message ID from a text message
|
||||
// TODO: move message styling into UI
|
||||
func getTextMessageString(msg *whatsapp.TextMessage) string {
|
||||
colorMe := config.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
|
||||
return arr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user