Преглед изворни кода

Try to track when the phone is online and warn user if it's offline for too long

Tulir Asokan пре 3 година
родитељ
комит
e8c77c7ec6
12 измењених фајлова са 154 додато и 42 уклоњено
  1. 11 2
      bridgestate.go
  2. 2 0
      config/bridge.go
  3. 1 0
      config/upgrade.go
  4. 10 0
      database/upgrades/2022-01-24-phone-last-seen-ts.go
  5. 1 1
      database/upgrades/upgrades.go
  6. 21 8
      database/user.go
  7. 4 7
      disappear.go
  8. 3 0
      example-config.yaml
  9. 4 4
      go.mod
  10. 9 8
      go.sum
  11. 19 1
      main.go
  12. 69 11
      user.go

+ 11 - 2
bridgestate.go

@@ -47,17 +47,19 @@ const (
 type BridgeErrorCode string
 
 const (
-	WANotLoggedIn   BridgeErrorCode = "wa-logged-out"
+	WALoggedOut     BridgeErrorCode = "wa-logged-out"
 	WANotConnected  BridgeErrorCode = "wa-not-connected"
 	WAConnecting    BridgeErrorCode = "wa-connecting"
 	WAServerTimeout BridgeErrorCode = "wa-server-timeout"
+	WAPhoneOffline  BridgeErrorCode = "wa-phone-offline"
 )
 
 var bridgeHumanErrors = map[BridgeErrorCode]string{
-	WANotLoggedIn:   "You're not logged into WhatsApp",
+	WALoggedOut:     "You were logged out from another device. Relogin to continue using the bridge.",
 	WANotConnected:  "You're not connected to WhatsApp",
 	WAConnecting:    "Reconnecting to WhatsApp...",
 	WAServerTimeout: "The WhatsApp web servers are not responding. The bridge will try to reconnect.",
+	WAPhoneOffline:  "Your phone hasn't been seen in over 12 days. The bridge is currently connected, but will get disconnected if you don't open the app soon.",
 }
 
 type BridgeState struct {
@@ -172,6 +174,13 @@ func (user *User) sendBridgeState(state BridgeState) {
 	}
 }
 
+func (user *User) GetPrevBridgeState() BridgeState {
+	if user.prevBridgeStatus != nil {
+		return *user.prevBridgeStatus
+	}
+	return BridgeState{}
+}
+
 func (prov *ProvisioningAPI) BridgeStatePing(w http.ResponseWriter, r *http.Request) {
 	if !prov.bridge.AS.CheckServerToken(w, r) {
 		return

+ 2 - 0
config/bridge.go

@@ -75,6 +75,8 @@ type BridgeConfig struct {
 
 	DisappearingMessagesInGroups bool `yaml:"disappearing_messages_in_groups"`
 
+	DisableBridgeAlerts bool `yaml:"disable_bridge_alerts"`
+
 	CommandPrefix string `yaml:"command_prefix"`
 
 	ManagementRoomText struct {

+ 1 - 0
config/upgrade.go

@@ -103,6 +103,7 @@ func (helper *UpgradeHelper) doUpgrade() {
 	helper.Copy(Str, "bridge", "command_prefix")
 	helper.Copy(Bool, "bridge", "federate_rooms")
 	helper.Copy(Bool, "bridge", "disappearing_messages_in_groups")
+	helper.Copy(Bool, "bridge", "disable_bridge_alerts")
 	helper.Copy(Str, "bridge", "management_room_text", "welcome")
 	helper.Copy(Str, "bridge", "management_room_text", "welcome_connected")
 	helper.Copy(Str, "bridge", "management_room_text", "welcome_unconnected")

+ 10 - 0
database/upgrades/2022-01-24-phone-last-seen-ts.go

@@ -0,0 +1,10 @@
+package upgrades
+
+import "database/sql"
+
+func init() {
+	upgrades[35] = upgrade{"Store approximate last seen timestamp of the main device", func(tx *sql.Tx, ctx context) error {
+		_, err := tx.Exec(`ALTER TABLE "user" ADD COLUMN phone_last_seen BIGINT`)
+		return err
+	}}
+}

+ 1 - 1
database/upgrades/upgrades.go

@@ -40,7 +40,7 @@ type upgrade struct {
 	fn      upgradeFunc
 }
 
-const NumberOfUpgrades = 35
+const NumberOfUpgrades = 36
 
 var upgrades [NumberOfUpgrades]upgrade
 

+ 21 - 8
database/user.go

@@ -44,7 +44,7 @@ func (uq *UserQuery) New() *User {
 }
 
 func (uq *UserQuery) GetAll() (users []*User) {
-	rows, err := uq.db.Query(`SELECT mxid, username, agent, device, management_room, space_room FROM "user"`)
+	rows, err := uq.db.Query(`SELECT mxid, username, agent, device, management_room, space_room, phone_last_seen FROM "user"`)
 	if err != nil || rows == nil {
 		return nil
 	}
@@ -56,7 +56,7 @@ func (uq *UserQuery) GetAll() (users []*User) {
 }
 
 func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
-	row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room, space_room FROM "user" WHERE mxid=$1`, userID)
+	row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room, space_room, phone_last_seen FROM "user" WHERE mxid=$1`, userID)
 	if row == nil {
 		return nil
 	}
@@ -64,7 +64,7 @@ func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
 }
 
 func (uq *UserQuery) GetByUsername(username string) *User {
-	row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room, space_room FROM "user" WHERE username=$1`, username)
+	row := uq.db.QueryRow(`SELECT mxid, username, agent, device, management_room, space_room, phone_last_seen FROM "user" WHERE username=$1`, username)
 	if row == nil {
 		return nil
 	}
@@ -79,6 +79,7 @@ type User struct {
 	JID            types.JID
 	ManagementRoom id.RoomID
 	SpaceRoom      id.RoomID
+	PhoneLastSeen  time.Time
 
 	lastReadCache     map[PortalKey]time.Time
 	lastReadCacheLock sync.Mutex
@@ -89,7 +90,8 @@ type User struct {
 func (user *User) Scan(row Scannable) *User {
 	var username sql.NullString
 	var device, agent sql.NullByte
-	err := row.Scan(&user.MXID, &username, &agent, &device, &user.ManagementRoom, &user.SpaceRoom)
+	var phoneLastSeen sql.NullInt64
+	err := row.Scan(&user.MXID, &username, &agent, &device, &user.ManagementRoom, &user.SpaceRoom, &phoneLastSeen)
 	if err != nil {
 		if err != sql.ErrNoRows {
 			user.log.Errorln("Database scan failed:", err)
@@ -99,6 +101,9 @@ func (user *User) Scan(row Scannable) *User {
 	if len(username.String) > 0 {
 		user.JID = types.NewADJID(username.String, agent.Byte, device.Byte)
 	}
+	if phoneLastSeen.Valid {
+		user.PhoneLastSeen = time.Unix(phoneLastSeen.Int64, 0)
+	}
 	return user
 }
 
@@ -123,17 +128,25 @@ func (user *User) devicePtr() *uint8 {
 	return nil
 }
 
+func (user *User) phoneLastSeenPtr() *int64 {
+	if user.PhoneLastSeen.IsZero() {
+		return nil
+	}
+	ts := user.PhoneLastSeen.Unix()
+	return &ts
+}
+
 func (user *User) Insert() {
-	_, err := user.db.Exec(`INSERT INTO "user" (mxid, username, agent, device, management_room, space_room) VALUES ($1, $2, $3, $4, $5, $6)`,
-		user.MXID, user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.SpaceRoom)
+	_, err := user.db.Exec(`INSERT INTO "user" (mxid, username, agent, device, management_room, space_room, phone_last_seen) VALUES ($1, $2, $3, $4, $5, $6, $7)`,
+		user.MXID, user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.SpaceRoom, user.phoneLastSeenPtr())
 	if err != nil {
 		user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
 	}
 }
 
 func (user *User) Update() {
-	_, err := user.db.Exec(`UPDATE "user" SET username=$1, agent=$2, device=$3, management_room=$4, space_room=$5 WHERE mxid=$6`,
-		user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.SpaceRoom, user.MXID)
+	_, err := user.db.Exec(`UPDATE "user" SET username=$1, agent=$2, device=$3, management_room=$4, space_room=$5, phone_last_seen=$6 WHERE mxid=$7`,
+		user.usernamePtr(), user.agentPtr(), user.devicePtr(), user.ManagementRoom, user.SpaceRoom, user.phoneLastSeenPtr(), user.MXID)
 	if err != nil {
 		user.log.Warnfln("Failed to update %s: %v", user.MXID, err)
 	}

+ 4 - 7
disappear.go

@@ -50,13 +50,10 @@ func (portal *Portal) ScheduleDisappearing() {
 	}
 }
 
-func (bridge *Bridge) DisappearingLoop() {
-	for {
-		for _, msg := range bridge.DB.DisappearingMessage.GetUpcomingScheduled(1 * time.Hour) {
-			portal := bridge.GetPortalByMXID(msg.RoomID)
-			go portal.sleepAndDelete(msg)
-		}
-		time.Sleep(1 * time.Hour)
+func (bridge *Bridge) SleepAndDeleteUpcoming() {
+	for _, msg := range bridge.DB.DisappearingMessage.GetUpcomingScheduled(1 * time.Hour) {
+		portal := bridge.GetPortalByMXID(msg.RoomID)
+		go portal.sleepAndDelete(msg)
 	}
 }
 

+ 3 - 0
example-config.yaml

@@ -191,6 +191,9 @@ bridge:
     # the messages will be determined by the first user to read the message, rather than individually.
     # If the bridge only has a single user, this can be turned on safely.
     disappearing_messages_in_groups: false
+    # Should the bridge never send alerts to the bridge management room?
+    # These are mostly things like the user being logged out.
+    disable_bridge_alerts: false
 
     # The prefix for commands. Only required in non-management rooms.
     command_prefix: "!wa"

+ 4 - 4
go.mod

@@ -9,13 +9,13 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.10
 	github.com/prometheus/client_golang v1.11.0
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	go.mau.fi/whatsmeow v0.0.0-20220111203410-b078a9e90863
+	go.mau.fi/whatsmeow v0.0.0-20220124150706-afc33ee3c21a
 	golang.org/x/image v0.0.0-20211028202545-6944b10bf410
 	google.golang.org/protobuf v1.27.1
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 	maunium.net/go/mauflag v1.0.0
 	maunium.net/go/maulogger/v2 v2.3.2
-	maunium.net/go/mautrix v0.10.11-0.20220117200125-6d9d537973fa
+	maunium.net/go/mautrix v0.10.11-0.20220118151622-7cc9a5066c70
 )
 
 require (
@@ -29,10 +29,10 @@ require (
 	github.com/prometheus/common v0.26.0 // indirect
 	github.com/prometheus/procfs v0.6.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/tidwall/gjson v1.10.2 // indirect
+	github.com/tidwall/gjson v1.13.0 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
-	github.com/tidwall/sjson v1.2.3 // indirect
+	github.com/tidwall/sjson v1.2.4 // indirect
 	go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910 // indirect
 	golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
 	golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect

+ 9 - 8
go.sum

@@ -129,18 +129,19 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo=
-github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.13.0 h1:3TFY9yxOQShrvmjdM76K+jc66zJeT6D3/VFFYCGQf7M=
+github.com/tidwall/gjson v1.13.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8=
-github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs=
+github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc=
+github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM=
 go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910 h1:9FFhG0OmkuMau5UEaTgiUQ+7cSbtbOQ7hiWKdN8OI3I=
 go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910/go.mod h1:AufGrvVh+00Nc07Jm4hTquh7yleZyn20tKJI2wCPAKg=
-go.mau.fi/whatsmeow v0.0.0-20220111203410-b078a9e90863 h1:5xGt9ghwG3XvlCAnq1WJuJ4mdOR6u/Ho5oYR0Ql9uFw=
-go.mau.fi/whatsmeow v0.0.0-20220111203410-b078a9e90863/go.mod h1:8jUjOAi3xtGubxcZgG8uSHpAdyQXBRbWAfxkctX/4y4=
+go.mau.fi/whatsmeow v0.0.0-20220124150706-afc33ee3c21a h1:e8aExGixi/O+kveh6S3wgydk9ogU5+gx0NqOmqWMapM=
+go.mau.fi/whatsmeow v0.0.0-20220124150706-afc33ee3c21a/go.mod h1:8jUjOAi3xtGubxcZgG8uSHpAdyQXBRbWAfxkctX/4y4=
 golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -222,5 +223,5 @@ maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
 maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
 maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0=
 maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
-maunium.net/go/mautrix v0.10.11-0.20220117200125-6d9d537973fa h1:PA9cvjbiV2tqd5qgrnk42G4HfSLVWWqs27dKKvG1tCQ=
-maunium.net/go/mautrix v0.10.11-0.20220117200125-6d9d537973fa/go.mod h1:4XljZZGZiIlpfbQ+Tt2ykjapskJ8a7Z2i9y/+YaceF8=
+maunium.net/go/mautrix v0.10.11-0.20220118151622-7cc9a5066c70 h1:T/NnQ9DC/nSX+4wnpyl8tf+c8XjRxt7A3RAYfdAtevA=
+maunium.net/go/mautrix v0.10.11-0.20220118151622-7cc9a5066c70/go.mod h1:lm06wYU/IcPcdicMNrG9wj0t3xhqYpEA1k+4G9EGZwc=

+ 19 - 1
main.go

@@ -333,10 +333,28 @@ func (bridge *Bridge) Start() {
 	if bridge.Config.Bridge.ResendBridgeInfo {
 		go bridge.ResendBridgeInfo()
 	}
-	go bridge.DisappearingLoop()
+	go bridge.Loop()
 	bridge.AS.Ready = true
 }
 
+func (bridge *Bridge) Loop() {
+	for {
+		bridge.SleepAndDeleteUpcoming()
+		time.Sleep(1 * time.Hour)
+		bridge.WarnUsersAboutDisconnection()
+	}
+}
+
+func (bridge *Bridge) WarnUsersAboutDisconnection() {
+	bridge.usersLock.Lock()
+	for _, user := range bridge.usersByUsername {
+		if user.IsConnected() && !user.PhoneRecentlySeen() {
+			go user.sendPhoneOfflineWarning()
+		}
+	}
+	bridge.usersLock.Unlock()
+}
+
 func (bridge *Bridge) ResendBridgeInfo() {
 	if *dontSaveConfig {
 		bridge.Log.Warnln("Not setting resend_bridge_info to false in config due to --no-update flag")

+ 69 - 11
user.go

@@ -21,6 +21,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"math"
 	"net/http"
 	"strconv"
 	"strings"
@@ -66,7 +67,8 @@ type User struct {
 	prevBridgeStatus *BridgeState
 	lastPresence     types.Presence
 
-	spaceMembershipChecked bool
+	spaceMembershipChecked  bool
+	lastPhoneOfflineWarning time.Time
 }
 
 func (bridge *Bridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User {
@@ -119,7 +121,7 @@ func (user *User) removeFromJIDMap(state BridgeStateEvent) {
 	}
 	user.bridge.usersLock.Unlock()
 	user.bridge.Metrics.TrackLoginState(user.JID, false)
-	user.sendBridgeState(BridgeState{StateEvent: state, Error: WANotLoggedIn})
+	user.sendBridgeState(BridgeState{StateEvent: state})
 }
 
 func (bridge *Bridge) GetAllUsers() []*User {
@@ -425,15 +427,10 @@ func (user *User) tryAutomaticDoublePuppeting() {
 	user.log.Infoln("Successfully automatically enabled custom puppet")
 }
 
-func (user *User) sendBridgeNotice(formatString string, args ...interface{}) {
-	notice := fmt.Sprintf(formatString, args...)
-	_, err := user.bridge.Bot.SendNotice(user.GetManagementRoom(), notice)
-	if err != nil {
-		user.log.Warnf("Failed to send bridge notice \"%s\": %v", notice, err)
-	}
-}
-
 func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface{}) {
+	if user.bridge.Config.Bridge.DisableBridgeAlerts {
+		return
+	}
 	notice := fmt.Sprintf(formatString, args...)
 	content := format.RenderMarkdown(notice, true, false)
 	_, err := user.bridge.Bot.SendMessageEvent(user.GetManagementRoom(), event.EventMessage, content)
@@ -465,6 +462,49 @@ func (user *User) handleCallStart(sender types.JID, id, callType string, ts time
 	}
 }
 
+const PhoneDisconnectWarningTime = 12 * 24 * time.Hour // 12 days
+
+func (user *User) PhoneRecentlySeen() bool {
+	return user.PhoneLastSeen.IsZero() || user.PhoneLastSeen.Add(PhoneDisconnectWarningTime).After(time.Now())
+}
+
+// phoneSeen records a timestamp when the user's main device was seen online.
+// The stored timestamp can later be used to warn the user if the main device is offline for too long.
+func (user *User) phoneSeen(ts time.Time) {
+	if user.PhoneLastSeen.Add(1 * time.Hour).After(ts) {
+		// The last seen timestamp isn't going to be perfectly accurate in any case,
+		// so don't spam the database with an update every time there's an event.
+		return
+	} else if !user.PhoneRecentlySeen() && user.GetPrevBridgeState().Error == WAPhoneOffline && user.IsConnected() {
+		user.log.Debugfln("Saw phone after current bridge state said it has been offline, switching state back to connected")
+		go user.sendBridgeState(BridgeState{StateEvent: StateConnected})
+	}
+	user.PhoneLastSeen = ts
+	go user.Update()
+}
+
+func formatDisconnectTime(dur time.Duration) string {
+	days := int(math.Floor(dur.Hours() / 24))
+	hours := int(dur.Hours()) % 24
+	if hours == 0 {
+		return fmt.Sprintf("%d days", days)
+	} else if hours == 1 {
+		return fmt.Sprintf("%d days and 1 hour", days)
+	} else {
+		return fmt.Sprintf("%d days and %d hours", days, hours)
+	}
+}
+
+func (user *User) sendPhoneOfflineWarning() {
+	if user.lastPhoneOfflineWarning.Add(12 * time.Hour).After(time.Now()) {
+		// Don't spam the warning too much
+		return
+	}
+	user.lastPhoneOfflineWarning = time.Now()
+	timeSinceSeen := time.Now().Sub(user.PhoneLastSeen)
+	user.sendMarkdownBridgeAlert("Your phone hasn't been seen in %s. The server will force the bridge to log out if the phone is not active at least every 2 weeks.", formatDisconnectTime(timeSinceSeen))
+}
+
 func (user *User) HandleEvent(event interface{}) {
 	switch v := event.(type) {
 	case *events.LoggedOut:
@@ -484,6 +524,20 @@ func (user *User) HandleEvent(event interface{}) {
 			}()
 		}
 		go user.tryAutomaticDoublePuppeting()
+	case *events.OfflineSyncPreview:
+		user.log.Infofln("Server says it's going to send %d messages and %d receipts that were missed during downtime", v.Messages, v.Receipts)
+		go user.sendBridgeState(BridgeState{
+			StateEvent: StateBackfilling,
+			Message:    fmt.Sprintf("backfilling %d messages and %d receipts", v.Messages, v.Receipts),
+		})
+	case *events.OfflineSyncCompleted:
+		if !user.PhoneRecentlySeen() {
+			user.log.Infofln("Offline sync completed, but phone last seen date is still %s - sending phone offline bridge status", user.PhoneLastSeen)
+			go user.sendBridgeState(BridgeState{StateEvent: StateTransientDisconnect, Error: WAPhoneOffline})
+		} else if user.GetPrevBridgeState().StateEvent == StateBackfilling {
+			user.log.Infoln("Offline sync completed")
+			go user.sendBridgeState(BridgeState{StateEvent: StateConnected})
+		}
 	case *events.AppStateSyncComplete:
 		if len(user.Client.Store.PushName) > 0 && v.Name == appstate.WAPatchCriticalBlock {
 			err := user.Client.SendPresence(user.lastPresence)
@@ -506,6 +560,7 @@ func (user *User) HandleEvent(event interface{}) {
 			user.log.Warnln("Failed to send presence after push name update:", err)
 		}
 	case *events.PairSuccess:
+		user.PhoneLastSeen = time.Now()
 		user.Session = user.Client.Store
 		user.JID = v.ID
 		user.addToJIDMap()
@@ -527,6 +582,9 @@ func (user *User) HandleEvent(event interface{}) {
 	case *events.Picture:
 		go user.handlePictureUpdate(v)
 	case *events.Receipt:
+		if v.IsFromMe && v.Sender.Device == 0 {
+			user.phoneSeen(v.Timestamp)
+		}
 		go user.handleReceipt(v)
 	case *events.ChatPresence:
 		go user.handleChatPresence(v)
@@ -752,7 +810,7 @@ func (user *User) UpdateDirectChats(chats map[id.UserID][]id.RoomID) {
 }
 
 func (user *User) handleLoggedOut(onConnect bool) {
-	user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WANotLoggedIn})
+	user.sendBridgeState(BridgeState{StateEvent: StateBadCredentials, Error: WALoggedOut})
 	user.JID = types.EmptyJID
 	user.Update()
 	if onConnect {