diff --git a/botcommands/whapp-bridge.go b/botcommands/whapp-bridge.go new file mode 100644 index 0000000..6ce0031 --- /dev/null +++ b/botcommands/whapp-bridge.go @@ -0,0 +1,84 @@ +package botcommands + +import ( + "fmt" + "log" + + "github.com/Rhymen/go-whatsapp" + "github.com/hugot/go-deltachat/deltachat" +) + +type Database interface { + GetWhappJIDForDCID(DCID uint32) (*string, error) +} + +func NewWhappBridge( + wac *whatsapp.Conn, + db Database, + UserChatID uint32, +) *WhappBridge { + return &WhappBridge{ + wac: wac, + db: db, + UserChatID: UserChatID, + } +} + +type WhappBridge struct { + wac *whatsapp.Conn + db Database + UserChatID uint32 +} + +func (b *WhappBridge) Accepts(c *deltachat.Chat, m *deltachat.Message) bool { + chatID := c.GetID() + + chatJID, err := b.db.GetWhappJIDForDCID(chatID) + + if err != nil { + // The database is failing, time to die :( + log.Fatal(err) + } + + return chatJID != nil +} + +func (b *WhappBridge) Execute( + c *deltachat.Context, + chat *deltachat.Chat, + m *deltachat.Message, +) { + JID, err := b.db.GetWhappJIDForDCID(chat.GetID()) + + if err != nil { + c.SendTextMessage( + b.UserChatID, + fmt.Sprintf( + "Whapp bridge dying: %s", + err.Error(), + ), + ) + + log.Fatal(err) + } + + text := whatsapp.TextMessage{ + Info: whatsapp.MessageInfo{ + RemoteJid: *JID, + }, + Text: m.GetText(), + } + + _, err = b.wac.Send(text) + + if err != nil { + c.SendTextMessage( + b.UserChatID, + fmt.Sprintf( + "Error sending message to %s. Message contents: %s", + JID, + m.GetText(), + ), + ) + } +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..a8a8e02 --- /dev/null +++ b/database.go @@ -0,0 +1,138 @@ +package main + +import ( + "encoding/binary" + + "go.etcd.io/bbolt" +) + +type Database struct { + db *bbolt.DB + dbPath string +} + +const ( + JID_TO_DCID_INT uint8 = iota + DCID_TO_JID_INT + KEY_VALUE_INT +) + +var ( + JID_TO_DCID = []byte{JID_TO_DCID_INT} + DCID_TO_JID = []byte{DCID_TO_JID_INT} + KEY_VALUE = []byte{KEY_VALUE_INT} +) + +func (d *Database) Init() error { + db, err := bbolt.Open(d.dbPath, 0600, nil) + + if err != nil { + return err + } + + err = db.Update(func(tx *bbolt.Tx) error { + _, err = tx.CreateBucketIfNotExists(JID_TO_DCID) + if err != nil { + return err + } + + _, err = tx.CreateBucketIfNotExists(DCID_TO_JID) + + if err != nil { + return err + } + + _, err = tx.CreateBucketIfNotExists(KEY_VALUE) + + return err + }) + + d.db = db + + return nil +} + +func (d *Database) GetDCIDForWhappJID(JID string) (*uint32, error) { + var DCID *uint32 + + err := d.db.View(func(tx *bbolt.Tx) error { + rawDCID := tx.Bucket(JID_TO_DCID).Get([]byte(JID)) + + if rawDCID == nil { + DCID = nil + } else { + i := binary.LittleEndian.Uint32(rawDCID) + DCID = &i + } + + return nil + }) + + return DCID, err +} + +func (d *Database) GetWhappJIDForDCID(DCID uint32) (*string, error) { + var JID *string + + rawDCID := make([]byte, 4) + binary.LittleEndian.PutUint32(rawDCID, DCID) + + err := d.db.View(func(tx *bbolt.Tx) error { + + rawJID := tx.Bucket(DCID_TO_JID).Get(rawDCID) + + if rawJID == nil { + JID = nil + } else { + str := string(rawJID) + JID = &str + } + + return nil + }) + + return JID, err +} + +func (d *Database) StoreDCIDForJID(JID string, DCID uint32) error { + err := d.db.Update(func(tx *bbolt.Tx) error { + + DCIDbs := make([]byte, 4) + + binary.LittleEndian.PutUint32(DCIDbs, DCID) + + err := tx.Bucket(JID_TO_DCID).Put([]byte(JID), DCIDbs) + + if err != nil { + return err + } + + err = tx.Bucket(DCID_TO_JID).Put(DCIDbs, []byte(JID)) + + return err + }) + + return err +} + +func (d *Database) Put(key []byte, value []byte) error { + err := d.db.Update(func(tx *bbolt.Tx) error { + err := tx.Bucket(KEY_VALUE).Put(key, value) + + return err + }) + + return err +} + +func (d *Database) Get(key []byte) []byte { + var value []byte + + d.db.View(func(tx *bbolt.Tx) error { + value = tx.Bucket(KEY_VALUE).Get(key) + + return nil + }) + + return value +} diff --git a/deltachat.go b/deltachat.go index 85a66b6..bad2889 100644 --- a/deltachat.go +++ b/deltachat.go @@ -1,12 +1,13 @@ package main import ( + "errors" "fmt" "log" + "os" - "github.com/hugot/go-deltachat/deltabot" "github.com/hugot/go-deltachat/deltachat" - "github.com/hugot/whapp-deltachat/botcommands" + "github.com/mdp/qrterminal" ) func DcClientFromConfig(databasePath string, config map[string]string) *deltachat.Client { @@ -51,11 +52,75 @@ func BootstrapDcClientFromConfig(config Config) *deltachat.Client { context.SendTextMessage(context.CreateChatByContactID(userID), "Whapp-Deltachat initialized") - bot := &deltabot.Bot{} + return dcClient +} - bot.AddCommand(&botcommands.Echo{}) +// Add a user as verified contact to the deltachat context. This is necessary to be able +// to create verified groups. +func AddUserAsVerifiedContact(dcUserID uint32, client *deltachat.Client) (uint32, error) { + confirmChan := make(chan bool) - dcClient.On(deltachat.DC_EVENT_INCOMING_MSG, bot.HandleMessage) + client.On( + deltachat.DC_EVENT_SECUREJOIN_INVITER_PROGRESS, + func(c *deltachat.Context, e *deltachat.Event) { + contactIDInt, err := e.Data1.Int() - return dcClient + if err != nil { + log.Println(err) + + // Something weird is going on here, deltachat is not passing expected + // values. Make the join process fail. + confirmChan <- false + return + } + + contactID := uint32(*contactIDInt) + + if contactID != dcUserID { + log.Println( + fmt.Sprintf( + "Unexpected contact ID encountered for securejoin progress: %v", + contactID, + ), + ) + + return + } + + progress, err := e.Data2.Int() + + if err != nil { + log.Println(err) + + confirmChan <- false + return + + } + + if *progress == 1000 { + confirmChan <- true + } + }, + ) + + ctx := client.Context() + + chatID := ctx.CreateGroupChat(true, "Whapp-DC ***status***") + + log.Println("Scan this qr code with your DC client") + qrterminal.Generate( + ctx.GetSecurejoinQR(chatID), + qrterminal.L, + os.Stdout, + ) + + success := <-confirmChan + + if !success { + return chatID, errors.New("Contact Verification process failed") + } + + log.Println("Securejoin verification completed") + + return chatID, nil } diff --git a/go.mod b/go.mod index b3b0991..1b30856 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,22 @@ module github.com/hugot/whapp-deltachat go 1.13 require ( + 9fans.net/go v0.0.2 // indirect + github.com/Rhymen/go-whatsapp v0.1.0 + github.com/dougm/goflymake v0.0.0-20140731161037-3b9634ef394a // indirect + github.com/golang/protobuf v1.3.2 // indirect + github.com/gorilla/websocket v1.4.1 // indirect github.com/hugot/go-deltachat v0.0.0-20200103100028-e93f1f3d8b97 + github.com/mdp/qrterminal v1.0.1 + github.com/mdp/qrterminal/v3 v3.0.0 // indirect + github.com/nsf/gocode v0.0.0-20190302080247-5bee97b48836 // indirect + github.com/rogpeppe/godef v1.1.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 + go.etcd.io/bbolt v1.3.3 + golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 // indirect + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect + golang.org/x/tools v0.0.0-20200103211127-7bda30096dc1 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 542d234..8847d66 100644 --- a/go.sum +++ b/go.sum @@ -1,25 +1,64 @@ +9fans.net/go v0.0.0-20181112161441-237454027057 h1:OcHlKWkAMJEF1ndWLGxp5dnJQkYM/YImUOvsBoz6h5E= +9fans.net/go v0.0.0-20181112161441-237454027057/go.mod h1:diCsxrliIURU9xsYtjCp5AbpQKqdhKmf0ujWDUSkfoY= +9fans.net/go v0.0.2 h1:RYM6lWITV8oADrwLfdzxmt8ucfW6UtP9v1jg4qAbqts= +9fans.net/go v0.0.2/go.mod h1:lfPdxjq9v8pVQXUMBCx5EO5oLXWQFlKRQgs1kEkjoIM= +github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f/go.mod h1:4a58ifQTEe2uwwsaqbh3i2un5/CBPg+At/qHpt18Tmk= github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA= +github.com/Rhymen/go-whatsapp v0.1.0 h1:XTXhFIQ/fx9jKObUnUX2Q+nh58EyeHNhX7DniE8xeuA= +github.com/Rhymen/go-whatsapp v0.1.0/go.mod h1:xJSy+okeRjKkQEH/lEYrnekXB3PG33fqL0I6ncAkV50= +github.com/Rhymen/go-whatsapp/examples/echo v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:zgCiQtBtZ4P4gFWvwl9aashsdwOcbb/EHOGRmSzM8ME= +github.com/Rhymen/go-whatsapp/examples/restoreSession v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:5sCUSpG616ZoSJhlt9iBNI/KXBqrVLcNUJqg7J9+8pU= +github.com/Rhymen/go-whatsapp/examples/sendImage v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:RdiyhanVEGXTam+mZ3k6Y3VDCCvXYCwReOoxGozqhHw= +github.com/Rhymen/go-whatsapp/examples/sendTextMessages v0.0.0-20190325075644-cc2581bbf24d/go.mod h1:suwzklatySS3Q0+NCxCDh5hYfgXdQUWU1DNcxwAxStM= github.com/cheggaaa/pb v1.0.25/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dougm/goflymake v0.0.0-20140731161037-3b9634ef394a h1:S4+OSVOtl5a40p2ZgV7rA6YLl0wcjehR2jQVyWK1/UE= +github.com/dougm/goflymake v0.0.0-20140731161037-3b9634ef394a/go.mod h1:5K1XZIXX56t7gg4jDBoVXBH9ABugPa830vLJiD/XPHI= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/kvannotten/pcd v0.6.1/go.mod h1:7T/uPMn1rNucUEPEWtuwJIOBjqB+GBYg+tS5aEURXXs= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= +github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= +github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= +github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nsf/gocode v0.0.0-20190302080247-5bee97b48836 h1:oc3CL18CoGhyOQJ7HDa9gJAde33bwI8Vi28zLdIzJVc= +github.com/nsf/gocode v0.0.0-20190302080247-5bee97b48836/go.mod h1:6Q8/OMaaKAgTX7/jt2bOXVDrm1eJhoNd+iwzghR7jvs= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/godef v1.1.1 h1:NujOtt9q9vIClRTB3sCZpavac+NMRaIayzrcz1h4fSE= +github.com/rogpeppe/godef v1.1.1/go.mod h1:oEo1eMy1VUEHUzUIX4F7IqvMJRiz9UId44mvnR8oPlQ= +github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 h1:lpEzuenPuO1XNTeikEmvqYFcU37GVLl8SRNblzyvGBE= +github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= +github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/spf13/afero v1.1.1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= @@ -31,13 +70,43 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181130195746-895048a75ecf/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20200103211127-7bda30096dc1 h1:NdJXeSjUw9JgihCnQeXjdsCFXq0n4I0Kv8wrEiQYZXU= +golang.org/x/tools v0.0.0-20200103211127-7bda30096dc1/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/main.go b/main.go index ba1c91d..68979a9 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,16 @@ package main import ( + "encoding/binary" "fmt" "log" "os" "os/signal" + + "github.com/Rhymen/go-whatsapp" + "github.com/hugot/go-deltachat/deltabot" + "github.com/hugot/go-deltachat/deltachat" + "github.com/hugot/whapp-deltachat/botcommands" ) func main() { @@ -23,13 +29,82 @@ func main() { log.Fatal(err) } - err = ensureDirectory(config.App.DataFolder) + ensureDirectoryOrDie(config.App.DataFolder) + ensureDirectoryOrDie(config.App.DataFolder + "/tmp") + + dcClient := BootstrapDcClientFromConfig(*config) + + // Give dc an opportunity to perform some close-down logic + // and close it's db etc. + defer dcClient.Close() + + db := &Database{ + dbPath: config.App.DataFolder + "/app.db", + } + + err = db.Init() if err != nil { log.Fatal(err) } - dcClient := BootstrapDcClientFromConfig(*config) + ctx := dcClient.Context() + userName := "user" + dcUserID := ctx.CreateContact(&userName, &config.App.UserAddress) + + userChatIDRaw := db.Get([]byte("user-chat-id")) + var userChatID uint32 + + if userChatIDRaw == nil { + userChatID, err = AddUserAsVerifiedContact(dcUserID, dcClient) + if err != nil { + log.Fatal(err) + } + } else { + userChatID = binary.LittleEndian.Uint32(userChatIDRaw) + } + + userChatIDbs := make([]byte, 4) + binary.LittleEndian.PutUint32(userChatIDbs, userChatID) + err = db.Put([]byte("user-chat-id"), userChatIDbs) + + if err != nil { + log.Fatal(err) + } + + var wac *whatsapp.Conn + + for i := 0; i < 10; i++ { + wac, err = CreateAndLoginWhappConnection( + config.App.DataFolder, + dcClient.Context(), + dcUserID, + ) + + if err == nil { + break + } + } + + if err != nil { + log.Fatal(err) + } + + wac.AddHandler(&WhappHandler{ + dcContext: dcClient.Context(), + db: db, + dcUserID: dcUserID, + wac: wac, + }) + + bot := &deltabot.Bot{} + + bot.AddCommand(&botcommands.Echo{}) + bot.AddCommand(botcommands.NewWhappBridge( + wac, db, dcClient.Context().GetChatIDByContactID(dcUserID), + )) + + dcClient.On(deltachat.DC_EVENT_INCOMING_MSG, bot.HandleMessage) wait := make(chan os.Signal, 1) signal.Notify(wait, os.Interrupt) @@ -38,10 +113,6 @@ func main() { select { case sig := <-wait: log.Println(sig) - - // Give dc an opportunity to perform some close-down logic - // and close it's db etc. - dcClient.Close() return } } @@ -64,3 +135,11 @@ func ensureDirectory(dir string) error { return nil } + +func ensureDirectoryOrDie(dir string) { + err := ensureDirectory(dir) + + if err != nil { + log.Fatal(err) + } +} diff --git a/whapp-handler.go b/whapp-handler.go new file mode 100644 index 0000000..d7d354c --- /dev/null +++ b/whapp-handler.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "log" + + "github.com/Rhymen/go-whatsapp" + "github.com/hugot/go-deltachat/deltachat" +) + +type WhappHandler struct { + dcContext *deltachat.Context + db *Database + dcUserID uint32 + wac *whatsapp.Conn +} + +// Find or create a deltachat verified group chat for a whatsapp JID and return it's ID. +func (h *WhappHandler) getOrCreateDCIDForJID(JID string, isGroup bool) (uint32, error) { + if DCID, _ := h.db.GetDCIDForWhappJID(JID); DCID != nil { + return *DCID, nil + } + + chatName := JID + if isGroup { + chat, ok := h.wac.Store.Chats[JID] + + if ok { + chatName = chat.Name + } + } else { + contact, ok := h.wac.Store.Contacts[JID] + + if ok { + chatName = contact.Name + } + } + + DCID := h.dcContext.CreateGroupChat(true, chatName) + + err := h.db.StoreDCIDForJID(JID, DCID) + + if err != nil { + return DCID, err + } + + h.dcContext.AddContactToChat(DCID, h.dcUserID) + + return DCID, err +} + +func (h *WhappHandler) HandleError(err error) { + log.Println("Whatsapp Error: " + err.Error()) +} + +func (h *WhappHandler) HandleTextMessage(m whatsapp.TextMessage) { + JID := m.Info.RemoteJid + + DCID, err := h.getOrCreateDCIDForJID(JID, m.Info.RemoteJid != m.Info.SenderJid) + + if err != nil { + log.Println(err) + + chatID := h.dcContext.GetChatIDByContactID(h.dcUserID) + h.dcContext.SendTextMessage( + chatID, + err.Error(), + ) + } + + senderName := m.Info.SenderJid + contact, ok := h.wac.Store.Contacts[m.Info.SenderJid] + if ok { + senderName = contact.Name + } + + h.dcContext.SendTextMessage( + DCID, + fmt.Sprintf("%s:\n%s", senderName, m.Text), + ) +} diff --git a/whapp.go b/whapp.go new file mode 100644 index 0000000..31218bd --- /dev/null +++ b/whapp.go @@ -0,0 +1,111 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "log" + "mime" + "os" + "time" + + "github.com/Rhymen/go-whatsapp" + "github.com/hugot/go-deltachat/deltachat" + "github.com/skip2/go-qrcode" +) + +func CreateAndLoginWhappConnection( + storageDir string, + dcContext *deltachat.Context, + dcUserID uint32, +) (*whatsapp.Conn, error) { + wac, err := whatsapp.NewConn(20 * time.Second) + + if err != nil { + return wac, err + } + + var session whatsapp.Session + + sessionFile := storageDir + "/whapp-session.json" + if _, err := os.Stat(sessionFile); os.IsNotExist(err) { + session, err = WhappQrLogin(storageDir, wac, dcContext, dcUserID) + + if err != nil { + return wac, err + } + } else { + session = whatsapp.Session{} + + sessionJson, err := ioutil.ReadFile(sessionFile) + + err = json.Unmarshal(sessionJson, &session) + + if err != nil { + return wac, err + } + + session, err = wac.RestoreWithSession(session) + + if err != nil { + return wac, err + } + } + + err = StoreWhappSession(session, storageDir) + + return wac, err +} + +func WhappQrLogin( + storageDir string, + wac *whatsapp.Conn, + dcContext *deltachat.Context, + dcUserID uint32, +) (whatsapp.Session, error) { + qrChan := make(chan string) + + go func() { + qrCode := <-qrChan + + tmpFile, err := ioutil.TempFile(storageDir+"/tmp", "qr") + + if err != nil { + log.Fatal(err) + } + + err = qrcode.WriteFile(qrCode, qrcode.Medium, 256, tmpFile.Name()) + + if err != nil { + log.Fatal(err) + } + + message := dcContext.NewMessage(deltachat.DC_MSG_IMAGE) + + log.Println("MIME: " + mime.TypeByExtension("png")) + + message.SetFile(tmpFile.Name(), "image/png") + + message.SetText("Scan this QR code from whatsapp") + + dcContext.SendMessage( + dcContext.GetChatIDByContactID(dcUserID), + message, + ) + }() + + session, err := wac.Login(qrChan) + + return session, err +} + +func StoreWhappSession(session whatsapp.Session, storageDir string) error { + sessionJson, err := json.Marshal(session) + + if err != nil { + return err + } + + err = ioutil.WriteFile(storageDir+"/whapp-session.json", sessionJson, 0600) + + return err +}