|
@@ -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 {
|