Compare commits

...

34 Commits

Author SHA1 Message Date
normen
f8368e4998 add /read command to set chat to read
- display connection status (connecting, connected, disconnected)
- allow setting unread count color
2020-11-27 00:11:59 +01:00
normen
121a73c312 add unread count (no reset yet) 2020-11-26 22:52:47 +01:00
normen
86af0d82a4 remove contact file name getter 2020-11-26 21:50:41 +01:00
normen
b94129fb0e rename focus_contacts and contact_sidebar_width - sorry! 2020-11-26 21:48:59 +01:00
normen
59a843bb8d use chat and contact list reported by go-whatsapp
Fixes #22
2020-11-26 21:43:59 +01:00
normen
a86aa3eec3 show contact name for status messages 2020-11-26 18:53:22 +01:00
normen
a20b4e3592 update dependencies 2020-11-26 18:45:23 +01:00
normen
9e62295188 unify variable names 2020-11-26 02:46:00 +01:00
normen
2e891d05ca don't start session manager twice 2020-11-26 02:17:44 +01:00
normen
bfbec54de3 remove number suffix for groups in fallback 2020-11-26 02:10:28 +01:00
normen
c677bce14e fix contact/group color mixup in contact list 2020-11-26 02:05:24 +01:00
normen
6f30efeebe abstract messages, move message styling to UI 2020-11-26 01:27:48 +01:00
normen
14a0e74b25 use pointer for database 2020-11-25 21:34:27 +01:00
normen
4951ca24da help screen cleanups 2020-11-25 03:19:28 +01:00
normen
1cd6c25b02 fix preview and download path mixup 2020-11-25 03:11:38 +01:00
normen
d9a9e7f753 separate messages and interfaces to separate file 2020-11-25 02:12:57 +01:00
normen
5388a1b408 cleanup help 2020-11-25 01:38:06 +01:00
normen
7687af38e1 set file name when uploading 2020-11-24 23:16:29 +01:00
normen
9b26641a0c don't swallow error messages on upload/send 2020-11-24 22:53:36 +01:00
normen
e5ef667909 show help and set no receiver on contact root 2020-11-24 22:29:50 +01:00
normen
c39479f12b fix notifications for own messages 2020-11-24 22:28:24 +01:00
normen
f1109f6465 add /colorlist command 2020-11-24 21:05:12 +01:00
normen
127883701d fix version snafu in 0.9.1 release 2020-11-24 20:52:07 +01:00
normen
5cacc3c5ea bump version 2020-11-24 20:46:17 +01:00
normen
65957ff732 use color_negative for all error messages 2020-11-24 20:38:04 +01:00
normen
7a096dbc94 allow configuring command for image to text conversion 2020-11-24 20:25:21 +01:00
normen
a7977959b5 help info update 2020-11-24 19:19:51 +01:00
normen
ea92d56426 allow sending image, video and audio messages
files have to be right format to be recognized by WhatsApp
2020-11-24 19:16:37 +01:00
normen
3c193d219e allow opening URLs in messages
Fixes #19
2020-11-24 18:47:58 +01:00
normen
f3a2bd3e88 add some code documentation 2020-11-24 17:37:07 +01:00
normen
f0488851ae remove local contacts feature creep
remove singleton connection
2020-11-24 17:30:06 +01:00
normen
c5276247b8 avoid sending notifications when loading backlog 2020-11-24 14:20:26 +01:00
normen
27d3a48d98 update readme 2020-11-24 14:03:52 +01:00
normen
9065248d1c add notification support 2020-11-24 13:58:31 +01:00
9 changed files with 635 additions and 336 deletions

View File

@@ -8,10 +8,13 @@ A command line interface for whatsapp, based on [go-whatsapp](https://github.com
Things that work.
- Sending and receiving WhatsApp messages in a command line app
- Connects through the Web App API without a browser
- Allows sending and receiving WhatsApp messages in a command line app
- Allows downloading and opening image/video/audio/document attachments
- Uses QR code for simple setup
- Allows downloading and opening image/video/audio/document attachments
- Allows sending documents
- Allows color customization
- Supports desktop notifications
- Binaries for Windows, Mac, Linux and RaspBerry Pi
### Caveats
@@ -19,10 +22,8 @@ Things that work.
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
- Only shows existing chats
- Only fetches a few old messages
- No incoming message notification / count
- No unread message count
- No proper connection drop handling
- No uploading of images/video/audio/data
- FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when FaceBook changes stuff in their web app
## Installation / Usage

View File

@@ -22,29 +22,34 @@ type IniFile struct {
}
type General struct {
DownloadPath string
PreviewPath string
CmdPrefix string
DownloadPath string
PreviewPath string
CmdPrefix string
ShowCommand string
EnableNotifications bool
NotificationTimeout int64
}
type Keymap struct {
SwitchPanels string
FocusMessages string
FocusInput string
FocusContacts string
FocusChats string
CommandBacklog string
CommandRead string
CommandConnect string
CommandQuit string
CommandHelp string
MessageDownload string
MessageOpen string
MessageShow string
MessageUrl string
MessageInfo string
MessageRevoke string
}
type Ui struct {
ContactSidebarWidth int
ChatSidebarWidth int
}
type Colors struct {
@@ -58,33 +63,39 @@ type Colors struct {
Borders string
InputBackground string
InputText string
UnreadCount string
Positive string
Negative string
}
var Config = IniFile{
&General{
DownloadPath: GetHomeDir() + "Downloads",
PreviewPath: GetHomeDir() + "Downloads",
CmdPrefix: "/",
DownloadPath: GetHomeDir() + "Downloads",
PreviewPath: GetHomeDir() + "Downloads",
CmdPrefix: "/",
ShowCommand: "jp2a --color",
EnableNotifications: false,
NotificationTimeout: 60,
},
&Keymap{
SwitchPanels: "Tab",
FocusMessages: "Ctrl+w",
FocusInput: "Ctrl+Space",
FocusContacts: "Ctrl+e",
FocusChats: "Ctrl+e",
CommandBacklog: "Ctrl+b",
CommandRead: "Ctrl+n",
CommandConnect: "Ctrl+r",
CommandQuit: "Ctrl+q",
CommandHelp: "Ctrl+?",
MessageDownload: "d",
MessageInfo: "i",
MessageOpen: "o",
MessageUrl: "u",
MessageRevoke: "r",
MessageShow: "s",
},
&Ui{
ContactSidebarWidth: 30,
ChatSidebarWidth: 30,
},
&Colors{
Background: "black",
@@ -97,6 +108,7 @@ var Config = IniFile{
Borders: "white",
InputBackground: "blue",
InputText: "white",
UnreadCount: "yellow",
Positive: "green",
Negative: "red",
},
@@ -152,13 +164,6 @@ func GetSessionFilePath() string {
return GetHomeDir() + ".whatscli.session"
}
func GetContactsFilePath() string {
if sessionFilePath, err := xdg.ConfigFile("whatscli/contacts"); err == nil {
return sessionFilePath
}
return GetHomeDir() + ".whatscli.contacts"
}
// gets the OS home dir with a path separator at the end
func GetHomeDir() string {
usr, err := user.Current()

6
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/adrg/xdg v0.2.3
github.com/gabriel-vasile/mimetype v1.1.2
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591
github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28
github.com/golang/protobuf v1.4.3 // indirect
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
@@ -16,9 +17,10 @@ require (
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.4
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
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
)

28
go.sum
View File

@@ -23,6 +23,12 @@ github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZc
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=
@@ -41,11 +47,18 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
@@ -57,6 +70,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -66,6 +81,7 @@ github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893 h1:24As98PZlIdjZn6V4wUu
github.com/rivo/tview v0.0.0-20201118063654-f007e9ad3893/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
@@ -73,12 +89,16 @@ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EE
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
@@ -101,10 +121,13 @@ golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2 h1:WFCmm2Hi9I2gYf1kv7LQ8ajKA5x9heC2v9xuUKwvf68=
golang.org/x/sys v0.0.0-20201126144705-a4b67b81d3d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -136,8 +159,13 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/xurls/v2 v2.2.0 h1:NSZPykBXJFCetGZykLAxaL6SIpvbVy/UFEniIfHAa8A=
mvdan.cc/xurls/v2 v2.2.0/go.mod h1:EV1RMtya9D6G5DMYPGD8zTQzaHet6Jh8gFlRgGRJeO8=

223
main.go
View File

@@ -6,6 +6,7 @@ import (
"io"
"os/exec"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/normen/whatscli/config"
@@ -15,11 +16,11 @@ import (
"gitlab.com/tslocum/cbind"
)
var VERSION string = "v0.8.6"
var VERSION string = "v0.9.8"
var sndTxt string = ""
var currentReceiver string = ""
var curRegions []string
var currentReceiver messages.Chat = messages.Chat{}
var curRegions []messages.Message
var textView *tview.TextView
var treeView *tview.TreeView
@@ -27,7 +28,7 @@ var textInput *tview.InputField
var topBar *tview.TextView
var infoBar *tview.TextView
var contactRoot *tview.TreeNode
var chatRoot *tview.TreeNode
var app *tview.Application
var sessionManager *messages.SessionManager
@@ -41,11 +42,10 @@ func main() {
uiHandler = UiHandler{}
sessionManager = &messages.SessionManager{}
sessionManager.Init(uiHandler)
messages.LoadContacts()
app = tview.NewApplication()
sideBarWidth := config.Config.Ui.ContactSidebarWidth
sideBarWidth := config.Config.Ui.ChatSidebarWidth
gridLayout := tview.NewGrid()
gridLayout.SetRows(1, 0, 1)
gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth)
@@ -121,36 +121,35 @@ func main() {
app.SetRoot(gridLayout, true)
app.EnableMouse(true)
app.SetFocus(textInput)
go func() {
if err := sessionManager.StartManager(); err != nil {
PrintError(err)
}
}()
if err := sessionManager.StartManager(); err != nil {
PrintError(err)
}
LoadShortcuts()
app.Run()
}
// creates the TreeView for contacts
// creates the TreeView for chats
func MakeTree() *tview.TreeView {
rootDir := "Contacts"
contactRoot = tview.NewTreeNode(rootDir).
rootDir := "Chats"
chatRoot = tview.NewTreeNode(rootDir).
SetColor(tcell.ColorNames[config.Config.Colors.ListHeader])
treeView = tview.NewTreeView().
SetRoot(contactRoot).
SetCurrentNode(contactRoot)
SetRoot(chatRoot).
SetCurrentNode(chatRoot)
treeView.SetBackgroundColor(tcell.ColorNames[config.Config.Colors.Background])
// If a contact was selected, open it.
// If a chat was selected, open it.
treeView.SetChangedFunc(func(node *tview.TreeNode) {
reference := node.GetReference()
if reference == nil {
SetDisplayedChat(messages.Chat{"", false, "", 0, 0})
return // Selecting the root node does nothing.
}
children := node.GetChildren()
if len(children) == 0 {
// Load and show files in this directory.
recv := reference.(string)
SetDisplayedContact(recv)
recv := reference.(messages.Chat)
SetDisplayedChat(recv)
} else {
// Collapse if visible, expand if collapsed.
node.SetExpanded(!node.IsExpanded())
@@ -163,7 +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
@@ -236,7 +235,7 @@ func handleMessagesUp(ev *tcell.EventKey) *tcell.EventKey {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[len(curRegions)-1])
textView.Highlight(curRegions[len(curRegions)-1].Id)
}
textView.ScrollToHighlight()
return nil
@@ -253,7 +252,7 @@ func handleMessagesDown(ev *tcell.EventKey) *tcell.EventKey {
textView.Highlight(newId)
}
} else {
textView.Highlight(curRegions[0])
textView.Highlight(curRegions[0].Id)
}
textView.ScrollToHighlight()
return nil
@@ -263,7 +262,7 @@ func handleMessagesLast(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
textView.Highlight(curRegions[len(curRegions)-1])
textView.Highlight(curRegions[len(curRegions)-1].Id)
textView.ScrollToHighlight()
return nil
}
@@ -272,7 +271,7 @@ func handleMessagesFirst(ev *tcell.EventKey) *tcell.EventKey {
if curRegions == nil || len(curRegions) == 0 {
return nil
}
textView.Highlight(curRegions[0])
textView.Highlight(curRegions[0].Id)
textView.ScrollToHighlight()
return nil
}
@@ -286,6 +285,7 @@ func handleExitMessages(ev *tcell.EventKey) *tcell.EventKey {
return nil
}
// load the key map
func LoadShortcuts() {
keyBindings = cbind.NewConfiguration()
if err := keyBindings.Set(config.Config.Keymap.FocusMessages, handleFocusMessage); err != nil {
@@ -294,12 +294,15 @@ func LoadShortcuts() {
if err := keyBindings.Set(config.Config.Keymap.FocusInput, handleFocusInput); err != nil {
PrintErrorMsg("focus_input:", err)
}
if err := keyBindings.Set(config.Config.Keymap.FocusContacts, handleFocusContacts); err != nil {
if err := keyBindings.Set(config.Config.Keymap.FocusChats, handleFocusContacts); err != nil {
PrintErrorMsg("focus_contacts:", err)
}
if err := keyBindings.Set(config.Config.Keymap.SwitchPanels, handleSwitchPanels); err != nil {
PrintErrorMsg("switch_panels:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandRead, handleCommand("read")); err != nil {
PrintErrorMsg("command_read:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil {
PrintErrorMsg("command_backlog:", err)
}
@@ -323,6 +326,9 @@ func LoadShortcuts() {
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
PrintErrorMsg("message_show:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageUrl, handleMessageCommand("url")); err != nil {
PrintErrorMsg("message_url:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageInfo, handleMessageCommand("info")); err != nil {
PrintErrorMsg("message_info:", err)
}
@@ -345,31 +351,41 @@ func PrintHelp() {
fmt.Fprintln(textView, "[::b]WhatsCLI "+VERSION+"[-]")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::u]Keys:[-::-]")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = scroll history/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = switch input/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusMessages, "[::-] = focus message panel")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusContacts, "[::-] = focus contacts panel")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusInput, "[::-] = focus input")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Message panel focused:[-::-]")
fmt.Fprintln(textView, "Global")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = Scroll history/chats")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = Switch input/chats")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.FocusMessages, "[::-] = Focus message panel")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::-]Message panel[-::-]")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = select message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageDownload, "[::-] = download attachment -> ", config.Config.General.DownloadPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = download & open attachment -> ", config.Config.General.PreviewPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = download & show image using jp2a -> ", config.Config.General.PreviewPath)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageRevoke, "[::-] = revoke message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageInfo, "[::-] = info about message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageDownload, "[::-] = Download attachment")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageOpen, "[::-] = Download & open attachment")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageShow, "[::-] = Download & show image using", config.Config.General.ShowCommand)
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageUrl, "[::-] = Find URL in message and open it")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageRevoke, "[::-] = Revoke message")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.MessageInfo, "[::-] = Info about message")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "[-::u]Commands:[-::-]")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"backlog [::-]or[::b]", config.Config.Keymap.CommandBacklog, "[::-] = load next 10 older messages for current chat")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"connect [::-]or[::b]", config.Config.Keymap.CommandConnect, "[::-] = (re)connect in case the connection dropped")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"help [::-]or[::b]", config.Config.Keymap.CommandHelp, "[::-] = show this help")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"quit [::-]or[::b]", config.Config.Keymap.CommandQuit, "[::-] = exit app")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"leave[::-] = leave group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"upload[::-] /path/to/file = upload file to current chat")
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, "[::b] "+cmdPrefix+"leave[::-] = Leave group")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Configuration:")
fmt.Fprintln(textView, " ->", config.GetConfigFilePath())
fmt.Fprintln(textView, "")
}
// called when text is entered by the user
@@ -410,7 +426,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("")
@@ -422,17 +438,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
}
}
@@ -454,7 +470,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
@@ -462,13 +478,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 {
@@ -480,6 +504,7 @@ func PrintImage(path string) {
PrintError(err)
}
// updates the status bar
func UpdateStatusBar(statusInfo messages.SessionStatus) {
out := " "
if statusInfo.Connected {
@@ -509,58 +534,94 @@ func UpdateStatusBar(statusInfo messages.SessionStatus) {
//infoBar.SetText("🔋: ??%")
}
// notifies about a new message if its recent
//func NotifyMsg(msg whatsapp.TextMessage) {
//if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
//fmt.Print("\a")
//err := beeep.Notify(messages.GetIdName(msg.Info.RemoteJid), msg.Text, "")
//if err != nil {
// fmt.Fprintln(textView, "[red]error in notification[-]")
//}
//}
//}
// sets the current contact, loads text from storage to TextView
func SetDisplayedContact(wid string) {
// sets the current chat, loads text from storage to TextView
func SetDisplayedChat(wid messages.Chat) {
//TODO: how to get chat to set
currentReceiver = wid
textView.Clear()
textView.SetTitle(messages.GetIdName(wid))
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver}}
textView.SetTitle(wid.Name)
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
}
// get a string representation of all messages for chat
func getMessagesString(msgs []messages.Message) string {
out := ""
for _, msg := range msgs {
out += getTextMessageString(&msg)
out += "\n"
}
return out
}
// create a formatted string with regions based on message ID from a text message
func getTextMessageString(msg *messages.Message) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
out := ""
text := tview.Escape(msg.Text)
tim := time.Unix(int64(msg.Timestamp), 0)
time := tim.Format("02-01-06 15:04:05")
out += "[\""
out += msg.Id
out += "\"]"
if msg.FromMe { //msg from me
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
} else { // message from others
out += "[-::d](" + time + ") [" + colorContact + "::b]" + msg.ContactShort + ": [-::-]" + text
}
out += "[\"\"]"
return out
}
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 == "" {
PrintHelp()
}
})
}
// 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()
for _, element := range ids {
node := tview.NewTreeNode(messages.GetIdName(element)).
name := element.Name
if name == "" {
name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX)
}
if element.Unread > 0 {
name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])"
//tim := time.Unix(element.LastMessage, 0)
//sin := time.Since(tim)
//since := fmt.Sprintf("%s", sin)
//time := tim.Format("02-01-06 15:04:05")
//name += since
}
node := tview.NewTreeNode(name).
SetReference(element).
SetSelectable(true)
if strings.Count(element, messages.CONTACTSUFFIX) > 0 {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
} else {
if element.IsGroup {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup])
} else {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
}
contactRoot.AddChild(node)
chatRoot.AddChild(node)
if element == currentReceiver {
treeView.SetCurrentNode(node)
}

View File

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

77
messages/messages.go Normal file
View File

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

View File

@@ -4,109 +4,141 @@ import (
"encoding/gob"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/gdamore/tcell/v2"
"github.com/gen2brain/beeep"
"github.com/rivo/tview"
"mvdan.cc/xurls/v2"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
"github.com/normen/whatscli/qrcode"
)
// TODO: move message styling and ordering into UI, don't use strings
// move these funcs/interface to channels
type UiMessageHandler interface {
NewMessage(string, string)
NewScreen(string, []string)
SetContacts([]string)
PrintError(error)
PrintText(string)
PrintFile(string)
SetStatus(SessionStatus)
OpenFile(string)
GetWriter() io.Writer
}
type SessionStatus struct {
BatteryCharge int
BatteryLoading bool
BatteryPowersave bool
Connected bool
LastSeen string
}
type BatteryMsg struct {
charge int
loading bool
powersave bool
}
type StatusMsg struct {
connected bool
err error
}
type Command struct {
Name string
Params []string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
// SessionManager deals with the connection and receives commands from the UI
// it updates the UI accordingly
type SessionManager struct {
db MessageDatabase
currentReceiver string // currently selected contact for message handling
db *MessageDatabase
currentReceiver string // currently selected chat for message handling
uiHandler UiMessageHandler
connection *whatsapp.Conn
BatteryChannel chan BatteryMsg
StatusChannel chan StatusMsg
CommandChannel chan Command
ChatChannel chan whatsapp.Chat
ContactChannel chan whatsapp.Contact
TextChannel chan whatsapp.TextMessage
OtherChannel chan interface{}
statusInfo SessionStatus
lastSent time.Time
started bool
}
// initialize the SessionManager
func (sm *SessionManager) Init(handler UiMessageHandler) {
sm.db = MessageDatabase{}
sm.db = &MessageDatabase{}
sm.db.Init()
sm.uiHandler = handler
sm.BatteryChannel = make(chan BatteryMsg, 10)
sm.StatusChannel = make(chan StatusMsg, 10)
sm.CommandChannel = make(chan Command, 10)
sm.ChatChannel = make(chan whatsapp.Chat, 10)
sm.ContactChannel = make(chan whatsapp.Contact, 10)
sm.TextChannel = make(chan whatsapp.TextMessage, 10)
sm.OtherChannel = make(chan interface{}, 10)
}
// starts the receiver and message handling thread
// TODO: can't be stopped, can only be called once!
// starts the receiver and message handling go routine
func (sm *SessionManager) StartManager() error {
if sm.started {
return errors.New("session manager running, send commands to control")
}
sm.started = true
go sm.runManager()
return nil
}
func (sm *SessionManager) runManager() error {
var wac = sm.getConnection()
err := sm.loginWithConnection(wac)
if err != nil {
sm.uiHandler.PrintError(err)
}
wac.AddHandler(sm)
for {
for sm.started == true {
select {
case msg := <-sm.TextChannel:
didNew := sm.db.AddTextMessage(&msg)
if msg.Info.RemoteJid == sm.currentReceiver {
if didNew {
sm.uiHandler.NewMessage(getTextMessageString(&msg), msg.Info.Id)
sm.uiHandler.NewMessage(sm.createMessage(&msg))
} else {
screen, ids := sm.db.GetMessagesString(sm.currentReceiver)
sm.uiHandler.NewScreen(screen, ids)
screen := sm.getMessages(sm.currentReceiver)
sm.uiHandler.NewScreen(screen)
}
// notify if chat is in focus and we didn't send a message recently
// TODO: move to UI (when UI has time in messages)
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
err := beeep.Notify(sm.db.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
if err != nil {
sm.uiHandler.PrintError(err)
}
}
}
}
} else {
if config.Config.General.EnableNotifications && !msg.Info.FromMe {
// notify if message is younger than 30 sec and not in focus
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
err := beeep.Notify(sm.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:
@@ -115,6 +147,7 @@ func (sm *SessionManager) StartManager() error {
sm.statusInfo.BatteryCharge = batteryMsg.charge
sm.uiHandler.SetStatus(sm.statusInfo)
case statusMsg := <-sm.StatusChannel:
prevStatus := sm.statusInfo.Connected
if statusMsg.err != nil {
} else {
sm.statusInfo.Connected = statusMsg.connected
@@ -123,6 +156,13 @@ func (sm *SessionManager) StartManager() error {
connected := wac.GetConnected()
sm.statusInfo.Connected = connected
sm.uiHandler.SetStatus(sm.statusInfo)
if prevStatus != sm.statusInfo.Connected {
if sm.statusInfo.Connected {
sm.uiHandler.PrintText("connected")
} else {
sm.uiHandler.PrintText("disconnected")
}
}
}
}
fmt.Fprintln(sm.uiHandler.GetWriter(), "closing the receiver")
@@ -130,25 +170,26 @@ 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 {
if sm.connection == nil {
wacc, err := whatsapp.NewConn(5 * time.Second)
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
}
@@ -162,6 +203,7 @@ func (sm *SessionManager) login() error {
// loginWithConnection logs in the user using a provided connection. It ries to see if a session already exists. If not, tries to create a
// new one using qr scanned on the terminal.
func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
sm.uiHandler.PrintText("connecting..")
if wac != nil && wac.GetConnected() {
wac.Disconnect()
sm.StatusChannel <- StatusMsg{false, nil}
@@ -198,6 +240,7 @@ func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
return nil
}
// disconnects the session
func (sm *SessionManager) disconnect() error {
wac := sm.getConnection()
var err error
@@ -208,30 +251,32 @@ func (sm *SessionManager) disconnect() error {
return err
}
// logout logs out the user.
// logout logs out the user, deletes session file
func (ub *SessionManager) logout() error {
ub.getConnection().Disconnect()
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":
@@ -246,19 +291,36 @@ 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)
}
}
}
} else {
sm.printCommandUsage("read", "-> only works in a chat")
}
case "info":
if checkParam(command.Params, 1) {
sm.uiHandler.PrintText(sm.db.GetMessageInfo(command.Params[0]))
} else {
sm.uiHandler.PrintText("[red]Usage:[-] info [message-id[]")
sm.printCommandUsage("info", "[message-id[]")
}
case "download":
if checkParam(command.Params, 1) {
@@ -268,7 +330,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) {
@@ -278,7 +340,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) {
@@ -288,18 +350,61 @@ 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 {
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,
},
@@ -307,14 +412,68 @@ func (sm *SessionManager) execCommand(command Command) {
Content: file,
}
wac := sm.getConnection()
_, err := wac.Send(msg)
if err != nil {
sm.uiHandler.PrintError(err)
}
sm.lastSent = time.Now()
_, err = wac.Send(msg)
}
}
} else {
sm.uiHandler.PrintText("[red]Usage:[-] upload [/path/to/file[]")
sm.printCommandUsage("sendimage", "/path/to/file")
}
sm.uiHandler.PrintError(err)
case "sendvideo":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendvideo", "-> only works in a chat")
return
}
var err error
var mime *mimetype.MIME
var file *os.File
if checkParam(command.Params, 1) {
path := strings.Join(command.Params, " ")
if mime, err = mimetype.DetectFile(path); err == nil {
if file, err = os.Open(path); err == nil {
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":
@@ -344,7 +503,7 @@ 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
@@ -358,9 +517,21 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintText("left group " + groupId)
}
sm.uiHandler.PrintError(err)
case "colorlist":
out := ""
for idx, _ := range tcell.ColorNames {
out = out + "[" + idx + "]" + idx + "[-]\n"
}
sm.uiHandler.PrintText(out)
}
}
// helper for built-in command help
func (sm *SessionManager) printCommandUsage(command string, usage string) {
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Usage:[-] " + command + " " + usage)
}
// check if parameters for command are okay
func checkParam(arr []string, length int) bool {
if arr == nil || len(arr) < length {
return false
@@ -368,6 +539,41 @@ 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
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 {
@@ -391,9 +597,9 @@ func (sm *SessionManager) downloadMessage(wid string, preview bool) (string, err
if msg, ok := sm.db.otherMessages[wid]; ok {
var fileName string = ""
if preview {
fileName += config.Config.General.DownloadPath
} else {
fileName += config.Config.General.PreviewPath
} else {
fileName += config.Config.General.DownloadPath
}
fileName += string(os.PathSeparator)
switch v := (*msg).(type) {
@@ -460,7 +666,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,
@@ -470,13 +676,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))
}
}
}
@@ -485,7 +692,7 @@ 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.PrintText("[" + config.Config.Colors.Negative + "]go-whatsapp reported an error:[-]")
sm.uiHandler.PrintError(err)
statusMsg := StatusMsg{false, err}
sm.StatusChannel <- statusMsg
@@ -561,7 +768,7 @@ func (sm *SessionManager) HandleAudioMessage(message whatsapp.AudioMessage) {
// add contact info to database (not needed, internal db of connection is used)
func (sm *SessionManager) HandleNewContact(contact whatsapp.Contact) {
// redundant, wac has contacts
//contactChannel <- contact
sm.ContactChannel <- contact
}
// handle battery messages
@@ -569,6 +776,18 @@ 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
}
}
// helper to save an attachment and open it if specified
func saveAttachment(data []byte, path string) (string, error) {
err := ioutil.WriteFile(path, data, 0644)
@@ -581,11 +800,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()

View File

@@ -3,11 +3,8 @@ package messages
import (
"sort"
"strings"
"time"
"github.com/Rhymen/go-whatsapp"
"github.com/normen/whatscli/config"
"github.com/rivo/tview"
)
type MessageDatabase struct {
@@ -15,6 +12,8 @@ type MessageDatabase struct {
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
latestMessage map[string]uint64 // last message from RemoteJid
otherMessages map[string]*interface{} // other non-text messages, stored by ID
contacts map[string]Contact
chats map[string]Chat
}
// initialize the database
@@ -24,6 +23,8 @@ func (db *MessageDatabase) Init() {
db.messagesById = make(map[string]*whatsapp.TextMessage)
db.otherMessages = make(map[string]*interface{})
db.latestMessage = make(map[string]uint64)
db.contacts = make(map[string]Contact)
db.chats = make(map[string]Chat)
}
// add a text message to the database, stored by RemoteJid
@@ -70,21 +71,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 +135,10 @@ func (db *MessageDatabase) GetMessageInfo(id string) string {
}
// get a string containing all messages for a chat by chat id
func (db *MessageDatabase) GetMessagesString(wid string) (string, []string) {
//var this = *db
var out = ""
var arr = []string{}
func (db *MessageDatabase) GetMessages(wid string) []whatsapp.TextMessage {
var arr = []whatsapp.TextMessage{}
for _, element := range db.textMessages[wid] {
out += getTextMessageString(element)
out += "\n"
arr = append(arr, element.Info.Id)
arr = append(arr, *element)
}
return out, arr
}
// create a formatted string with regions based on message ID from a text message
// TODO: move message styling into UI
func getTextMessageString(msg *whatsapp.TextMessage) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
out := ""
text := tview.Escape(msg.Text)
tim := time.Unix(int64(msg.Info.Timestamp), 0)
time := tim.Format("02-01-06 15:04:05")
out += "[\""
out += msg.Info.Id
out += "\"]"
if msg.Info.FromMe { //msg from me
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) { // group msg
userId := msg.Info.SenderJid
out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(userId) + ": [-::-]" + text
} else { // message from others
out += "[-::d](" + time + ") [" + colorContact + "::b]" + GetIdShort(msg.Info.RemoteJid) + ": [-::-]" + text
}
out += "[\"\"]"
return out
return arr
}