From 7cb9983da8b8d5c9285cd1067c5920aa02900a96 Mon Sep 17 00:00:00 2001 From: Karmanyaah Malhotra Date: Sun, 21 Feb 2021 00:58:50 -0500 Subject: [PATCH] super basic --- commands.go | 69 +- database/database.go | 15 + database/message.go | 24 +- database/portal.go | 17 +- database/puppet.go | 10 +- database/statestore.go | 13 +- database/user.go | 293 ++++--- groupme-ext/client.go | 3 - groupmeExt/client.go | 52 ++ {groupme-ext => groupmeExt}/message.go | 0 groupmeExt/user.go | 6 + main.go | 4 +- matrix.go | 1 + portal.go | 1000 ++++++++++++------------ puppet.go | 3 +- types/types.go | 2 + user.go | 379 ++++----- 17 files changed, 965 insertions(+), 926 deletions(-) delete mode 100644 groupme-ext/client.go create mode 100644 groupmeExt/client.go rename {groupme-ext => groupmeExt}/message.go (100%) create mode 100644 groupmeExt/user.go diff --git a/commands.go b/commands.go index e5f1b55..3052533 100644 --- a/commands.go +++ b/commands.go @@ -20,13 +20,13 @@ import ( // "errors" "context" "fmt" + // "math" - "sort" + "strconv" "strings" "github.com/Rhymen/go-whatsapp" - "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -34,9 +34,6 @@ import ( "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" "maunium.net/go/mautrix/id" - - // "maunium.net/go/mautrix-whatsapp/database" - "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) type CommandHandler struct { @@ -382,10 +379,10 @@ const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client` // CommandLogin handles login command func (handler *CommandHandler) CommandLogin(ce *CommandEvent) { - if !ce.User.Connect(true) { - ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") - return - } + // if !ce.User.Connect(true) { + // ce.User.log.Debugln("Connect() returned false, assuming error was logged elsewhere and canceling login.") + // return + // } ce.User.Login(ce) } @@ -418,8 +415,13 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { // ce.User.log.Warnln("Error while disconnecting after logout:", err) // } // ce.User.Conn.RemoveHandlers() - // ce.User.Conn = nil - // ce.User.removeFromJIDMap() + ce.User.Conn.Stop(context.TODO()) + ce.User.Conn = nil + ce.User.removeFromJIDMap() + ce.User.Token = "" + ce.User.JID = "" + ce.User.Client = nil + ce.User.Update() // // TODO this causes a foreign key violation, which should be fixed // //ce.User.JID = "" // ce.User.SetSession(nil) @@ -489,15 +491,16 @@ func (handler *CommandHandler) CommandDeleteSession(ce *CommandEvent) { const cmdReconnectHelp = `reconnect - Reconnect to WhatsApp` func (handler *CommandHandler) CommandReconnect(ce *CommandEvent) { - // if ce.User.Conn == nil { - // if ce.User.Session == nil { - // ce.Reply("No existing connection and no session. Did you mean `login`?") - // } else { - // ce.Reply("No existing connection, creating one...") - // ce.User.Connect(false) - // } - // return - // } + fmt.Println(ce.User.Conn) + if ce.User.Conn == nil { + if len(ce.User.Token) == 0 { + ce.Reply("No existing connection and no token. Did you mean `login`?") + } else { + ce.Reply("No existing connection, creating one...") + ce.User.Connect() + } + return + } // wasConnected := true // sess, err := ce.User.Conn.Disconnect() @@ -581,7 +584,7 @@ func (handler *CommandHandler) CommandDisconnect(ce *CommandEvent) { ce.Reply("You don't have a WhatsApp connection.") return } - ce.User.Conn.Stop(context.TODO()) + ce.User.Conn.Stop(context.TODO()) // if err == whatsapp.ErrNotConnected { // ce.Reply("You were not connected.") // return @@ -750,18 +753,18 @@ func (handler *CommandHandler) CommandDeleteAllPortals(ce *CommandEvent) { const cmdListHelp = `list [page] [items per page] - Get a list of all contacts and groups.` func formatContacts(contacts bool, input map[string]whatsapp.Contact) (result []string) { - for jid, contact := range input { - if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) != contacts { - continue - } - - if contacts { - result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.Jid[:len(contact.Jid)-len(whatsappExt.NewUserSuffix)])) - } else { - result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.Jid)) - } - } - sort.Sort(sort.StringSlice(result)) + // for jid, contact := range input { + // if strings.HasSuffix(jid, whatsappExt.NewUserSuffix) != contacts { + // continue + // } + // + // if contacts { + // result = append(result, fmt.Sprintf("* %s / %s - `%s`", contact.Name, contact.Notify, contact.Jid[:len(contact.Jid)-len(whatsappExt.NewUserSuffix)])) + // } else { + // result = append(result, fmt.Sprintf("* %s - `%s`", contact.Name, contact.Jid)) + // } + // } + // sort.Sort(sort.StringSlice(result)) return } diff --git a/database/database.go b/database/database.go index 11ad4d2..a7bd11b 100644 --- a/database/database.go +++ b/database/database.go @@ -17,6 +17,8 @@ package database import ( + "strings" + _ "github.com/lib/pq" _ "github.com/mattn/go-sqlite3" @@ -26,6 +28,7 @@ import ( "gorm.io/driver/postgres" "gorm.io/driver/sqlite" "gorm.io/gorm" + "gorm.io/gorm/schema" ) type Database struct { @@ -52,6 +55,9 @@ func New(dbType string, uri string, baseLog log.Logger) (*Database, error) { print("no") gdb, err := gorm.Open(conn, &gorm.Config{ // Logger: baseLog, + NamingStrategy: schema.NamingStrategy{ + NameReplacer: strings.NewReplacer("JID", "Jid", "MXID", "Mxid"), + }, }) if err != nil { panic("failed to connect database") @@ -81,6 +87,7 @@ func New(dbType string, uri string, baseLog log.Logger) (*Database, error) { } func (db *Database) Init() error { + println("actual upgrade") err := db.AutoMigrate(&Portal{}) if err != nil { return err @@ -104,6 +111,14 @@ func (db *Database) Init() error { return err } + err = db.AutoMigrate(&User{}) + if err != nil { + return err + } + err = db.AutoMigrate(&UserPortal{}) + if err != nil { + return err + } return upgrades.Run(db.log.Sub("Upgrade"), db.dialect, db.DB) } diff --git a/database/message.go b/database/message.go index 18e5d5c..f0d7cef 100644 --- a/database/message.go +++ b/database/message.go @@ -46,8 +46,8 @@ func (mq *MessageQuery) GetAll(chat PortalKey) (messages []*Message) { func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *Message { var message Message - ans := mq.db.Where("chat_jid = ? AND chat_receiver = ? AND jid = ?", chat.JID, chat.Receiver, jid).Take(&message) - if ans.Error != nil { + ans := mq.db.Where("chat_jid = ? AND chat_receiver = ? AND jid = ?", chat.JID, chat.Receiver, jid).Limit(1).Find(&message) + if ans.Error != nil || ans.RowsAffected == 0 { return nil } return &message @@ -55,8 +55,8 @@ func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *M func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message { var message Message - ans := mq.db.Where("mxid = ?", mxid).Take(&message) - if ans.Error != nil { + ans := mq.db.Where("mxid = ?", mxid).Limit(1).Find(&message) + if ans.Error != nil || ans.RowsAffected == 0 { return nil } return &message @@ -64,8 +64,8 @@ func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message { func (mq *MessageQuery) GetLastInChat(chat PortalKey) *Message { var message Message - ans := mq.db.Where("chat_jid = ? AND chat_receiver = ?", chat.JID, chat.Receiver).Order("timestamp desc").First(&message) - if ans.Error != nil { + ans := mq.db.Where("chat_jid = ? AND chat_receiver = ?", chat.JID, chat.Receiver).Order("timestamp desc").Limit(1).Find(&message) + if ans.Error != nil || ans.RowsAffected == 0 { return nil } return &message @@ -76,12 +76,12 @@ type Message struct { db *Database log log.Logger - Chat PortalKey `gorm:"primaryKey;embedded;embeddedPrefix:chat_"` - JID types.WhatsAppMessageID `gorm:"primaryKey"` - MXID id.EventID `gorm:"unique;notNull"` - Sender types.GroupMeID `gorm:"notNull"` - Timestamp uint64 `gorm:"notNull;default:0"` - Content *groupme.Message `gorm:"type:TEXT;notNull"` + Chat PortalKey `gorm:"primaryKey;embedded;embeddedPrefix:chat_"` + JID types.GroupMeID `gorm:"primaryKey"` + MXID id.EventID `gorm:"unique;notNull"` + Sender types.GroupMeID `gorm:"notNull"` + Timestamp uint64 `gorm:"notNull;default:0"` + Content *groupme.Message `gorm:"type:TEXT;notNull"` // Portal Portal `gorm:"foreignKey:JID;"` //`gorm:"foreignKey:Chat.Receiver,Chat.JID;references:jid,receiver;constraint:onDelete:CASCADE;"`TODO } diff --git a/database/portal.go b/database/portal.go index fa36c1c..10879f4 100644 --- a/database/portal.go +++ b/database/portal.go @@ -73,7 +73,7 @@ func (pq *PortalQuery) GetAll() []*Portal { } func (pq *PortalQuery) GetByJID(key PortalKey) *Portal { - return pq.get(pq.db.DB.Where("jit = ? AND receiver = ?", key.JID, key.Receiver)) + return pq.get(pq.db.DB.Where("jid = ? AND receiver = ?", key.JID, key.Receiver)) } @@ -97,16 +97,23 @@ func (pq *PortalQuery) getAll(db *gorm.DB) (portals []*Portal) { if ans.Error != nil || len(portals) == 0 { return nil } + for _, i := range portals { + i.db = pq.db + i.log = pq.log + } return } func (pq *PortalQuery) get(db *gorm.DB) *Portal { var portal Portal - ans := db.Take(&portal) - if ans.Error != nil { + ans := db.Limit(1).Find(&portal) + if ans.Error != nil || db.RowsAffected == 0 { return nil } + portal.db = pq.db + portal.log = pq.log + return &portal } @@ -155,8 +162,7 @@ func (portal *Portal) Insert() { } func (portal *Portal) Update() { - - ans := portal.db.Model(&portal).Updates(portal) + ans := portal.db.Where("jid = ? AND receiver = ?", portal.Key.JID, portal.Key.Receiver).Save(&portal) print("check .model vs not") if ans.Error != nil { @@ -172,6 +178,7 @@ func (portal *Portal) Delete() { } func (portal *Portal) GetUserIDs() []id.UserID { + println("HI AAAAAAAAAAAAAAAAAAAa") rows, err := portal.db.Raw(`SELECT "user".mxid FROM "user", user_portal WHERE "user".jid=user_portal.user_jid AND user_portal.portal_jid=$1 diff --git a/database/puppet.go b/database/puppet.go index 275b299..a7d0485 100644 --- a/database/puppet.go +++ b/database/puppet.go @@ -53,8 +53,8 @@ func (pq *PuppetQuery) GetAll() (puppets []*Puppet) { func (pq *PuppetQuery) Get(jid types.GroupMeID) *Puppet { puppet := Puppet{} - ans := pq.db.Where("jid = ?", jid).Take(puppet) - if ans.Error != nil { + ans := pq.db.Where("jid = ?", jid).Limit(1).Find(&puppet) + if ans.Error != nil || ans.RowsAffected == 0 { return nil } return &puppet @@ -62,8 +62,8 @@ func (pq *PuppetQuery) Get(jid types.GroupMeID) *Puppet { func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet { puppet := Puppet{} - ans := pq.db.Where("custom_mxid = ?", mxid).Take(puppet) - if ans.Error != nil { + ans := pq.db.Where("custom_mxid = ?", mxid).Limit(1).Find(&puppet) + if ans.Error != nil || ans.RowsAffected == 0 { return nil } return &puppet @@ -93,7 +93,7 @@ type Puppet struct { Displayname string NameQuality int8 - CustomMXID id.UserID + CustomMXID id.UserID `gorm:"column:custom_mxid;"` AccessToken string NextBatch string EnablePresence bool `gorm:"notNull;default:true"` diff --git a/database/statestore.go b/database/statestore.go index be70f93..bbea364 100644 --- a/database/statestore.go +++ b/database/statestore.go @@ -55,7 +55,8 @@ func NewSQLStateStore(db *Database) *SQLStateStore { func (store *SQLStateStore) IsRegistered(userID id.UserID) bool { v := mxRegistered{UserID: userID.String()} - ans := store.db.First(&v) + var count int64 + ans := store.db.Model(&mxRegistered{}).Where(&v).Count(&count) if errors.Is(ans.Error, gorm.ErrRecordNotFound) { return false @@ -63,7 +64,7 @@ func (store *SQLStateStore) IsRegistered(userID id.UserID) bool { if ans.Error != nil { store.log.Warnfln("Failed to scan registration existence for %s: %v", userID, ans.Error) } - return true + return count >= 1 } func (store *SQLStateStore) MarkRegistered(userID id.UserID) { @@ -113,7 +114,7 @@ func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*even func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { var user mxUserProfile - ans := store.db.Where("room_id = ? AND user_id = ?", roomID, userID).Take(&user) + ans := store.db.Where("room_id = ? AND user_id = ?", roomID, userID).Limit(1).Find(&user) membership := event.MembershipLeave if ans.Error != nil && ans.Error != gorm.ErrRecordNotFound { store.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, ans.Error) @@ -197,8 +198,8 @@ func (store *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, me print("weird thing 2 502650285") print(user.Membership) - ans := store.db.Select("roomID", "userID", "membership").Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "roomID"}, {Name: "userID"}}, + ans := store.db.Debug().Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "room_id"}, {Name: "user_id"}}, DoUpdates: clause.AssignmentColumns([]string{"membership"}), }).Create(&user) @@ -217,7 +218,7 @@ func (store *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member AvatarURL: string(member.AvatarURL), } ans := store.db.Clauses(clause.OnConflict{ - Columns: []clause.Column{{Name: "roomID"}, {Name: "userID"}}, + Columns: []clause.Column{{Name: "room_id"}, {Name: "user_id"}}, DoUpdates: clause.AssignmentColumns([]string{"membership"}), }).Create(&user) diff --git a/database/user.go b/database/user.go index 2ddfd49..174e286 100644 --- a/database/user.go +++ b/database/user.go @@ -17,15 +17,12 @@ package database import ( - "database/sql" "strings" - - "github.com/Rhymen/go-whatsapp" + "time" log "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix-whatsapp/types" - whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix/id" ) @@ -42,72 +39,77 @@ func (uq *UserQuery) New() *User { } func (uq *UserQuery) GetAll() (users []*User) { - // rows, err := uq.db.Query(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user"`) - // if err != nil || rows == nil { - // return nil - // } - // defer rows.Close() - // for rows.Next() { - // users = append(users, uq.New().Scan(rows)) - // } + ans := uq.db.Find(&users) + if ans.Error != nil || len(users) == 0 { + return nil + } + for _, i := range users { + i.db = uq.db + i.log = uq.log + } return } func (uq *UserQuery) GetByMXID(userID id.UserID) *User { - // row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE mxid=$1`, userID) - // if row == nil { - // return nil - // } - // return uq.New().Scan(row) - return nil + var user User + ans := uq.db.Where("mxid = ?", userID).Take(&user) + user.db = uq.db + user.log = uq.log + if ans.Error != nil { + return nil + } + return &user } func (uq *UserQuery) GetByJID(userID types.GroupMeID) *User { - // row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE jid=$1`, stripSuffix(userID)) - // if row == nil { - // return nil - // } - // return uq.New().Scan(row) - return nil + var user User + ans := uq.db.Where("jid = ?", userID).Limit(1).Find(&user) + if ans.Error != nil || ans.RowsAffected == 0 { + return nil + } + user.db = uq.db + user.log = uq.log + + return &user } type User struct { db *Database log log.Logger - MXID id.UserID - JID types.GroupMeID - Token types.AuthToken - + MXID id.UserID `gorm:"primaryKey"` + JID types.GroupMeID `gorm:"unique"` + Token types.AuthToken + ManagementRoom id.RoomID - LastConnection uint64 + LastConnection uint64 `gorm:"notNull;default:0"` } -func (user *User) Scan(row Scannable) *User { - var jid, clientID, clientToken, serverToken sql.NullString - var encKey, macKey []byte - err := row.Scan(&user.MXID, &jid, &user.ManagementRoom, &user.LastConnection, &clientID, &clientToken, &serverToken, &encKey, &macKey) - if err != nil { - if err != sql.ErrNoRows { - user.log.Errorln("Database scan failed:", err) - } - return nil - } - if len(jid.String) > 0 && len(clientID.String) > 0 { - user.JID = jid.String + whatsappExt.NewUserSuffix - // user.Session = &whatsapp.Session{ - // ClientId: clientID.String, - // ClientToken: clientToken.String, - // ServerToken: serverToken.String, - // EncKey: encKey, - // MacKey: macKey, - // Wid: jid.String + whatsappExt.OldUserSuffix, - // } - }// else { - // user.Session = nil - // } - return user -} +//func (user *User) Scan(row Scannable) *User { +// var jid, clientID, clientToken, serverToken sql.NullString +// var encKey, macKey []byte +// err := row.Scan(&user.MXID, &jid, &user.ManagementRoom, &user.LastConnection, &clientID, &clientToken, &serverToken, &encKey, &macKey) +// if err != nil { +// if err != sql.ErrNoRows { +// user.log.Errorln("Database scan failed:", err) +// } +// return nil +// } +// if len(jid.String) > 0 && len(clientID.String) > 0 { +// user.JID = jid.String + whatsappExt.NewUserSuffix +// // user.Session = &whatsapp.Session{ +// // ClientId: clientID.String, +// // ClientToken: clientToken.String, +// // ServerToken: serverToken.String, +// // EncKey: encKey, +// // MacKey: macKey, +// // Wid: jid.String + whatsappExt.OldUserSuffix, +// // } +// } // else { +// // user.Session = nil +// // } +// return user +//} func stripSuffix(jid types.GroupMeID) string { if len(jid) == 0 { @@ -130,42 +132,30 @@ func (user *User) jidPtr() *string { return nil } -func (user *User) sessionUnptr() (sess whatsapp.Session) { - // if user.Session != nil { - // sess = *user.Session - // } - return -} +//func (user *User) sessionUnptr() (sess whatsapp.Session) { +// // if user.Session != nil { +// // sess = *user.Session +// // } +// return +//} func (user *User) Insert() { - // sess := user.sessionUnptr() - // _, err := user.db.Exec(`INSERT INTO "user" (mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - // user.MXID, user.jidPtr(), - // user.ManagementRoom, user.LastConnection, - // sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) - // if err != nil { - // user.log.Warnfln("Failed to insert %s: %v", user.MXID, err) - // } + ans := user.db.Create(&user) + if ans.Error != nil { + user.log.Warnfln("Failed to insert %s: %v", user.MXID, ans.Error) + } } func (user *User) UpdateLastConnection() { - // user.LastConnection = uint64(time.Now().Unix()) - // _, err := user.db.Exec(`UPDATE "user" SET last_connection=$1 WHERE mxid=$2`, - // user.LastConnection, user.MXID) - // if err != nil { - // user.log.Warnfln("Failed to update last connection ts: %v", err) - // } + user.LastConnection = uint64(time.Now().Unix()) + user.Update() } func (user *User) Update() { - // sess := user.sessionUnptr() - // _, err := user.db.Exec(`UPDATE "user" SET jid=$1, management_room=$2, last_connection=$3, client_id=$4, client_token=$5, server_token=$6, enc_key=$7, mac_key=$8 WHERE mxid=$9`, - // user.jidPtr(), user.ManagementRoom, user.LastConnection, - // sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, - // user.MXID) - // if err != nil { - // user.log.Warnfln("Failed to update %s: %v", user.MXID, err) - // } + ans := user.db.Save(&user) + if ans.Error != nil { + user.log.Warnfln("Failed to update last connection ts: %v", ans.Error) + } } type PortalKeyWithMeta struct { @@ -173,82 +163,85 @@ type PortalKeyWithMeta struct { InCommunity bool } +type UserPortal struct { + UserJID types.GroupMeID `gorm:"primaryKey;"` + + PortalJID types.GroupMeID `gorm:"primaryKey;"` + PortalReceiver types.GroupMeID `gorm:"primaryKey;"` + + InCommunity bool `gorm:"notNull;default:false;"` + + User User `gorm:"foreignKey:UserJID;references:jid;constraint:OnDelete:CASCADE;"` + Portal Portal `gorm:"foreignKey:PortalJID,PortalReceiver;references:JID,Receiver;constraint:OnDelete:CASCADE;"` +} + func (user *User) SetPortalKeys(newKeys []PortalKeyWithMeta) error { - // tx, err := user.db.Begin() - // if err != nil { - // return err - // } - // _, err = tx.Exec("DELETE FROM user_portal WHERE user_jid=$1", user.jidPtr()) - // if err != nil { - // _ = tx.Rollback() - // return err - // } - // valueStrings := make([]string, len(newKeys)) - // values := make([]interface{}, len(newKeys)*4) - // for i, key := range newKeys { - // pos := i * 4 - // valueStrings[i] = fmt.Sprintf("($%d, $%d, $%d, $%d)", pos+1, pos+2, pos+3, pos+4) - // values[pos] = user.jidPtr() - // values[pos+1] = key.JID - // values[pos+2] = key.Receiver - // values[pos+3] = key.InCommunity - // } - // query := fmt.Sprintf("INSERT INTO user_portal (user_jid, portal_jid, portal_receiver, in_community) VALUES %s", - // strings.Join(valueStrings, ", ")) - // _, err = tx.Exec(query, values...) - // if err != nil { - // _ = tx.Rollback() - // return err - // } - // return tx.Commit() - return nil + tx := user.db.Begin() + ans := tx.Where("user_jid = ?", *user.jidPtr()).Delete(&UserPortal{}) + print("make sure all are deletede") + if ans.Error != nil { + _ = tx.Rollback() + return ans.Error + } + + for _, key := range newKeys { + ans = tx.Create(&UserPortal{ + UserJID: *user.jidPtr(), + PortalJID: key.JID, + PortalReceiver: key.Receiver, + InCommunity: key.InCommunity, + }) + if ans.Error != nil { + _ = tx.Rollback() + return ans.Error + } + } + + return tx.Commit().Error } func (user *User) IsInPortal(key PortalKey) bool { - // row := user.db.QueryRow(`SELECT EXISTS(SELECT 1 FROM user_portal WHERE user_jid=$1 AND portal_jid=$2 AND portal_receiver=$3)`, user.jidPtr(), &key.JID, &key.Receiver) - // var exists bool - // _ = row.Scan(&exists) - // return exists - return false + var count int64 + user.db.Find(&UserPortal{ + UserJID: *user.jidPtr(), + PortalJID: key.JID, + PortalReceiver: key.Receiver, + }).Count(&count) //TODO: efficient + return count > 0 } func (user *User) GetPortalKeys() []PortalKey { - // rows, err := user.db.Query(`SELECT portal_jid, portal_receiver FROM user_portal WHERE user_jid=$1`, user.jidPtr()) - // if err != nil { - // user.log.Warnln("Failed to get user portal keys:", err) - // return nil - // } - // var keys []PortalKey - // for rows.Next() { - // var key PortalKey - // err = rows.Scan(&key.JID, &key.Receiver) - // if err != nil { - // user.log.Warnln("Failed to scan row:", err) - // continue - // } - // keys = append(keys, key) - // } - // return keys - return nil + var up []UserPortal + ans := user.db.Where("user_jid = ?", *user.jidPtr()).Find(&up) + if ans.Error != nil { + user.log.Warnln("Failed to get user portal keys:", ans.Error) + return nil + } + var keys []PortalKey + for _, i := range up { + key := PortalKey{ + JID: i.UserJID, + Receiver: i.PortalReceiver, + } + keys = append(keys, key) + } + return keys } func (user *User) GetInCommunityMap() map[PortalKey]bool { - // rows, err := user.db.Query(`SELECT portal_jid, portal_receiver, in_community FROM user_portal WHERE user_jid=$1`, user.jidPtr()) - // if err != nil { - // user.log.Warnln("Failed to get user portal keys:", err) - // return nil - // } - // keys := make(map[PortalKey]bool) - // for rows.Next() { - // var key PortalKey - // var inCommunity bool - // err = rows.Scan(&key.JID, &key.Receiver, &inCommunity) - // if err != nil { - // user.log.Warnln("Failed to scan row:", err) - // continue - // } - // keys[key] = inCommunity - // } - // return keys - return nil + var up []UserPortal + ans := user.db.Where("user_jid = ?", *user.jidPtr()).Find(&up) + if ans.Error != nil { + user.log.Warnln("Failed to get user portal keys:", ans.Error) + return nil + } + keys := make(map[PortalKey]bool) + for _, i := range up { + key := PortalKey{ + JID: i.PortalJID, + Receiver: i.PortalReceiver, + } + keys[key] = i.InCommunity + } + return keys } diff --git a/groupme-ext/client.go b/groupme-ext/client.go deleted file mode 100644 index 8e4eff3..0000000 --- a/groupme-ext/client.go +++ /dev/null @@ -1,3 +0,0 @@ -package groupmeExt - -type Session struct{} \ No newline at end of file diff --git a/groupmeExt/client.go b/groupmeExt/client.go new file mode 100644 index 0000000..7f41f92 --- /dev/null +++ b/groupmeExt/client.go @@ -0,0 +1,52 @@ +package groupmeExt + +import ( + "context" + "fmt" + + "github.com/karmanyaahm/groupme" +) + +type Client struct { + *groupme.Client +} + +// NewClient creates a new GroupMe API Client +func NewClient(authToken string) *Client { + n := Client{ + Client: groupme.NewClient(authToken), + } + return &n +} +func (c Client) IndexAllGroups() ([]*groupme.Group, error) { + return c.IndexGroups(context.TODO(), &groupme.GroupsQuery{ + Omit: "memberships", + PerPage: 100, //TODO: Configurable and add multipage support + }) +} + +func (c Client) LoadMessagesAfter(groupID, lastMessageID string, lastMessageFromMe bool, num int) ([]*groupme.Message, error) { + //TODO: limit max 100 + i, e := c.IndexMessages(context.TODO(), groupme.ID(groupID), &groupme.IndexMessagesQuery{ + AfterID: groupme.ID(lastMessageID), + Limit: num, + }) + + if e != nil { + return nil, e + } + return i.Messages, nil +} + +func (c Client) LoadMessagesBefore(groupID, lastMessageID string, num int) ([]*groupme.Message, error) { + //TODO: limit max 100 + i, e := c.IndexMessages(context.TODO(), groupme.ID(groupID), &groupme.IndexMessagesQuery{ + BeforeID: groupme.ID(lastMessageID), + Limit: num, + }) + fmt.Println(groupID, lastMessageID, num, i.Count, e) + if e != nil { + return nil, e + } + return i.Messages, nil +} diff --git a/groupme-ext/message.go b/groupmeExt/message.go similarity index 100% rename from groupme-ext/message.go rename to groupmeExt/message.go diff --git a/groupmeExt/user.go b/groupmeExt/user.go new file mode 100644 index 0000000..2ae92d5 --- /dev/null +++ b/groupmeExt/user.go @@ -0,0 +1,6 @@ +package groupmeExt + +const ( + OldUserSuffix = "@c.groupme.com" + NewUserSuffix = "@groupme.com" +) diff --git a/main.go b/main.go index d09c532..fe7c449 100644 --- a/main.go +++ b/main.go @@ -327,7 +327,7 @@ func (bridge *Bridge) LoadRelaybot() { } bridge.Relaybot.ManagementRoom = bridge.Config.Bridge.Relaybot.ManagementRoom bridge.Relaybot.IsRelaybot = true - bridge.Relaybot.Connect(false) + bridge.Relaybot.Connect() } func (bridge *Bridge) UpdateBotProfile() { @@ -361,7 +361,7 @@ func (bridge *Bridge) UpdateBotProfile() { func (bridge *Bridge) StartUsers() { bridge.Log.Debugln("Starting users") for _, user := range bridge.GetAllUsers() { - go user.Connect(false) + go user.Connect() } bridge.Log.Debugln("Starting custom puppets") for _, loopuppet := range bridge.GetAllPuppetsWithCustomMXID() { diff --git a/matrix.go b/matrix.go index 94c1e3d..9b4ce30 100644 --- a/matrix.go +++ b/matrix.go @@ -336,6 +336,7 @@ func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { const sessionWaitTimeout = 5 * time.Second func (mx *MatrixHandler) HandleEncrypted(evt *event.Event) { + println("IDK iF encryption works yet") defer mx.bridge.Metrics.TrackEvent(evt.Type)() if mx.shouldIgnoreEvent(evt) || mx.bridge.Crypto == nil { return diff --git a/portal.go b/portal.go index e5cc78b..c80b515 100644 --- a/portal.go +++ b/portal.go @@ -18,6 +18,7 @@ package main import ( "bytes" + "context" "encoding/gob" "encoding/hex" "errors" @@ -28,13 +29,11 @@ import ( "image/jpeg" "image/png" "io/ioutil" - "math" "math/rand" "net/http" "os" "os/exec" "path/filepath" - "reflect" "strings" "sync" "time" @@ -45,6 +44,7 @@ import ( "github.com/Rhymen/go-whatsapp" waProto "github.com/Rhymen/go-whatsapp/binary/proto" + "github.com/karmanyaahm/groupme" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" @@ -54,6 +54,7 @@ import ( "maunium.net/go/mautrix/pushrules" "maunium.net/go/mautrix-whatsapp/database" + "maunium.net/go/mautrix-whatsapp/groupmeExt" "maunium.net/go/mautrix-whatsapp/types" whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) @@ -130,7 +131,7 @@ func (bridge *Bridge) NewManualPortal(key database.PortalKey) *Portal { bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", key)), - recentlyHandled: [recentlyHandledLength]types.WhatsAppMessageID{}, + recentlyHandled: [recentlyHandledLength]groupme.ID{}, messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer), } @@ -145,7 +146,7 @@ func (bridge *Bridge) NewPortal(dbPortal *database.Portal) *Portal { bridge: bridge, log: bridge.Log.Sub(fmt.Sprintf("Portal/%s", dbPortal.Key)), - recentlyHandled: [recentlyHandledLength]types.WhatsAppMessageID{}, + recentlyHandled: [recentlyHandledLength]groupme.ID{}, messages: make(chan PortalMessage, bridge.Config.Bridge.PortalMessageBuffer), } @@ -158,7 +159,7 @@ const recentlyHandledLength = 100 type PortalMessage struct { chat string source *User - data interface{} + data *groupme.Message timestamp uint64 } @@ -170,7 +171,7 @@ type Portal struct { roomCreateLock sync.Mutex - recentlyHandled [recentlyHandledLength]types.WhatsAppMessageID + recentlyHandled [recentlyHandledLength]groupme.ID recentlyHandledLock sync.Mutex recentlyHandledIndex uint8 @@ -213,56 +214,57 @@ func (portal *Portal) handleMessage(msg PortalMessage) { portal.log.Warnln("handleMessage called even though portal.MXID is empty") return } - switch data := msg.data.(type) { - case whatsapp.TextMessage: - portal.HandleTextMessage(msg.source, data) - case whatsapp.ImageMessage: - portal.HandleMediaMessage(msg.source, mediaMessage{ - base: base{data.Download, data.Info, data.ContextInfo, data.Type}, - thumbnail: data.Thumbnail, - caption: data.Caption, - }) - case whatsapp.StickerMessage: - portal.HandleMediaMessage(msg.source, mediaMessage{ - base: base{data.Download, data.Info, data.ContextInfo, data.Type}, - sendAsSticker: true, - }) - case whatsapp.VideoMessage: - portal.HandleMediaMessage(msg.source, mediaMessage{ - base: base{data.Download, data.Info, data.ContextInfo, data.Type}, - thumbnail: data.Thumbnail, - caption: data.Caption, - length: data.Length, - }) - case whatsapp.AudioMessage: - portal.HandleMediaMessage(msg.source, mediaMessage{ - base: base{data.Download, data.Info, data.ContextInfo, data.Type}, - length: data.Length, - }) - case whatsapp.DocumentMessage: - fileName := data.FileName - if len(fileName) == 0 { - fileName = data.Title - } - portal.HandleMediaMessage(msg.source, mediaMessage{ - base: base{data.Download, data.Info, data.ContextInfo, data.Type}, - thumbnail: data.Thumbnail, - fileName: fileName, - }) - case whatsapp.ContactMessage: - portal.HandleContactMessage(msg.source, data) - case whatsapp.LocationMessage: - portal.HandleLocationMessage(msg.source, data) - case whatsappExt.MessageRevocation: - portal.HandleMessageRevoke(msg.source, data) - case FakeMessage: - portal.HandleFakeMessage(msg.source, data) - default: - portal.log.Warnln("Unknown message type:", reflect.TypeOf(msg.data)) - } + portal.HandleTextMessage(msg.source, msg.data) + // switch msg.data. { + // case whatsapp.TextMessage: + // portal.HandleTextMessage(msg.source, data) + // case whatsapp.ImageMessage: + // portal.HandleMediaMessage(msg.source, mediaMessage{ + // base: base{data.Download, data.Info, data.ContextInfo, data.Type}, + // thumbnail: data.Thumbnail, + // caption: data.Caption, + // }) + // case whatsapp.StickerMessage: + // portal.HandleMediaMessage(msg.source, mediaMessage{ + // base: base{data.Download, data.Info, data.ContextInfo, data.Type}, + // sendAsSticker: true, + // }) + // case whatsapp.VideoMessage: + // portal.HandleMediaMessage(msg.source, mediaMessage{ + // base: base{data.Download, data.Info, data.ContextInfo, data.Type}, + // thumbnail: data.Thumbnail, + // caption: data.Caption, + // length: data.Length, + // }) + // case whatsapp.AudioMessage: + // portal.HandleMediaMessage(msg.source, mediaMessage{ + // base: base{data.Download, data.Info, data.ContextInfo, data.Type}, + // length: data.Length, + // }) + // case whatsapp.DocumentMessage: + // fileName := data.FileName + // if len(fileName) == 0 { + // fileName = data.Title + // } + // portal.HandleMediaMessage(msg.source, mediaMessage{ + // base: base{data.Download, data.Info, data.ContextInfo, data.Type}, + // thumbnail: data.Thumbnail, + // fileName: fileName, + // }) + // case whatsapp.ContactMessage: + // portal.HandleContactMessage(msg.source, data) + // case whatsapp.LocationMessage: + // portal.HandleLocationMessage(msg.source, data) + // case whatsappExt.MessageRevocation: + // portal.HandleMessageRevoke(msg.source, data) + // case FakeMessage: + // portal.HandleFakeMessage(msg.source, data) + // default: + // portal.log.Warnln("Unknown message type:", reflect.TypeOf(msg.data)) + // } } -func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool { +func (portal *Portal) isRecentlyHandled(id groupme.ID) bool { start := portal.recentlyHandledIndex for i := start; i != start; i = (i - 1) % recentlyHandledLength { if portal.recentlyHandled[i] == id { @@ -272,8 +274,8 @@ func (portal *Portal) isRecentlyHandled(id types.WhatsAppMessageID) bool { return false } -func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool { - msg := portal.bridge.DB.Message.GetByJID(portal.Key, id) +func (portal *Portal) isDuplicate(id groupme.ID) bool { + msg := portal.bridge.DB.Message.GetByJID(portal.Key, id.String()) if msg != nil { return true } @@ -284,7 +286,7 @@ func init() { gob.Register(&waProto.Message{}) } -func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { +func (portal *Portal) markHandled(source *User, message *groupme.Message, mxid id.EventID) { print("handle message") // msg := portal.bridge.DB.Message.New() // msg.Chat = portal.Key @@ -311,34 +313,35 @@ func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, // portal.recentlyHandled[index] = msg.JID } -func (portal *Portal) getMessageIntent(user *User, info whatsapp.MessageInfo) *appservice.IntentAPI { - if info.FromMe { +func (portal *Portal) getMessageIntent(user *User, info *groupme.Message) *appservice.IntentAPI { + if info.UserID.String() == user.GetJID() { //from me return portal.bridge.GetPuppetByJID(user.JID).IntentFor(portal) } else if portal.IsPrivateChat() { return portal.MainIntent() - } else if len(info.SenderJid) == 0 { - if len(info.Source.GetParticipant()) != 0 { - info.SenderJid = info.Source.GetParticipant() - } else { - return nil - } + } else if len(info.UserID.String()) == 0 { + // if len(info.Source.GetParticipant()) != 0 { + // info.SenderJid = info.Source.GetParticipant() + // } else { + // return nil + // } + println("TODO weird uid stuff") } - return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) + return portal.bridge.GetPuppetByJID(info.UserID.String()).IntentFor(portal) } -func (portal *Portal) startHandling(source *User, info whatsapp.MessageInfo) *appservice.IntentAPI { +func (portal *Portal) startHandling(source *User, info *groupme.Message) *appservice.IntentAPI { // TODO these should all be trace logs - if portal.lastMessageTs > info.Timestamp+1 { - portal.log.Debugfln("Not handling %s: message is older (%d) than last bridge message (%d)", info.Id, info.Timestamp, portal.lastMessageTs) - } else if portal.isRecentlyHandled(info.Id) { - portal.log.Debugfln("Not handling %s: message was recently handled", info.Id) - } else if portal.isDuplicate(info.Id) { - portal.log.Debugfln("Not handling %s: message is duplicate", info.Id) + if portal.lastMessageTs > uint64(info.CreatedAt.ToTime().Unix()+1) { + portal.log.Debugfln("Not handling %s: message is older (%d) than last bridge message (%d)", info.ID, info.CreatedAt, portal.lastMessageTs) + } else if portal.isRecentlyHandled(info.ID) { + portal.log.Debugfln("Not handling %s: message was recently handled", info.ID) + } else if portal.isDuplicate(info.ID) { + portal.log.Debugfln("Not handling %s: message is duplicate", info.ID) } else { - portal.lastMessageTs = info.Timestamp + portal.lastMessageTs = uint64(info.CreatedAt.ToTime().Unix()) intent := portal.getMessageIntent(source, info) if intent != nil { - portal.log.Debugfln("Starting handling of %s (ts: %d)", info.Id, info.Timestamp) + portal.log.Debugfln("Starting handling of %s (ts: %d)", info.ID, info.CreatedAt) } else { portal.log.Debugfln("Not handling %s: sender is not known") } @@ -347,13 +350,13 @@ func (portal *Portal) startHandling(source *User, info whatsapp.MessageInfo) *ap return nil } -func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) { +func (portal *Portal) finishHandling(source *User, message *groupme.Message, mxid id.EventID) { portal.markHandled(source, message, mxid) portal.sendDeliveryReceipt(mxid) - portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid) + portal.log.Debugln("Handled message", message.ID.String(), "->", mxid) } -func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { +func (portal *Portal) SyncParticipants(metadata *groupme.Group) { changed := false levels, err := portal.MainIntent().PowerLevels(portal.MXID) if err != nil { @@ -361,23 +364,24 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { changed = true } participantMap := make(map[string]bool) - for _, participant := range metadata.Participants { - participantMap[participant.JID] = true - user := portal.bridge.GetUserByJID(participant.JID) + for _, participant := range metadata.Members { + participantMap[participant.ID.String()] = true + fmt.Println(participant.ID.String()) + user := portal.bridge.GetUserByJID(participant.ID.String()) portal.userMXIDAction(user, portal.ensureMXIDInvited) - puppet := portal.bridge.GetPuppetByJID(participant.JID) + puppet := portal.bridge.GetPuppetByJID(participant.ID.String()) err := puppet.IntentFor(portal).EnsureJoined(portal.MXID) if err != nil { - portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.JID, portal.MXID, err) + portal.log.Warnfln("Failed to make puppet of %s join %s: %v", participant.ID.String(), portal.MXID, err) } expectedLevel := 0 - if participant.IsSuperAdmin { - expectedLevel = 95 - } else if participant.IsAdmin { - expectedLevel = 50 - } + // if participant.IsSuperAdmin { + // expectedLevel = 95 + // } else if participant.IsAdmin { + // expectedLevel = 50 + // } changed = levels.EnsureUserLevel(puppet.MXID, expectedLevel) || changed if user != nil { changed = levels.EnsureUserLevel(user.MXID, expectedLevel) || changed @@ -397,6 +401,7 @@ func (portal *Portal) SyncParticipants(metadata *whatsappExt.GroupInfo) { jid, ok := portal.bridge.ParsePuppetMXID(member) if ok { _, shouldBePresent := participantMap[jid] + fmt.Println(jid) if !shouldBePresent { _, err := portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{ UserID: member, @@ -454,7 +459,7 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf // if updateInfo { // portal.UpdateBridgeInfo() // } - return true + return true } func (portal *Portal) UpdateName(name string, setBy types.GroupMeID, updateInfo bool) bool { @@ -496,35 +501,35 @@ func (portal *Portal) UpdateTopic(topic string, setBy types.GroupMeID, updateInf } func (portal *Portal) UpdateMetadata(user *User) bool { - // if portal.IsPrivateChat() { - // return false - // } else if portal.IsStatusBroadcastRoom() { - // update := false - // update = portal.UpdateName("WhatsApp Status Broadcast", "", false) || update - // update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", false) || update - // return update - // } - // metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID) - // if err != nil { - // portal.log.Errorln(err) - // return false - // } - // if metadata.Status != 0 { - // // 401: access denied - // // 404: group does (no longer) exist - // // 500: ??? happens with status@broadcast + if portal.IsPrivateChat() { + return false + } else if portal.IsStatusBroadcastRoom() { + update := false + update = portal.UpdateName("WhatsApp Status Broadcast", "", false) || update + update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", false) || update + return update + } + group, err := user.Client.ShowGroup(context.TODO(), groupme.ID(strings.Replace(portal.Key.JID, groupmeExt.NewUserSuffix, "", 1))) + if err != nil { + portal.log.Errorln(err) + return false + } + // if metadata.Status != 0 { + // 401: access denied + // 404: group does (no longer) exist + // 500: ??? happens with status@broadcast - // // TODO: update the room, e.g. change priority level - // // to send messages to moderator - // return false - // } + // TODO: update the room, e.g. change priority level + // to send messages to moderator + return false + // } - // portal.SyncParticipants(metadata) + portal.SyncParticipants(group) update := false - // update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update - // update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update + update = portal.UpdateName(group.Name, "", false) || update + update = portal.UpdateTopic(group.Description, "", false) || update - // portal.RestrictMessageSending(metadata.Announce) + // portal.RestrictMessageSending(metadata.Announce) return update } @@ -559,7 +564,7 @@ func (portal *Portal) ensureUserInvited(user *User) { } } -func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { +func (portal *Portal) Sync(user *User, group groupme.Group) { portal.log.Infoln("Syncing portal for", user.MXID) if user.IsRelaybot { @@ -569,7 +574,7 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { if len(portal.MXID) == 0 { if !portal.IsPrivateChat() { - portal.Name = contact.Name + portal.Name = group.Name } err := portal.CreateMatrixRoom(user) if err != nil { @@ -691,48 +696,45 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) { } func (portal *Portal) BackfillHistory(user *User, lastMessageTime uint64) error { - // if !portal.bridge.Config.Bridge.RecoverHistory { - // return nil - // } + if !portal.bridge.Config.Bridge.RecoverHistory { + return nil + } - // endBackfill := portal.beginBackfill() - // defer endBackfill() + endBackfill := portal.beginBackfill() + defer endBackfill() - // lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key) - // if lastMessage == nil { - // return nil - // } - // if lastMessage.Timestamp >= lastMessageTime { - // portal.log.Debugln("Not backfilling: no new messages") - // return nil - // } + lastMessage := portal.bridge.DB.Message.GetLastInChat(portal.Key) + if lastMessage == nil { + return nil + } + if lastMessage.Timestamp >= lastMessageTime { + portal.log.Debugln("Not backfilling: no new messages") + return nil + } - // lastMessageID := lastMessage.JID - // lastMessageFromMe := lastMessage.Sender == user.JID - // portal.log.Infoln("Backfilling history since", lastMessageID, "for", user.MXID) - // for len(lastMessageID) > 0 { - // portal.log.Debugln("Fetching 50 messages of history after", lastMessageID) - // resp, err := user.Conn.LoadMessagesAfter(portal.Key.JID, lastMessageID, lastMessageFromMe, 50) - // if err != nil { - // return err - // } - // messages, ok := resp.Content.([]interface{}) - // if !ok || len(messages) == 0 { - // portal.log.Debugfln("Didn't get more messages to backfill (resp.Content is %T)", resp.Content) - // break - // } + lastMessageID := lastMessage.JID + lastMessageFromMe := lastMessage.Sender == user.JID + portal.log.Infoln("Backfilling history since", lastMessageID, "for", user.MXID) + for len(lastMessageID) > 0 { + portal.log.Debugln("Fetching 50 messages of history after", lastMessageID) + messages, err := user.Client.LoadMessagesAfter(portal.Key.JID, lastMessageID, lastMessageFromMe, 50) + if err != nil { + return err + } + // messages, ok := resp.Content.([]interface{}) + if len(messages) == 0 { + portal.log.Debugfln("Didn't get more messages to backfill (resp.Content is %T)", messages) + break + } - // portal.handleHistory(user, messages) + portal.handleHistory(user, messages) - // lastMessageProto, ok := messages[len(messages)-1].(*waProto.WebMessageInfo) - // if ok { - // lastMessageID = lastMessageProto.GetKey().GetId() - // lastMessageFromMe = lastMessageProto.GetKey().GetFromMe() - // } - // } - // portal.log.Infoln("Backfilling finished") - // return nil -return nil + lastMessageProto := messages[len(messages)-1] + lastMessageID = lastMessageProto.ID.String() + lastMessageFromMe = lastMessageProto.UserID.String() == user.JID + } + portal.log.Infoln("Backfilling finished") + return nil } func (portal *Portal) beginBackfill() func() { @@ -801,80 +803,85 @@ func (portal *Portal) enableNotifications(user *User) { } func (portal *Portal) FillInitialHistory(user *User) error { - // if portal.bridge.Config.Bridge.InitialHistoryFill == 0 { - // return nil - // } - // endBackfill := portal.beginBackfill() - // defer endBackfill() - // if portal.privateChatBackfillInvitePuppet != nil { - // portal.privateChatBackfillInvitePuppet() - // } + if portal.bridge.Config.Bridge.InitialHistoryFill == 0 { + return nil + } + endBackfill := portal.beginBackfill() + defer endBackfill() + if portal.privateChatBackfillInvitePuppet != nil { + portal.privateChatBackfillInvitePuppet() + } - // n := portal.bridge.Config.Bridge.InitialHistoryFill - // portal.log.Infoln("Filling initial history, maximum", n, "messages") - // var messages []interface{} - // before := "" - // fromMe := true - // chunkNum := 1 - // for n > 0 { - // count := 50 - // if n < count { - // count = n - // } - // portal.log.Debugfln("Fetching chunk %d (%d messages / %d cap) before message %s", chunkNum, count, n, before) - // resp, err := user.Conn.LoadMessagesBefore(portal.Key.JID, before, fromMe, count) - // if err != nil { - // return err - // } - // chunk, ok := resp.Content.([]interface{}) - // if !ok || len(chunk) == 0 { - // portal.log.Infoln("Chunk empty, starting handling of loaded messages") - // break - // } + n := portal.bridge.Config.Bridge.InitialHistoryFill + portal.log.Infoln("Filling initial history, maximum", n, "messages") + var messages []*groupme.Message + before := "" + chunkNum := 1 + for n > 0 { + count := 50 + if n < count { + count = n + } + portal.log.Debugfln("Fetching chunk %d (%d messages / %d cap) before message %s", chunkNum, count, n, before) + chunk, err := user.Client.LoadMessagesBefore(portal.Key.JID, before, count) + if err != nil { + return err + } + if len(chunk) == 0 { + portal.log.Infoln("Chunk empty, starting handling of loaded messages") + break + } - // messages = append(chunk, messages...) + //reverses chunk to ascending order (oldest first) + i := 0 + j := len(chunk) - 1 + for i < j { + chunk[i], chunk[j] = chunk[j], chunk[i] + i++ + j-- + } - // portal.log.Debugfln("Fetched chunk and received %d messages", len(chunk)) + messages = append(chunk, messages...) - // n -= len(chunk) - // key := chunk[0].(*waProto.WebMessageInfo).GetKey() - // before = key.GetId() - // fromMe = key.GetFromMe() - // if len(before) == 0 { - // portal.log.Infoln("No message ID for first message, starting handling of loaded messages") - // break - // } - // } - // portal.disableNotifications(user) - // portal.handleHistory(user, messages) - // portal.enableNotifications(user) - // portal.log.Infoln("Initial history fill complete") + portal.log.Debugfln("Fetched chunk and received %d messages", len(chunk)) + + n -= len(chunk) + before = chunk[0].ID.String() + if len(before) == 0 { + portal.log.Infoln("No message ID for first message, starting handling of loaded messages") + break + } + } + portal.disableNotifications(user) + portal.handleHistory(user, messages) + portal.enableNotifications(user) + portal.log.Infoln("Initial history fill complete") return nil } -func (portal *Portal) handleHistory(user *User, messages []interface{}) { +func (portal *Portal) handleHistory(user *User, messages []*groupme.Message) { portal.log.Infoln("Handling", len(messages), "messages of history") - for _, rawMessage := range messages { - message, ok := rawMessage.(*waProto.WebMessageInfo) - if !ok { - portal.log.Warnln("Unexpected non-WebMessageInfo item in history response:", rawMessage) - continue - } - data := whatsapp.ParseProtoMessage(message) - if data == nil || data == whatsapp.ErrMessageTypeNotImplemented { - st := message.GetMessageStubType() - // Ignore some types that are known to fail - if st == waProto.WebMessageInfo_CALL_MISSED_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_VIDEO || - st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VIDEO { - continue - } - portal.log.Warnln("Message", message.GetKey().GetId(), "failed to parse during backfilling") - continue - } - if portal.privateChatBackfillInvitePuppet != nil && message.GetKey().GetFromMe() && portal.IsPrivateChat() { + for _, message := range messages { + // data, ok := rawMessage.(*groupme.Message) + // if !ok { + // portal.log.Warnln("Unexpected non-WebMessageInfo item in history response:", rawMessage) + // continue + // } + // data := whatsapp.ParseProtoMessage(message) + // if data == nil || data == whatsapp.ErrMessageTypeNotImplemented { + // st := message.GetMessageStubType() + // // Ignore some types that are known to fail + // if st == waProto.WebMessageInfo_CALL_MISSED_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_VIDEO || + // st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VOICE || st == waProto.WebMessageInfo_CALL_MISSED_GROUP_VIDEO { + // continue + // } + // portal.log.Warnln("Message", message.GetKey().GetId(), "failed to parse during backfilling") + // continue + // } + if portal.privateChatBackfillInvitePuppet != nil && message.UserID.String() == user.JID && portal.IsPrivateChat() { portal.privateChatBackfillInvitePuppet() } - portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()}) + portal.handleMessage(PortalMessage{portal.Key.JID, user, message, uint64(message.CreatedAt.ToTime().Unix())}) } } @@ -936,139 +943,139 @@ func (portal *Portal) UpdateBridgeInfo() { } func (portal *Portal) CreateMatrixRoom(user *User) error { - // portal.roomCreateLock.Lock() - // defer portal.roomCreateLock.Unlock() - // if len(portal.MXID) > 0 { - // return nil - // } + portal.roomCreateLock.Lock() + defer portal.roomCreateLock.Unlock() + if len(portal.MXID) > 0 { + return nil + } - // intent := portal.MainIntent() - // if err := intent.EnsureRegistered(); err != nil { - // return err - // } + intent := portal.MainIntent() + if err := intent.EnsureRegistered(); err != nil { + return err + } - // portal.log.Infoln("Creating Matrix room. Info source:", user.MXID) + portal.log.Infoln("Creating Matrix room. Info source:", user.MXID) - // var metadata *whatsappExt.GroupInfo - // if portal.IsPrivateChat() { - // puppet := portal.bridge.GetPuppetByJID(portal.Key.JID) - // if portal.bridge.Config.Bridge.PrivateChatPortalMeta { - // portal.Name = puppet.Displayname - // portal.AvatarURL = puppet.AvatarURL - // portal.Avatar = puppet.Avatar - // } else { - // portal.Name = "" - // } - // portal.Topic = "WhatsApp private chat" - // } else if portal.IsStatusBroadcastRoom() { - // portal.Name = "WhatsApp Status Broadcast" - // portal.Topic = "WhatsApp status updates from your contacts" - // } else { - // var err error - // metadata, err = user.Conn.GetGroupMetaData(portal.Key.JID) - // if err == nil && metadata.Status == 0 { - // portal.Name = metadata.Name - // portal.Topic = metadata.Topic - // } - // portal.UpdateAvatar(user, nil, false) - // } + var metadata *groupme.Group + if portal.IsPrivateChat() { + puppet := portal.bridge.GetPuppetByJID(portal.Key.JID) + if portal.bridge.Config.Bridge.PrivateChatPortalMeta { + portal.Name = puppet.Displayname + portal.AvatarURL = puppet.AvatarURL + portal.Avatar = puppet.Avatar + } else { + portal.Name = "" + } + portal.Topic = "WhatsApp private chat" + // } else if portal.IsStatusBroadcastRoom() { + // portal.Name = "WhatsApp Status Broadcast" + // portal.Topic = "WhatsApp status updates from your contacts" + } else { + var err error + metadata, err = user.Client.ShowGroup(context.TODO(), groupme.ID(portal.Key.JID)) + if err == nil { + portal.Name = metadata.Name + portal.Topic = metadata.Description + } + portal.UpdateAvatar(user, nil, false) + } - // bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo() + bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo() - // initialState := []*event.Event{{ - // Type: event.StatePowerLevels, - // Content: event.Content{ - // Parsed: portal.GetBasePowerLevels(), - // }, - // }, { - // Type: StateBridgeInfo, - // Content: event.Content{Parsed: bridgeInfo}, - // StateKey: &bridgeInfoStateKey, - // }, { - // // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec - // Type: StateHalfShotBridgeInfo, - // Content: event.Content{Parsed: bridgeInfo}, - // StateKey: &bridgeInfoStateKey, - // }} - // if !portal.AvatarURL.IsEmpty() { - // initialState = append(initialState, &event.Event{ - // Type: event.StateRoomAvatar, - // Content: event.Content{ - // Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL}, - // }, - // }) - // } + initialState := []*event.Event{{ + Type: event.StatePowerLevels, + Content: event.Content{ + Parsed: portal.GetBasePowerLevels(), + }, + }, { + Type: StateBridgeInfo, + Content: event.Content{Parsed: bridgeInfo}, + StateKey: &bridgeInfoStateKey, + }, { + // TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec + Type: StateHalfShotBridgeInfo, + Content: event.Content{Parsed: bridgeInfo}, + StateKey: &bridgeInfoStateKey, + }} + if !portal.AvatarURL.IsEmpty() { + initialState = append(initialState, &event.Event{ + Type: event.StateRoomAvatar, + Content: event.Content{ + Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL}, + }, + }) + } - // invite := []id.UserID{user.MXID} - // if user.IsRelaybot { - // invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers - // } + invite := []id.UserID{user.MXID} + if user.IsRelaybot { + invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers + } - // if portal.bridge.Config.Bridge.Encryption.Default { - // initialState = append(initialState, &event.Event{ - // Type: event.StateEncryption, - // Content: event.Content{ - // Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}, - // }, - // }) - // portal.Encrypted = true - // if portal.IsPrivateChat() { - // invite = append(invite, portal.bridge.Bot.UserID) - // } - // } + if portal.bridge.Config.Bridge.Encryption.Default { + initialState = append(initialState, &event.Event{ + Type: event.StateEncryption, + Content: event.Content{ + Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}, + }, + }) + portal.Encrypted = true + if portal.IsPrivateChat() { + invite = append(invite, portal.bridge.Bot.UserID) + } + } - // resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ - // Visibility: "private", - // Name: portal.Name, - // Topic: portal.Topic, - // Invite: invite, - // Preset: "private_chat", - // IsDirect: portal.IsPrivateChat(), - // InitialState: initialState, - // }) - // if err != nil { - // return err - // } - // portal.MXID = resp.RoomID - // portal.Update() - // portal.bridge.portalsLock.Lock() - // portal.bridge.portalsByMXID[portal.MXID] = portal - // portal.bridge.portalsLock.Unlock() + resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ + Visibility: "private", + Name: portal.Name, + Topic: portal.Topic, + Invite: invite, + Preset: "private_chat", + IsDirect: portal.IsPrivateChat(), + InitialState: initialState, + }) + if err != nil { + return err + } + portal.MXID = resp.RoomID + portal.Update() + portal.bridge.portalsLock.Lock() + portal.bridge.portalsByMXID[portal.MXID] = portal + portal.bridge.portalsLock.Unlock() - // // We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here. - // for _, user := range invite { - // portal.bridge.StateStore.SetMembership(portal.MXID, user, event.MembershipInvite) - // } + // We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here. + for _, user := range invite { + portal.bridge.StateStore.SetMembership(portal.MXID, user, event.MembershipInvite) + } - // if metadata != nil { - // portal.SyncParticipants(metadata) - // if metadata.Announce { - // portal.RestrictMessageSending(metadata.Announce) - // } - // } else { - // customPuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) - // if customPuppet != nil && customPuppet.CustomIntent() != nil { - // _ = customPuppet.CustomIntent().EnsureJoined(portal.MXID) - // } - // } - // user.addPortalToCommunity(portal) - // if portal.IsPrivateChat() { - // puppet := user.bridge.GetPuppetByJID(portal.Key.JID) - // user.addPuppetToCommunity(puppet) + if metadata != nil { + portal.SyncParticipants(metadata) + // if metadata.Announce { + // portal.RestrictMessageSending(metadata.Announce) + // } + } else { + customPuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if customPuppet != nil && customPuppet.CustomIntent() != nil { + _ = customPuppet.CustomIntent().EnsureJoined(portal.MXID) + } + } + user.addPortalToCommunity(portal) + if portal.IsPrivateChat() { + puppet := user.bridge.GetPuppetByJID(portal.Key.JID) + user.addPuppetToCommunity(puppet) - // if portal.bridge.Config.Bridge.Encryption.Default { - // err = portal.bridge.Bot.EnsureJoined(portal.MXID) - // if err != nil { - // portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err) - // } - // } + if portal.bridge.Config.Bridge.Encryption.Default { + err = portal.bridge.Bot.EnsureJoined(portal.MXID) + if err != nil { + portal.log.Errorln("Failed to join created portal with bridge bot for e2be:", err) + } + } - // user.UpdateDirectChats(map[id.UserID][]id.RoomID{puppet.MXID: {portal.MXID}}) - // } - // err = portal.FillInitialHistory(user) - // if err != nil { - // portal.log.Errorln("Failed to fill history:", err) - // } + user.UpdateDirectChats(map[id.UserID][]id.RoomID{puppet.MXID: {portal.MXID}}) + } + err = portal.FillInitialHistory(user) + if err != nil { + portal.log.Errorln("Failed to fill history:", err) + } return nil } @@ -1153,30 +1160,30 @@ func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.Messag msg.Delete() } -func (portal *Portal) HandleFakeMessage(_ *User, message FakeMessage) { - if portal.isRecentlyHandled(message.ID) { - return - } - - content := event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: message.Text, - } - if message.Alert { - content.MsgType = event.MsgText - } - _, err := portal.sendMainIntentMessage(content) - if err != nil { - portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err) - return - } - - portal.recentlyHandledLock.Lock() - index := portal.recentlyHandledIndex - portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength - portal.recentlyHandledLock.Unlock() - portal.recentlyHandled[index] = message.ID -} +//func (portal *Portal) HandleFakeMessage(_ *User, message FakeMessage) { +// if portal.isRecentlyHandled(message.ID) { +// return +// } +// +// content := event.MessageEventContent{ +// MsgType: event.MsgNotice, +// Body: message.Text, +// } +// if message.Alert { +// content.MsgType = event.MsgText +// } +// _, err := portal.sendMainIntentMessage(content) +// if err != nil { +// portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err) +// return +// } +// +// portal.recentlyHandledLock.Lock() +// index := portal.recentlyHandledIndex +// portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength +// portal.recentlyHandledLock.Unlock() +// portal.recentlyHandled[index] = message.ID +//} func (portal *Portal) sendMainIntentMessage(content interface{}) (*mautrix.RespSendEvent, error) { return portal.sendMessage(portal.MainIntent(), event.EventMessage, content, 0) @@ -1232,8 +1239,8 @@ func (portal *Portal) sendMessageDirect(intent *appservice.IntentAPI, eventType } } -func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { - intent := portal.startHandling(source, message.Info) +func (portal *Portal) HandleTextMessage(source *User, message *groupme.Message) { + intent := portal.startHandling(source, message) if intent == nil { return } @@ -1243,132 +1250,133 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa MsgType: event.MsgText, } - portal.bridge.Formatter.ParseWhatsApp(content, message.ContextInfo.MentionedJID) - portal.SetReply(content, message.ContextInfo) + // portal.bridge.Formatter.ParseWhatsApp(content, message.ContextInfo.MentionedJID) + // portal.SetReply(content, message.ContextInfo) + //TODO: mentions _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) + resp, err := portal.sendMessage(intent, event.EventMessage, content, message.CreatedAt.ToTime().UnixNano()) if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) + portal.log.Errorfln("Failed to handle message %s: %v", message.ID, err) return } - portal.finishHandling(source, message.Info.Source, resp.EventID) + portal.finishHandling(source, message, resp.EventID) } -func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) { - intent := portal.startHandling(source, message.Info) - if intent == nil { - return - } +//func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) { +// intent := portal.startHandling(source, message.Info) +// if intent == nil { +// return +// } +// +// url := message.Url +// if len(url) == 0 { +// url = fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude) +// } +// name := message.Name +// if len(name) == 0 { +// latChar := 'N' +// if message.DegreesLatitude < 0 { +// latChar = 'S' +// } +// longChar := 'E' +// if message.DegreesLongitude < 0 { +// longChar = 'W' +// } +// name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(message.DegreesLatitude), latChar, math.Abs(message.DegreesLongitude), longChar) +// } +// +// content := &event.MessageEventContent{ +// MsgType: event.MsgLocation, +// Body: fmt.Sprintf("Location: %s\n%s\n%s", name, message.Address, url), +// Format: event.FormatHTML, +// FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, message.Address), +// GeoURI: fmt.Sprintf("geo:%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude), +// } +// +// if len(message.JpegThumbnail) > 0 { +// thumbnailMime := http.DetectContentType(message.JpegThumbnail) +// uploadedThumbnail, _ := intent.UploadBytes(message.JpegThumbnail, thumbnailMime) +// if uploadedThumbnail != nil { +// cfg, _, _ := image.DecodeConfig(bytes.NewReader(message.JpegThumbnail)) +// content.Info = &event.FileInfo{ +// ThumbnailInfo: &event.FileInfo{ +// Size: len(message.JpegThumbnail), +// Width: cfg.Width, +// Height: cfg.Height, +// MimeType: thumbnailMime, +// }, +// ThumbnailURL: uploadedThumbnail.ContentURI.CUString(), +// } +// } +// } +// +// portal.SetReply(content, message.ContextInfo) +// +// _, _ = intent.UserTyping(portal.MXID, false, 0) +// resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) +// if err != nil { +// portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) +// return +// } +// portal.finishHandling(source, message.Info.Source, resp.EventID) +//} - url := message.Url - if len(url) == 0 { - url = fmt.Sprintf("https://maps.google.com/?q=%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude) - } - name := message.Name - if len(name) == 0 { - latChar := 'N' - if message.DegreesLatitude < 0 { - latChar = 'S' - } - longChar := 'E' - if message.DegreesLongitude < 0 { - longChar = 'W' - } - name = fmt.Sprintf("%.4f° %c %.4f° %c", math.Abs(message.DegreesLatitude), latChar, math.Abs(message.DegreesLongitude), longChar) - } +//func (portal *Portal) HandleContactMessage(source *User, message whatsapp.ContactMessage) { +// intent := portal.startHandling(source, message.Info) +// if intent == nil { +// return +// } +// +// fileName := fmt.Sprintf("%s.vcf", message.DisplayName) +// data := []byte(message.Vcard) +// mimeType := "text/vcard" +// data, uploadMimeType, file := portal.encryptFile(data, mimeType) +// +// uploadResp, err := intent.UploadBytesWithName(data, uploadMimeType, fileName) +// if err != nil { +// portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err) +// return +// } +// +// content := &event.MessageEventContent{ +// Body: fileName, +// MsgType: event.MsgFile, +// File: file, +// Info: &event.FileInfo{ +// MimeType: mimeType, +// Size: len(message.Vcard), +// }, +// } +// if content.File != nil { +// content.File.URL = uploadResp.ContentURI.CUString() +// } else { +// content.URL = uploadResp.ContentURI.CUString() +// } +// +// portal.SetReply(content, message.ContextInfo) +// +// _, _ = intent.UserTyping(portal.MXID, false, 0) +// resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) +// if err != nil { +// portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) +// return +// } +// portal.finishHandling(source, message.Info.Source, resp.EventID) +//} - content := &event.MessageEventContent{ - MsgType: event.MsgLocation, - Body: fmt.Sprintf("Location: %s\n%s\n%s", name, message.Address, url), - Format: event.FormatHTML, - FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, message.Address), - GeoURI: fmt.Sprintf("geo:%.5f,%.5f", message.DegreesLatitude, message.DegreesLongitude), - } - - if len(message.JpegThumbnail) > 0 { - thumbnailMime := http.DetectContentType(message.JpegThumbnail) - uploadedThumbnail, _ := intent.UploadBytes(message.JpegThumbnail, thumbnailMime) - if uploadedThumbnail != nil { - cfg, _, _ := image.DecodeConfig(bytes.NewReader(message.JpegThumbnail)) - content.Info = &event.FileInfo{ - ThumbnailInfo: &event.FileInfo{ - Size: len(message.JpegThumbnail), - Width: cfg.Width, - Height: cfg.Height, - MimeType: thumbnailMime, - }, - ThumbnailURL: uploadedThumbnail.ContentURI.CUString(), - } - } - } - - portal.SetReply(content, message.ContextInfo) - - _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) - return - } - portal.finishHandling(source, message.Info.Source, resp.EventID) -} - -func (portal *Portal) HandleContactMessage(source *User, message whatsapp.ContactMessage) { - intent := portal.startHandling(source, message.Info) - if intent == nil { - return - } - - fileName := fmt.Sprintf("%s.vcf", message.DisplayName) - data := []byte(message.Vcard) - mimeType := "text/vcard" - data, uploadMimeType, file := portal.encryptFile(data, mimeType) - - uploadResp, err := intent.UploadBytesWithName(data, uploadMimeType, fileName) - if err != nil { - portal.log.Errorfln("Failed to upload vcard of %s: %v", message.DisplayName, err) - return - } - - content := &event.MessageEventContent{ - Body: fileName, - MsgType: event.MsgFile, - File: file, - Info: &event.FileInfo{ - MimeType: mimeType, - Size: len(message.Vcard), - }, - } - if content.File != nil { - content.File.URL = uploadResp.ContentURI.CUString() - } else { - content.URL = uploadResp.ContentURI.CUString() - } - - portal.SetReply(content, message.ContextInfo) - - _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) - if err != nil { - portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) - return - } - portal.finishHandling(source, message.Info.Source, resp.EventID) -} - -func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, bridgeErr error) { - portal.log.Errorfln("Failed to bridge media for %s: %v", info.Id, bridgeErr) - resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: "Failed to bridge media", - }, int64(info.Timestamp*1000)) - if err != nil { - portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) - } else { - portal.finishHandling(source, info.Source, resp.EventID) - } -} +//func (portal *Portal) sendMediaBridgeFailure(source *User, intent *appservice.IntentAPI, info whatsapp.MessageInfo, bridgeErr error) { +// portal.log.Errorfln("Failed to bridge media for %s: %v", info.Id, bridgeErr) +// resp, err := portal.sendMessage(intent, event.EventMessage, &event.MessageEventContent{ +// MsgType: event.MsgNotice, +// Body: "Failed to bridge media", +// }, int64(info.Timestamp*1000)) +// if err != nil { +// portal.log.Errorfln("Failed to send media download error message for %s: %v", info.Id, err) +// } else { +// portal.finishHandling(source, info.Source, resp.EventID) +// } +//} func (portal *Portal) encryptFile(data []byte, mimeType string) ([]byte, string, *event.EncryptedFileInfo) { if !portal.Encrypted { @@ -1823,9 +1831,9 @@ func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEven return true } -func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*waProto.WebMessageInfo, *User) { +func (portal *Portal) convertMatrixMessage(sender *User, evt *event.Event) (*groupme.Message, *User) { print("convertMatrixMessage") - return nil,nil + return nil, nil // content, ok := evt.Content.Parsed.(*event.MessageEventContent) // if !ok { // portal.log.Debugfln("Failed to handle event %s: unexpected parsed content type %T", evt.ID, evt.Content.Parsed) @@ -2005,18 +2013,20 @@ func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) { var timeout = errors.New("message sending timed out") func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { - if !portal.HasRelaybot() && ((portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) || - portal.sendMatrixConnectionError(sender, evt.ID)) { - return - } - portal.log.Debugfln("Received event %s", evt.ID) - info, sender := portal.convertMatrixMessage(sender, evt) - if info == nil { - return - } - portal.markHandled(sender, info, evt.ID) - portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.Key.GetId()) - portal.sendRaw(sender, evt, info, false) + println("handle matrix message") + return + // if !portal.HasRelaybot() && ((portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) || + // portal.sendMatrixConnectionError(sender, evt.ID)) { + // return + // } + // portal.log.Debugfln("Received event %s", evt.ID) + // info, sender := portal.convertMatrixMessage(sender, evt) + // if info == nil { + // return + // } + // portal.markHandled(sender, info, evt.ID) + // portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID) + // portal.sendRaw(sender, evt, info, false) } func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo, isRetry bool) { diff --git a/puppet.go b/puppet.go index 534fa98..28a4500 100644 --- a/puppet.go +++ b/puppet.go @@ -45,7 +45,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.GroupMeID, bool) { return "", false } - jid := types.GroupMeID(match[1] + whatsappExt.NewUserSuffix) + jid := types.GroupMeID(match[1]) return jid, true } @@ -159,6 +159,7 @@ type Puppet struct { } func (puppet *Puppet) PhoneNumber() string { + println("phone num") return strings.Replace(puppet.JID, whatsappExt.NewUserSuffix, "", 1) } diff --git a/types/types.go b/types/types.go index 5bd55a9..f5d264e 100644 --- a/types/types.go +++ b/types/types.go @@ -24,3 +24,5 @@ type WhatsAppMessageID = string //AuthToken is the authentication token type AuthToken = string + +type TmpID = GroupMeID diff --git a/user.go b/user.go index a3698ba..609946c 100644 --- a/user.go +++ b/user.go @@ -22,11 +22,11 @@ import ( "errors" "fmt" "net/http" + "sort" "strings" "sync" "time" - "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger/v2" "github.com/Rhymen/go-whatsapp" @@ -40,6 +40,7 @@ import ( "github.com/karmanyaahm/groupme" "maunium.net/go/mautrix-whatsapp/database" + "maunium.net/go/mautrix-whatsapp/groupmeExt" "maunium.net/go/mautrix-whatsapp/types" whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext" ) @@ -57,7 +58,7 @@ type User struct { IsRelaybot bool - Client *groupme.Client + Client *groupmeExt.Client ConnectionErrors int CommunityID string @@ -230,10 +231,10 @@ func (user *User) SetSession(session *whatsapp.Session) { // user.Update() } -func (user *User) Connect(evenIfNoLogin bool) bool { +func (user *User) Connect() bool { if user.Conn != nil { return true - } else if !evenIfNoLogin && len(user.Token) < 0 { + } else if len(user.Token) == 0 { return false } user.log.Debugln("Connecting to WhatsApp") @@ -260,8 +261,6 @@ func (user *User) Connect(evenIfNoLogin bool) bool { func (user *User) RestoreSession() bool { if len(user.Token) > 0 { - //sess, err := - user.Conn.SubscribeToUser(context.TODO(), groupme.ID(user.GetJID()), user.Token) // if err == whatsapp.ErrAlreadyLoggedIn { // return true // } else if err != nil { @@ -275,7 +274,6 @@ func (user *User) RestoreSession() bool { // } // user.log.Debugln("Disconnecting due to failed session restore...") // _, err := - user.Conn.Stop(context.TODO()) // if err != nil { // user.log.Errorln("Failed to disconnect after failed session restore:", err) // } @@ -283,29 +281,31 @@ func (user *User) RestoreSession() bool { // } user.Conn.SubscribeToUser(context.TODO(), groupme.ID(user.JID), user.Token) - + //TODO: typing notifics user.ConnectionErrors = 0 //user.SetSession(&sess) user.log.Debugln("Session restored successfully") user.PostLogin() + return true + } else { + user.log.Debugln("tried login but no token") + return false } - return true } func (user *User) HasSession() bool { - return user.Conn != nil && len(user.Token) > 0 + return len(user.Token) > 0 } func (user *User) IsConnected() bool { - // return user.Conn != nil && user.Conn.IsConnected() && user.Conn.IsLoggedIn() - return true + println("better connectoin check TODO") + return user.Conn != nil } func (user *User) IsLoggedIn() bool { return true } - func (user *User) IsLoginInProgress() bool { // return user.Conn != nil && user.Conn.IsLoginInProgress() return false @@ -318,59 +318,6 @@ func (user *User) GetJID() types.GroupMeID { } return user.JID } - -func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) { - var qrEventID id.EventID - for code := range qrChan { - if code == "stop" { - return - } - qrCode, err := qrcode.Encode(code, qrcode.Low, 256) - if err != nil { - user.log.Errorln("Failed to encode QR code:", err) - ce.Reply("Failed to encode QR code: %v", err) - return - } - - bot := user.bridge.AS.BotClient() - - resp, err := bot.UploadBytes(qrCode, "image/png") - if err != nil { - user.log.Errorln("Failed to upload QR code:", err) - ce.Reply("Failed to upload QR code: %v", err) - return - } - - if qrEventID == "" { - sendResp, err := bot.SendImage(ce.RoomID, code, resp.ContentURI) - if err != nil { - user.log.Errorln("Failed to send QR code to user:", err) - return - } - qrEventID = sendResp.EventID - eventIDChan <- qrEventID - } else { - _, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{ - MsgType: event.MsgImage, - Body: code, - URL: resp.ContentURI.CUString(), - NewContent: &event.MessageEventContent{ - MsgType: event.MsgImage, - Body: code, - URL: resp.ContentURI.CUString(), - }, - RelatesTo: &event.RelatesTo{ - Type: event.RelReplace, - EventID: qrEventID, - }, - }) - if err != nil { - user.log.Errorln("Failed to send edited QR code to user:", err) - } - } - } -} - func (user *User) Login(ce *CommandEvent) { // qrChan := make(chan string, 3) // eventIDChan := make(chan id.EventID, 1) @@ -416,13 +363,13 @@ func (user *User) Login(ce *CommandEvent) { user.addToJIDMap() //user.SetSession(&session) ce.Reply("Successfully logged in, synchronizing chats...") - // user.PostLogin() + user.PostLogin() } type Chat struct { Portal *Portal LastMessageTime uint64 - Contact whatsapp.Contact + Group groupme.Group } type ChatList []Chat @@ -521,10 +468,19 @@ func (user *User) postConnPing() bool { func (user *User) intPostLogin() { defer user.syncWait.Done() user.lastReconnection = time.Now().Unix() + user.Client = groupmeExt.NewClient(user.Token) + myuser, err := user.Client.MyUser(context.TODO()) + if err != nil { + log.Fatal(err) //TODO + } + user.JID = myuser.ID.String() + user.Update() + user.createCommunity() user.tryAutomaticDoublePuppeting() user.log.Debugln("Waiting for chat list receive confirmation") + user.HandleChatList() select { case <-user.chatListReceived: user.log.Debugln("Chat list receive confirmation received in PostLogin") @@ -565,84 +521,79 @@ func (user *User) HandleStreamEvent(evt whatsappExt.StreamEvent) { } } -func (user *User) HandleChatList(chats []whatsapp.Chat) { - // user.log.Infoln("Chat list received") - // chatMap := make(map[string]whatsapp.Chat) - // for _, chat := range user.Conn.Store.Chats { - // chatMap[chat.Jid] = chat - // } - // for _, chat := range chats { - // chatMap[chat.Jid] = chat - // } - // select { - // case user.chatListReceived <- struct{}{}: - // default: - // } - // go user.syncPortals(chatMap, false) +func (user *User) HandleChatList() { + chatMap := make(map[string]groupme.Group) + chats, err := user.Client.IndexAllGroups() + if err != nil { + log.Fatal(err) //TODO: handle + } + for _, chat := range chats { + chatMap[chat.ID.String()] = *chat + } + user.chatListReceived <- struct{}{} + user.log.Infoln("Chat list received") + go user.syncPortals(chatMap, false) } -func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) { - // if chatMap == nil { - // chatMap = user.Conn.Store.Chats - // } - // user.log.Infoln("Reading chat list") - // chats := make(ChatList, 0, len(chatMap)) - // existingKeys := user.GetInCommunityMap() - // portalKeys := make([]database.PortalKeyWithMeta, 0, len(chatMap)) - // for _, chat := range chatMap { - // ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64) - // if err != nil { - // user.log.Warnfln("Non-integer last message time in %s: %s", chat.Jid, chat.LastMessageTime) - // continue - // } - // portal := user.GetPortalByJID(chat.Jid) +func (user *User) syncPortals(chatMap map[string]groupme.Group, createAll bool) { + if chatMap == nil { + // chatMap = user.Conn.Store.Chats + log.Fatal("chatmap nil major oops") + } + user.log.Infoln("Reading chat list") - // chats = append(chats, Chat{ - // Portal: portal, - // Contact: user.Conn.Store.Contacts[chat.Jid], - // LastMessageTime: ts, - // }) - // var inCommunity, ok bool - // if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity { - // inCommunity = user.addPortalToCommunity(portal) - // if portal.IsPrivateChat() { - // puppet := user.bridge.GetPuppetByJID(portal.Key.JID) - // user.addPuppetToCommunity(puppet) - // } - // } - // portalKeys = append(portalKeys, database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity}) - // } - // user.log.Infoln("Read chat list, updating user-portal mapping") - // err := user.SetPortalKeys(portalKeys) - // if err != nil { - // user.log.Warnln("Failed to update user-portal mapping:", err) - // } - // sort.Sort(chats) - // limit := user.bridge.Config.Bridge.InitialChatSync - // if limit < 0 { - // limit = len(chats) - // } - // now := uint64(time.Now().Unix()) - // user.log.Infoln("Syncing portals") - // for i, chat := range chats { - // if chat.LastMessageTime+user.bridge.Config.Bridge.SyncChatMaxAge < now { - // break - // } - // create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit - // if len(chat.Portal.MXID) > 0 || create || createAll { - // chat.Portal.Sync(user, chat.Contact) - // err := chat.Portal.BackfillHistory(user, chat.LastMessageTime) - // if err != nil { - // chat.Portal.log.Errorln("Error backfilling history:", err) - // } - // } - // } - // user.UpdateDirectChats(nil) - // user.log.Infoln("Finished syncing portals") - // select { - // case user.syncPortalsDone <- struct{}{}: - // default: - // } + chats := make(ChatList, 0, len(chatMap)) + existingKeys := user.GetInCommunityMap() + portalKeys := make([]database.PortalKeyWithMeta, 0, len(chatMap)) + for _, chat := range chatMap { + portal := user.GetPortalByJID(chat.ID.String()) + + chats = append(chats, Chat{ + Portal: portal, + LastMessageTime: uint64(time.Now().Unix()), + Group: chat, + }) + var inCommunity, ok bool + if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity { + inCommunity = user.addPortalToCommunity(portal) + if portal.IsPrivateChat() { + puppet := user.bridge.GetPuppetByJID(portal.Key.JID) + user.addPuppetToCommunity(puppet) + } + } + portalKeys = append(portalKeys, database.PortalKeyWithMeta{PortalKey: portal.Key, InCommunity: inCommunity}) + } + user.log.Infoln("Read chat list, updating user-portal mapping") + err := user.SetPortalKeys(portalKeys) + if err != nil { + user.log.Warnln("Failed to update user-portal mapping:", err) + } + sort.Sort(chats) + limit := user.bridge.Config.Bridge.InitialChatSync + if limit < 0 { + limit = len(chats) + } + now := uint64(time.Now().Unix()) + user.log.Infoln("Syncing portals") + for i, chat := range chats { + if chat.LastMessageTime+user.bridge.Config.Bridge.SyncChatMaxAge < now { + break + } + create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit + if len(chat.Portal.MXID) > 0 || create || createAll { + chat.Portal.Sync(user, chat.Group) + err := chat.Portal.BackfillHistory(user, chat.LastMessageTime) + if err != nil { + chat.Portal.log.Errorln("Error backfilling history:", err) + } + } + } + user.UpdateDirectChats(nil) + user.log.Infoln("Finished syncing portals") + select { + case user.syncPortalsDone <- struct{}{}: + default: + } } func (user *User) getDirectChats() map[id.UserID][]id.RoomID { @@ -881,41 +832,41 @@ func (user *User) HandleBatteryMessage(battery whatsapp.BatteryMessage) { } } -func (user *User) HandleTextMessage(message whatsapp.TextMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +func (user *User) HandleTextMessage(message groupme.Message) { + user.messageInput <- PortalMessage{message.GroupID.String(), user, &message, uint64(message.CreatedAt.ToTime().Unix())} } -func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleContactMessage(message whatsapp.ContactMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) { - user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} -} - -func (user *User) HandleMessageRevoke(message whatsappExt.MessageRevocation) { - user.messageInput <- PortalMessage{message.RemoteJid, user, message, 0} -} +//func (user *User) HandleImageMessage(message whatsapp.ImageMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleStickerMessage(message whatsapp.StickerMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleVideoMessage(message whatsapp.VideoMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleAudioMessage(message whatsapp.AudioMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleDocumentMessage(message whatsapp.DocumentMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleContactMessage(message whatsapp.ContactMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) { +// user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +//} +// +//func (user *User) HandleMessageRevoke(message whatsappExt.MessageRevocation) { +// user.messageInput <- PortalMessage{message.RemoteJid, user, message, 0} +//} type FakeMessage struct { Text string @@ -923,40 +874,40 @@ type FakeMessage struct { Alert bool } -func (user *User) HandleCallInfo(info whatsappExt.CallInfo) { - if info.Data != nil { - return - } - data := FakeMessage{ - ID: info.ID, - } - switch info.Type { - case whatsappExt.CallOffer: - if !user.bridge.Config.Bridge.CallNotices.Start { - return - } - data.Text = "Incoming call" - data.Alert = true - case whatsappExt.CallOfferVideo: - if !user.bridge.Config.Bridge.CallNotices.Start { - return - } - data.Text = "Incoming video call" - data.Alert = true - case whatsappExt.CallTerminate: - if !user.bridge.Config.Bridge.CallNotices.End { - return - } - data.Text = "Call ended" - data.ID += "E" - default: - return - } - portal := user.GetPortalByJID(info.From) - if portal != nil { - portal.messages <- PortalMessage{info.From, user, data, 0} - } -} +//func (user *User) HandleCallInfo(info whatsappExt.CallInfo) { +// if info.Data != nil { +// return +// } +// data := FakeMessage{ +// ID: info.ID, +// } +// switch info.Type { +// case whatsappExt.CallOffer: +// if !user.bridge.Config.Bridge.CallNotices.Start { +// return +// } +// data.Text = "Incoming call" +// data.Alert = true +// case whatsappExt.CallOfferVideo: +// if !user.bridge.Config.Bridge.CallNotices.Start { +// return +// } +// data.Text = "Incoming video call" +// data.Alert = true +// case whatsappExt.CallTerminate: +// if !user.bridge.Config.Bridge.CallNotices.End { +// return +// } +// data.Text = "Call ended" +// data.ID += "E" +// default: +// return +// } +// portal := user.GetPortalByJID(info.From) +// if portal != nil { +// portal.messages <- PortalMessage{info.From, user, data, 0} +// } +//} func (user *User) HandlePresence(info whatsappExt.Presence) { puppet := user.bridge.GetPuppetByJID(info.SenderJID) @@ -1119,10 +1070,10 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) { go portal.HandleWhatsAppKick(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) case whatsappExt.ChatActionAdd: go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) - case whatsappExt.ChatActionIntroduce: - if cmd.Data.SenderJID != "unknown" { - go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID}) - } + //case whatsappExt.ChatActionIntroduce: + // if cmd.Data.SenderJID != "unknown" { + // go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID}) + // } } }