From 3bccb1cf6186d899e7bf7c2c45d6f195df6e4bb1 Mon Sep 17 00:00:00 2001 From: Hugo Thunnissen Date: Tue, 7 Jan 2020 17:55:48 +0100 Subject: [PATCH] Prevent sending duplicate messages and improve username determination --- bridge_context.go | 29 +++++++++--------------- database.go | 40 +++++++++++++++++++++++++++++++++ deltachat.go | 13 +++++++++++ main.go | 11 ++++++++- message_handlers.go | 26 +++++++++++++++++++-- message_tracker.go | 55 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 message_tracker.go diff --git a/bridge_context.go b/bridge_context.go index b466bd4..7bd48b8 100644 --- a/bridge_context.go +++ b/bridge_context.go @@ -6,32 +6,25 @@ import ( ) type BridgeContext struct { - WhappConn *whatsapp.Conn - DCContext *deltachat.Context - DB *Database - DCUserID uint32 - DCUserChatID uint32 + WhappConn *whatsapp.Conn + DCContext *deltachat.Context + DB *Database + MessageTracker *MessageTracker + DCUserID uint32 + DCUserChatID uint32 } // Find or create a deltachat verified group chat for a whatsapp JID and return it's ID. -func (b *BridgeContext) GetOrCreateDCIDForJID(JID string, isGroup bool) (uint32, error) { +func (b *BridgeContext) GetOrCreateDCIDForJID(JID string) (uint32, error) { if DCID, _ := b.DB.GetDCIDForWhappJID(JID); DCID != nil { return *DCID, nil } chatName := JID - if isGroup { - chat, ok := b.WhappConn.Store.Chats[JID] - - if ok { - chatName = chat.Name - } - } else { - contact, ok := b.WhappConn.Store.Contacts[JID] - - if ok { - chatName = contact.Name - } + chat, ok := b.WhappConn.Store.Chats[JID] + + if ok { + chatName = chat.Name } DCID := b.DCContext.CreateGroupChat(true, chatName) diff --git a/database.go b/database.go index a8a8e02..79d1b44 100644 --- a/database.go +++ b/database.go @@ -15,12 +15,14 @@ const ( JID_TO_DCID_INT uint8 = iota DCID_TO_JID_INT KEY_VALUE_INT + ID_WAS_SENT_INT ) var ( JID_TO_DCID = []byte{JID_TO_DCID_INT} DCID_TO_JID = []byte{DCID_TO_JID_INT} KEY_VALUE = []byte{KEY_VALUE_INT} + ID_WAS_SENT = []byte{ID_WAS_SENT_INT} ) func (d *Database) Init() error { @@ -42,6 +44,12 @@ func (d *Database) Init() error { return err } + _, err = tx.CreateBucketIfNotExists(ID_WAS_SENT) + + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(KEY_VALUE) return err @@ -136,3 +144,35 @@ func (d *Database) Get(key []byte) []byte { return value } + +func (d *Database) MarkWhappMessagesSent(IDs []*string) error { + return d.db.Update(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(ID_WAS_SENT) + + for _, ID := range IDs { + if ID != nil { + bucket.Put([]byte(*ID), []byte{uint8(1)}) + } + } + + return nil + }) +} + +func (d *Database) WhappMessageWasSent(ID string) (bool, error) { + var wasSent bool = false + + err := d.db.View(func(tx *bbolt.Tx) error { + bucket := tx.Bucket(ID_WAS_SENT) + + rawWasSent := bucket.Get([]byte(ID)) + + if len(rawWasSent) > 0 && uint8(rawWasSent[0]) == uint8(1) { + wasSent = true + } + + return nil + }) + + return wasSent, err +} diff --git a/deltachat.go b/deltachat.go index b69454d..3d5aa74 100644 --- a/deltachat.go +++ b/deltachat.go @@ -43,13 +43,26 @@ func DcClientFromConfig(databasePath string, config map[string]string) *deltacha return client } +// Note: this manipulates the BridgeContext. func BootstrapDcClientFromConfig(config Config, ctx *BridgeContext) (*deltachat.Client, error) { dcClient := DcClientFromConfig(config.App.DataFolder+"/deltachat.db", config.Deltachat) DCCtx := dcClient.Context() + + log.Println("Waiting for deltachat client to be configured") + for !DCCtx.IsConfigured() { + } + userName := "user" dcUserID := DCCtx.CreateContact(&userName, &config.App.UserAddress) + // Send a message in a 1:1 chat first, this will let the user's client know that the + // crypto setup has changed if it has + DCCtx.SendTextMessage( + DCCtx.CreateChatByContactID(dcUserID), + "Hi, Whapp-Deltachat is initiallizing", + ) + userChatIDRaw := ctx.DB.Get([]byte("user-chat-id")) var ( userChatID uint32 diff --git a/main.go b/main.go index 839b788..6d8cd4f 100644 --- a/main.go +++ b/main.go @@ -36,16 +36,25 @@ func main() { err = db.Init() + messageTracker := &MessageTracker{ + DB: db, + } + + defer messageTracker.Flush() + if err != nil { log.Fatal(err) } bridgeCtx := &BridgeContext{ - DB: db, + DB: db, + MessageTracker: messageTracker, } dcClient, err := BootstrapDcClientFromConfig(*config, bridgeCtx) + bridgeCtx.SendLog("Whapp-Deltachat started.") + if err != nil { log.Fatal(err) } diff --git a/message_handlers.go b/message_handlers.go index f0ea219..0a85938 100644 --- a/message_handlers.go +++ b/message_handlers.go @@ -18,7 +18,19 @@ func MakeTextMessageAction(b *BridgeContext, m whatsapp.TextMessage) MessageActi return func() error { JID := m.Info.RemoteJid - DCID, err := b.GetOrCreateDCIDForJID(JID, m.Info.RemoteJid != m.Info.SenderJid) + wasSent, err := b.MessageTracker.WasSent(m.Info.Id) + + if err != nil { + log.Println(err) + b.SendLog(err.Error()) + } + + // Messgae has already been sent + if wasSent == true { + return nil + } + + DCID, err := b.GetOrCreateDCIDForJID(JID) if err != nil { log.Println(err) @@ -26,6 +38,16 @@ func MakeTextMessageAction(b *BridgeContext, m whatsapp.TextMessage) MessageActi } senderName := m.Info.Source.GetParticipant() + + // No participant probably means that this isn't a group chat. + if senderName == "" { + senderName = m.Info.RemoteJid + } + + if m.Info.FromMe == true { + senderName = b.DCContext.GetContact(b.DCUserID).GetDisplayName() + } + contact, ok := b.WhappConn.Store.Contacts[senderName] if ok { senderName = contact.Name @@ -36,6 +58,6 @@ func MakeTextMessageAction(b *BridgeContext, m whatsapp.TextMessage) MessageActi fmt.Sprintf("%s:\n%s", senderName, m.Text), ) - return nil + return b.MessageTracker.MarkSent(&JID) } } diff --git a/message_tracker.go b/message_tracker.go new file mode 100644 index 0000000..c58778f --- /dev/null +++ b/message_tracker.go @@ -0,0 +1,55 @@ +package main + +import ( + "sync" +) + +// MessageTracker will keep track of encountered whatsapp messages to prevent sending them +// twice. It's storage is buffered to prevent continuous locks on the database. This means +// that calling WasSent immediately after calling MarkSent will most likely not return an +// up to date answer. +type MessageTracker struct { + DB *Database + delivered [80]*string + deliveredMutex sync.RWMutex + deliveredIdx int +} + +func (t *MessageTracker) MarkSent(ID *string) error { + t.deliveredMutex.Lock() + defer t.deliveredMutex.Unlock() + + t.delivered[t.deliveredIdx] = ID + + if t.deliveredIdx == len(t.delivered)-1 { + err := t.flush() + + if err != nil { + return err + } + } + + t.deliveredIdx += 1 + + return nil +} + +// Flush without lock +func (t *MessageTracker) flush() error { + err := t.DB.MarkWhappMessagesSent(t.delivered[:]) + t.deliveredIdx = 0 + + return err +} + +// Flush with lock +func (t *MessageTracker) Flush() error { + t.deliveredMutex.Lock() + defer t.deliveredMutex.Unlock() + + return t.flush() +} + +func (t *MessageTracker) WasSent(ID string) (bool, error) { + return t.DB.WhappMessageWasSent(ID) +}