Compare commits

...

21 Commits

Author SHA1 Message Date
normen
96e9d75810 small fix in help screen 2020-11-27 21:48:04 +01:00
normen
b2904929b0 update README 2020-11-27 21:42:32 +01:00
normen
033e7aa1ac allow creating and managing groups 2020-11-27 21:33:40 +01:00
normen
53e404dd55 add command/binding to copy selected user id 2020-11-27 20:08:14 +01:00
normen
8318d2e80f remove redundant "reported an error" 2020-11-27 18:58:51 +01:00
normen
3af2a3738f fix unread count not updating 2020-11-27 14:22:20 +01:00
normen
e31c2c3e36 get the battery state on connection 2020-11-27 03:30:13 +01:00
normen
3f20981550 cleaner logout 2020-11-27 03:10:25 +01:00
normen
f8368e4998 add /read command to set chat to read
- display connection status (connecting, connected, disconnected)
- allow setting unread count color
2020-11-27 00:11:59 +01:00
normen
121a73c312 add unread count (no reset yet) 2020-11-26 22:52:47 +01:00
normen
86af0d82a4 remove contact file name getter 2020-11-26 21:50:41 +01:00
normen
b94129fb0e rename focus_contacts and contact_sidebar_width - sorry! 2020-11-26 21:48:59 +01:00
normen
59a843bb8d use chat and contact list reported by go-whatsapp
Fixes #22
2020-11-26 21:43:59 +01:00
normen
a86aa3eec3 show contact name for status messages 2020-11-26 18:53:22 +01:00
normen
a20b4e3592 update dependencies 2020-11-26 18:45:23 +01:00
normen
9e62295188 unify variable names 2020-11-26 02:46:00 +01:00
normen
2e891d05ca don't start session manager twice 2020-11-26 02:17:44 +01:00
normen
bfbec54de3 remove number suffix for groups in fallback 2020-11-26 02:10:28 +01:00
normen
c677bce14e fix contact/group color mixup in contact list 2020-11-26 02:05:24 +01:00
normen
6f30efeebe abstract messages, move message styling to UI 2020-11-26 01:27:48 +01:00
normen
14a0e74b25 use pointer for database 2020-11-25 21:34:27 +01:00
8 changed files with 542 additions and 185 deletions

View File

@@ -14,16 +14,17 @@ Things that work.
- Allows downloading and opening image/video/audio/document attachments
- Allows sending documents
- Allows color customization
- Allows basic group management
- Supports desktop notifications
- Binaries for Windows, Mac, Linux and RaspBerry Pi
### Caveats
This is a WIP and mainly meant for my personal use. Heres some things you might expect to work that don't. Plus some other things I should mention.
Heres some things you might expect to work that don't. Plus some other things I should mention.
- Only shows existing chats
- No unread message count
- No proper connection drop handling
- No auto-reconnect when connection drops
- No automation of messages, no sending of messages through shell commands
- FaceBook obviously doesn't endorse or like these kinds of apps and they're likely to break when FaceBook changes stuff in their web app
## Installation / Usage

View File

@@ -34,8 +34,10 @@ type Keymap struct {
SwitchPanels string
FocusMessages string
FocusInput string
FocusContacts string
FocusChats string
CommandBacklog string
CommandRead string
CommandCopyuser string
CommandConnect string
CommandQuit string
CommandHelp string
@@ -48,7 +50,7 @@ type Keymap struct {
}
type Ui struct {
ContactSidebarWidth int
ChatSidebarWidth int
}
type Colors struct {
@@ -62,6 +64,7 @@ type Colors struct {
Borders string
InputBackground string
InputText string
UnreadCount string
Positive string
Negative string
}
@@ -79,8 +82,10 @@ var Config = IniFile{
SwitchPanels: "Tab",
FocusMessages: "Ctrl+w",
FocusInput: "Ctrl+Space",
FocusContacts: "Ctrl+e",
FocusChats: "Ctrl+e",
CommandBacklog: "Ctrl+b",
CommandRead: "Ctrl+n",
CommandCopyuser: "Ctrl+c",
CommandConnect: "Ctrl+r",
CommandQuit: "Ctrl+q",
CommandHelp: "Ctrl+?",
@@ -92,7 +97,7 @@ var Config = IniFile{
MessageShow: "s",
},
&Ui{
ContactSidebarWidth: 30,
ChatSidebarWidth: 30,
},
&Colors{
Background: "black",
@@ -105,6 +110,7 @@ var Config = IniFile{
Borders: "white",
InputBackground: "blue",
InputText: "white",
UnreadCount: "yellow",
Positive: "green",
Negative: "red",
},
@@ -160,13 +166,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()

5
go.mod
View File

@@ -5,6 +5,7 @@ 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
@@ -17,8 +18,8 @@ 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

6
go.sum
View File

@@ -10,6 +10,8 @@ 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=
@@ -97,6 +99,8 @@ golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnf
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=
@@ -124,6 +128,8 @@ golang.org/x/sys v0.0.0-20201013132646-2da7054afaeb/go.mod h1:h1NjWce9XRLGQEsW7w
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=

189
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.9.5"
var VERSION string = "v1.0.0"
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
@@ -44,7 +45,7 @@ func main() {
app = tview.NewApplication()
sideBarWidth := config.Config.Ui.ContactSidebarWidth
sideBarWidth := config.Config.Ui.ChatSidebarWidth
gridLayout := tview.NewGrid()
gridLayout.SetRows(1, 0, 1)
gridLayout.SetColumns(sideBarWidth, 0, sideBarWidth)
@@ -120,37 +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 {
SetDisplayedContact("")
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
@@ -202,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()
@@ -236,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
@@ -253,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
}
@@ -272,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
}
@@ -295,12 +313,18 @@ 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.CommandCopyuser, handleCopyUser); err != nil {
PrintErrorMsg("command_copyuser:", err)
}
if err := keyBindings.Set(config.Config.Keymap.CommandBacklog, handleCommand("backlog")); err != nil {
PrintErrorMsg("command_backlog:", err)
}
@@ -321,6 +345,9 @@ func LoadShortcuts() {
if err := keysMessages.Set(config.Config.Keymap.MessageOpen, handleMessageCommand("open")); err != nil {
PrintErrorMsg("message_open:", err)
}
if err := keysMessages.Set(config.Config.Keymap.CommandCopyuser, handleCopyUser); err != nil {
PrintErrorMsg("command_copyuser:", err)
}
if err := keysMessages.Set(config.Config.Keymap.MessageShow, handleMessageCommand("show")); err != nil {
PrintErrorMsg("message_show:", err)
}
@@ -341,6 +368,10 @@ 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
@@ -351,8 +382,8 @@ func PrintHelp() {
fmt.Fprintln(textView, "[-::u]Keys:[-::-]")
fmt.Fprintln(textView, "")
fmt.Fprintln(textView, "Global")
fmt.Fprintln(textView, "[::b] Up/Down[::-] = Scroll history/contacts")
fmt.Fprintln(textView, "[::b]", config.Config.Keymap.SwitchPanels, "[::-] = Switch input/contacts")
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[-::-]")
@@ -374,14 +405,23 @@ func PrintHelp() {
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+"add[::-] [user-id[] = Add user to group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"admin[::-] [user-id[] = Set admin role for user in group")
fmt.Fprintln(textView, "[::b] "+cmdPrefix+"subject[::-] New Subject = Change subject of 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
@@ -422,7 +462,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("")
@@ -434,17 +474,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
}
}
@@ -530,51 +570,100 @@ func UpdateStatusBar(statusInfo messages.SessionStatus) {
//infoBar.SetText("🔋: ??%")
}
// sets the current contact, loads text from storage to TextView
func SetDisplayedContact(wid string) {
// sets the current chat, loads text from storage to TextView
func SetDisplayedChat(wid messages.Chat) {
//TODO: how to get chat to set
currentReceiver = wid
textView.Clear()
textView.SetTitle(sessionManager.GetIdName(wid))
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver}}
textView.SetTitle(wid.Name)
sessionManager.CommandChannel <- messages.Command{"select", []string{currentReceiver.Id}}
}
// get a string representation of all messages for chat
func getMessagesString(msgs []messages.Message) string {
out := ""
for _, msg := range msgs {
out += getTextMessageString(&msg)
out += "\n"
}
return out
}
// create a formatted string with regions based on message ID from a text message
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()
oldId := currentReceiver.Id
for _, element := range ids {
node := tview.NewTreeNode(sessionManager.GetIdName(element)).
name := element.Name
if name == "" {
name = strings.TrimSuffix(strings.TrimSuffix(element.Id, messages.GROUPSUFFIX), messages.CONTACTSUFFIX)
}
if element.Unread > 0 {
name += " ([" + config.Config.Colors.UnreadCount + "]" + fmt.Sprint(element.Unread) + "[-])"
//tim := time.Unix(element.LastMessage, 0)
//sin := time.Since(tim)
//since := fmt.Sprintf("%s", sin)
//time := tim.Format("02-01-06 15:04:05")
//name += since
}
node := tview.NewTreeNode(name).
SetReference(element).
SetSelectable(true)
if strings.Count(element, messages.CONTACTSUFFIX) > 0 {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
} else {
if element.IsGroup {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListGroup])
} else {
node.SetColor(tcell.ColorNames[config.Config.Colors.ListContact])
}
contactRoot.AddChild(node)
if element == currentReceiver {
// store new currentReceiver, else the selection on the left goes off
if element.Id == oldId {
currentReceiver = element
}
chatRoot.AddChild(node)
if element.Id == currentReceiver.Id {
treeView.SetCurrentNode(node)
}
}

View File

@@ -5,9 +5,9 @@ import "io"
// TODO: move these funcs/interface to channels
type UiMessageHandler interface {
NewMessage(string, string)
NewScreen(string, []string)
SetContacts([]string)
NewMessage(Message)
NewScreen([]Message)
SetChats([]Chat)
PrintError(error)
PrintText(string)
PrintFile(string)
@@ -46,18 +46,32 @@ type Command struct {
// internal message representation to abstract from message lib
type Message struct {
id string
timestamp uint64
sourceId string
sourceName string
fromMe bool
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
Id string
Name string
Short string
}
const GROUPSUFFIX = "@g.us"
const CONTACTSUFFIX = "@s.whatsapp.net"
const STATUSSUFFIX = "status@broadcast"

View File

@@ -2,82 +2,96 @@ package messages
import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"mime"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"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/rivo/tview"
"mvdan.cc/xurls/v2"
"github.com/Rhymen/go-whatsapp"
"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
// SessionManager deals with the connection and receives commands from the UI
// it updates the UI accordingly
type SessionManager struct {
db MessageDatabase
currentReceiver string // currently selected contact for message handling
db *MessageDatabase
currentReceiver string // currently selected chat for message handling
uiHandler UiMessageHandler
connection *whatsapp.Conn
BatteryChannel chan BatteryMsg
StatusChannel chan StatusMsg
CommandChannel chan Command
ChatChannel chan whatsapp.Chat
ContactChannel chan whatsapp.Contact
TextChannel chan whatsapp.TextMessage
OtherChannel chan interface{}
statusInfo SessionStatus
lastSent time.Time
started bool
}
// initialize the SessionManager
func (sm *SessionManager) Init(handler UiMessageHandler) {
sm.db = MessageDatabase{}
sm.db = &MessageDatabase{}
sm.db.Init()
sm.uiHandler = handler
sm.BatteryChannel = make(chan BatteryMsg, 10)
sm.StatusChannel = make(chan StatusMsg, 10)
sm.CommandChannel = make(chan Command, 10)
sm.ChatChannel = make(chan whatsapp.Chat, 10)
sm.ContactChannel = make(chan whatsapp.Contact, 10)
sm.TextChannel = make(chan whatsapp.TextMessage, 10)
sm.OtherChannel = make(chan interface{}, 10)
}
// starts the receiver and message handling thread
// TODO: can't be stopped, can only be called once!
// starts the receiver and message handling go routine
func (sm *SessionManager) StartManager() error {
if sm.started {
return errors.New("session manager running, send commands to control")
}
sm.started = true
go sm.runManager()
return nil
}
func (sm *SessionManager) runManager() error {
var wac = sm.getConnection()
err := sm.loginWithConnection(wac)
if err != nil {
sm.uiHandler.PrintError(err)
}
wac.AddHandler(sm)
for {
for sm.started == true {
select {
case msg := <-sm.TextChannel:
didNew := sm.db.AddTextMessage(&msg)
if msg.Info.RemoteJid == sm.currentReceiver {
if didNew {
sm.uiHandler.NewMessage(sm.getTextMessageString(&msg), msg.Info.Id)
sm.uiHandler.NewMessage(sm.createMessage(&msg))
} else {
screen, ids := sm.getMessagesString(sm.currentReceiver)
sm.uiHandler.NewScreen(screen, ids)
screen := sm.getMessages(sm.currentReceiver)
sm.uiHandler.NewScreen(screen)
}
// notify if contact is in focus and we didn't send a message recently
// 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.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
// notify if chat is in focus and we didn't send a message recently
// TODO: move notify to UI
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
if int64(msg.Info.Timestamp) > sm.lastSent.Unix()+config.Config.General.NotificationTimeout {
sm.db.NewUnreadChat(msg.Info.RemoteJid)
if 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)
}
@@ -85,19 +99,49 @@ func (sm *SessionManager) StartManager() error {
}
}
} 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.GetIdShort(msg.Info.RemoteJid), msg.Text, "")
// notify if message is younger than 30 sec and not in focus
if int64(msg.Info.Timestamp) > time.Now().Unix()-30 {
sm.db.NewUnreadChat(msg.Info.RemoteJid)
if 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:
@@ -106,6 +150,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
@@ -114,6 +159,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")
@@ -121,18 +173,23 @@ func (sm *SessionManager) StartManager() error {
return nil
}
// set the currently selected contact
// set the currently selected chat
func (sm *SessionManager) setCurrentReceiver(id string) {
sm.currentReceiver = id
screen, ids := sm.getMessagesString(id)
sm.uiHandler.NewScreen(screen, ids)
screen := sm.getMessages(id)
sm.uiHandler.NewScreen(screen)
}
// gets an existing connection or creates one
func (sm *SessionManager) getConnection() *whatsapp.Conn {
var wac *whatsapp.Conn
if sm.connection == nil {
wacc, err := whatsapp.NewConn(5 * time.Second)
options := &whatsapp.Options{
Timeout: 5 * time.Second,
LongClientName: "WhatsCLI Client",
ShortClientName: "whatscli",
}
wacc, err := whatsapp.NewConnWithOptions(options)
if err != nil {
return nil
}
@@ -154,6 +211,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}
@@ -185,7 +243,12 @@ func (sm *SessionManager) loginWithConnection(wac *whatsapp.Conn) error {
if err != nil {
return fmt.Errorf("error saving session: %v\n", err)
}
//<-time.After(3 * time.Second)
//get initial battery state
sm.BatteryChannel <- BatteryMsg{
wac.Info.Battery,
wac.Info.Plugged,
false,
}
sm.StatusChannel <- StatusMsg{true, nil}
return nil
}
@@ -203,7 +266,9 @@ func (sm *SessionManager) disconnect() error {
// logout logs out the user, deletes session file
func (ub *SessionManager) logout() error {
ub.getConnection().Disconnect()
err := ub.getConnection().Logout()
ub.StatusChannel <- StatusMsg{false, err}
ub.uiHandler.PrintText("removing login data..")
return removeSession()
}
@@ -214,15 +279,18 @@ func (sm *SessionManager) execCommand(command Command) {
default:
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]Unknown command: [-]" + cmd)
case "backlog":
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")
}
case "login":
sm.uiHandler.PrintError(sm.login())
@@ -238,13 +306,33 @@ func (sm *SessionManager) execCommand(command Command) {
text := strings.Join(textParams, " ")
sm.sendText(command.Params[0], text)
} else {
sm.printCommandUsage("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.printCommandUsage("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) {
@@ -252,6 +340,26 @@ func (sm *SessionManager) execCommand(command Command) {
} else {
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) {
if path, err := sm.downloadMessage(command.Params[0], false); err != nil {
@@ -296,6 +404,7 @@ func (sm *SessionManager) execCommand(command Command) {
}
case "upload":
if sm.currentReceiver == "" {
sm.printCommandUsage("upload", "-> only works in a chat")
return
}
var err error
@@ -323,6 +432,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendimage":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendimage", "-> only works in a chat")
return
}
var err error
@@ -350,6 +460,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendvideo":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendvideo", "-> only works in a chat")
return
}
var err error
@@ -377,6 +488,7 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintError(err)
case "sendaudio":
if sm.currentReceiver == "" {
sm.printCommandUsage("sendaudio", "-> only works in a chat")
return
}
var err error
@@ -433,8 +545,9 @@ func (sm *SessionManager) execCommand(command Command) {
}
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
@@ -443,6 +556,99 @@ func (sm *SessionManager) execCommand(command Command) {
sm.uiHandler.PrintText("left group " + groupId)
}
sm.uiHandler.PrintError(err)
case "create":
if !checkParam(command.Params, 1) {
sm.printCommandUsage("create", "[user-id[] [user-id[] New Group Subject")
sm.printCommandUsage("create", "New Group Subject")
return
}
// first params are users if ending in CONTACTSUFFIX, rest is name
users := []string{}
idx := 0
size := len(command.Params)
for idx = 0; idx < size && strings.Index(command.Params[idx], CONTACTSUFFIX) > 0; idx++ {
users = append(users, command.Params[idx])
}
name := ""
if len(command.Params) > idx {
name = strings.Join(command.Params[idx:], " ")
}
wac := sm.getConnection()
var err error
var groupId <-chan string
groupId, err = wac.CreateGroup(name, users)
if err == nil {
sm.uiHandler.PrintText("creating new group " + name)
resultInfo := <-groupId
//{"status":200,"gid":"491600000009-0606000436@g.us","participants":[{"491700000000@c.us":{"code":"200"}},{"4917600000001@c.us":{"code": "200"}}]}
var result map[string]interface{}
json.Unmarshal([]byte(resultInfo), &result)
newChatId := result["gid"].(string)
sm.uiHandler.PrintText("got new Id " + newChatId)
newChat := Chat{}
newChat.Id = newChatId
newChat.Name = name
newChat.IsGroup = true
sm.db.chats[newChatId] = newChat
sm.uiHandler.SetChats(sm.db.GetChatIds())
}
sm.uiHandler.PrintError(err)
case "add":
groupId := sm.currentReceiver
if strings.Index(groupId, GROUPSUFFIX) < 0 {
sm.uiHandler.PrintText("not a group")
return
}
if !checkParam(command.Params, 1) {
sm.printCommandUsage("add", "[user-id[]")
return
}
wac := sm.getConnection()
var err error
_, err = wac.AddMember(groupId, command.Params)
if err == nil {
sm.uiHandler.PrintText("added new members for " + groupId)
}
sm.uiHandler.PrintError(err)
case "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 {
@@ -465,69 +671,39 @@ func checkParam(arr []string, length int) bool {
return true
}
// gets a pretty name for a whatsapp id
func (sm *SessionManager) GetIdName(id string) string {
if val, ok := sm.getConnection().Store.Contacts[id]; ok {
if val.Name != "" {
return val.Name
} else if val.Short != "" {
return val.Short
} else if val.Notify != "" {
return val.Notify
}
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// gets a short name for a whatsapp id
func (sm *SessionManager) GetIdShort(id string) string {
if val, ok := sm.getConnection().Store.Contacts[id]; ok {
if val.Short != "" {
return val.Short
} else if val.Name != "" {
return val.Name
} else if val.Notify != "" {
return val.Notify
}
}
return strings.TrimSuffix(id, CONTACTSUFFIX)
}
// get a string representation of all messages for contact
func (sm *SessionManager) getMessagesString(wid string) (string, []string) {
out := ""
ids := []string{}
// get all messages for one chat id
func (sm *SessionManager) getMessages(wid string) []Message {
msgs := sm.db.GetMessages(wid)
ids := []Message{}
for _, msg := range msgs {
out += sm.getTextMessageString(&msg)
out += "\n"
ids = append(ids, msg.Info.Id)
ids = append(ids, sm.createMessage(&msg))
}
return out, ids
return ids
}
// create a formatted string with regions based on message ID from a text message
// TODO: move message styling into UI
func (sm *SessionManager) getTextMessageString(msg *whatsapp.TextMessage) string {
colorMe := config.Config.Colors.ChatMe
colorContact := config.Config.Colors.ChatContact
out := ""
text := tview.Escape(msg.Text)
tim := time.Unix(int64(msg.Info.Timestamp), 0)
time := tim.Format("02-01-06 15:04:05")
out += "[\""
out += msg.Info.Id
out += "\"]"
if msg.Info.FromMe { //msg from me
out += "[-::d](" + time + ") [" + colorMe + "::b]Me: [-::-]" + text
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) { // group msg
userId := msg.Info.SenderJid
out += "[-::d](" + time + ") [" + colorContact + "::b]" + sm.GetIdShort(userId) + ": [-::-]" + text
} else { // message from others
out += "[-::d](" + time + ") [" + colorContact + "::b]" + sm.GetIdShort(msg.Info.RemoteJid) + ": [-::-]" + text
// create internal message from whatsapp message
// TODO: store these instead of generating each time
func (sm *SessionManager) createMessage(msg *whatsapp.TextMessage) Message {
newMsg := Message{}
newMsg.Id = msg.Info.Id
newMsg.ChatId = msg.Info.RemoteJid
newMsg.FromMe = msg.Info.FromMe
newMsg.Timestamp = msg.Info.Timestamp
newMsg.Text = msg.Text
if strings.Contains(msg.Info.RemoteJid, STATUSSUFFIX) {
newMsg.ContactId = msg.Info.SenderJid
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
} else if strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX) {
newMsg.ContactId = msg.Info.SenderJid
newMsg.ContactName = sm.db.GetIdName(msg.Info.SenderJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.SenderJid)
} else {
newMsg.ContactId = msg.Info.RemoteJid
newMsg.ContactName = sm.db.GetIdName(msg.Info.RemoteJid)
newMsg.ContactShort = sm.db.GetIdShort(msg.Info.RemoteJid)
}
out += "[\"\"]"
return out
return newMsg
}
// load data for message specified by message id TODO: support types
@@ -639,7 +815,7 @@ func (sm *SessionManager) sendText(wid string, text string) {
} else {
sm.db.AddTextMessage(&msg)
if sm.currentReceiver == wid {
sm.uiHandler.NewMessage(sm.getTextMessageString(&msg), msg.Info.Id)
sm.uiHandler.NewMessage(sm.createMessage(&msg))
}
}
}
@@ -648,7 +824,6 @@ func (sm *SessionManager) sendText(wid string, text string) {
// HandleError implements the error handler interface for go-whatsapp
func (sm *SessionManager) HandleError(err error) {
sm.uiHandler.PrintText("[" + config.Config.Colors.Negative + "]go-whatsapp reported an error:[-]")
sm.uiHandler.PrintError(err)
statusMsg := StatusMsg{false, err}
sm.StatusChannel <- statusMsg
@@ -724,7 +899,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
@@ -732,6 +907,22 @@ func (sm *SessionManager) HandleBatteryMessage(msg whatsapp.BatteryMessage) {
sm.BatteryChannel <- BatteryMsg{msg.Percentage, msg.Plugged, msg.Powersave}
}
func (sm *SessionManager) HandleContactList(contacts []whatsapp.Contact) {
for _, c := range contacts {
sm.ContactChannel <- c
}
}
func (sm *SessionManager) HandleChatList(chats []whatsapp.Chat) {
for _, c := range chats {
sm.ChatChannel <- c
}
}
func (sm *SessionManager) HandleJsonMessage(message string) {
//sm.uiHandler.PrintText(message)
}
// helper to save an attachment and open it if specified
func saveAttachment(data []byte, path string) (string, error) {
err := ioutil.WriteFile(path, data, 0644)

View File

@@ -2,6 +2,7 @@ package messages
import (
"sort"
"strings"
"github.com/Rhymen/go-whatsapp"
)
@@ -11,6 +12,8 @@ type MessageDatabase struct {
messagesById map[string]*whatsapp.TextMessage // text messages stored by message ID
latestMessage map[string]uint64 // last message from RemoteJid
otherMessages map[string]*interface{} // other non-text messages, stored by ID
contacts map[string]Contact
chats map[string]Chat
}
// initialize the database
@@ -20,6 +23,8 @@ func (db *MessageDatabase) Init() {
db.messagesById = make(map[string]*whatsapp.TextMessage)
db.otherMessages = make(map[string]*interface{})
db.latestMessage = make(map[string]uint64)
db.contacts = make(map[string]Contact)
db.chats = make(map[string]Chat)
}
// add a text message to the database, stored by RemoteJid
@@ -36,6 +41,18 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
db.latestMessage[wid] = msg.Info.Timestamp
didNew = true
}
//do we know this chat? if not add
if _, ok := db.chats[msg.Info.RemoteJid]; !ok {
//don't have this chat!
isGroup := strings.Contains(msg.Info.RemoteJid, GROUPSUFFIX)
db.chats[msg.Info.RemoteJid] = Chat{
msg.Info.RemoteJid,
isGroup,
db.GetIdName(msg.Info.RemoteJid),
1,
int64(msg.Info.Timestamp),
}
}
//check if message exists, ignore otherwise
if _, ok := db.messagesById[msg.Info.Id]; !ok {
db.messagesById[msg.Info.Id] = msg
@@ -47,6 +64,13 @@ func (db *MessageDatabase) AddTextMessage(msg *whatsapp.TextMessage) bool {
return didNew
}
func (db *MessageDatabase) NewUnreadChat(id string) {
if chat, ok := db.chats[id]; ok {
chat.Unread++
db.chats[id] = chat
}
}
// add audio/video/image/doc message, stored by message id
func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
var id = ""
@@ -66,21 +90,53 @@ func (db *MessageDatabase) AddOtherMessage(msg *interface{}) {
}
}
func (db *MessageDatabase) AddContact(contact Contact) {
db.contacts[contact.Id] = contact
}
func (db *MessageDatabase) AddChat(chat Chat) {
db.chats[chat.Id] = chat
}
// get an array of all chat ids
func (db *MessageDatabase) GetContactIds() []string {
//var this = *db
keys := make([]string, len(db.textMessages))
func (db *MessageDatabase) GetChatIds() []Chat {
keys := make([]Chat, len(db.chats))
i := 0
for k := range db.textMessages {
for _, k := range db.chats {
keys[i] = k
i++
}
sort.Slice(keys, func(i, j int) bool {
return db.latestMessage[keys[i]] > db.latestMessage[keys[j]]
return db.latestMessage[keys[i].Id] > db.latestMessage[keys[j].Id]
})
return keys
}
// gets a pretty name for a whatsapp id
func (sm *MessageDatabase) GetIdName(id string) string {
if val, ok := sm.contacts[id]; ok {
if val.Name != "" {
return val.Name
} else if val.Short != "" {
return val.Short
}
}
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
// gets a short name for a whatsapp id
func (sm *MessageDatabase) GetIdShort(id string) string {
if val, ok := sm.contacts[id]; ok {
//TODO val.notify from whatsapp??
if val.Short != "" {
return val.Short
} else if val.Name != "" {
return val.Name
}
}
return strings.TrimSuffix(strings.TrimSuffix(id, CONTACTSUFFIX), GROUPSUFFIX)
}
func (db *MessageDatabase) GetMessageInfo(id string) string {
if _, ok := db.otherMessages[id]; ok {
return "[yellow]OtherMessage[-]"