github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/maps/livelocation.go (about)

     1  package maps
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/chat/globals"
    11  	"github.com/keybase/client/go/chat/storage"
    12  	"github.com/keybase/client/go/chat/types"
    13  	"github.com/keybase/client/go/chat/utils"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	"github.com/keybase/clockwork"
    19  	"golang.org/x/sync/errgroup"
    20  )
    21  
    22  type LiveLocationTracker struct {
    23  	globals.Contextified
    24  	utils.DebugLabeler
    25  	sync.Mutex
    26  
    27  	clock          clockwork.Clock
    28  	storage        *trackStorage
    29  	updateInterval time.Duration
    30  	uid            gregor1.UID
    31  	eg             errgroup.Group
    32  	trackers       map[types.LiveLocationKey]*locationTrack
    33  	lastCoord      chat1.Coordinate
    34  	maxCoords      int
    35  
    36  	// testing only
    37  	TestingCoordsAddedCh chan struct{}
    38  }
    39  
    40  func NewLiveLocationTracker(g *globals.Context) *LiveLocationTracker {
    41  	return &LiveLocationTracker{
    42  		Contextified: globals.NewContextified(g),
    43  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "LiveLocationTracker", false),
    44  
    45  		storage:        newTrackStorage(g),
    46  		trackers:       make(map[types.LiveLocationKey]*locationTrack),
    47  		updateInterval: 30 * time.Second,
    48  		maxCoords:      500,
    49  		clock:          clockwork.NewRealClock(),
    50  	}
    51  }
    52  
    53  func (l *LiveLocationTracker) Start(ctx context.Context, uid gregor1.UID) {
    54  	defer l.Trace(ctx, nil, "Start")()
    55  	l.Lock()
    56  	defer l.Unlock()
    57  	l.uid = uid
    58  	// bring back any trackers that we have stored. This is most relavent when being woken
    59  	// up on iOS due to a location update. THe app might need to recreate all of its trackers
    60  	// if the app had been killed.
    61  	l.restoreLocked(ctx)
    62  }
    63  
    64  func (l *LiveLocationTracker) Stop(ctx context.Context) chan struct{} {
    65  	defer l.Trace(ctx, nil, "Stop")()
    66  	l.Lock()
    67  	defer l.Unlock()
    68  	ch := make(chan struct{})
    69  	for _, t := range l.trackers {
    70  		t.Stop()
    71  	}
    72  	go func() {
    73  		_ = l.eg.Wait()
    74  		close(ch)
    75  	}()
    76  	return ch
    77  }
    78  
    79  func (l *LiveLocationTracker) ActivelyTracking(ctx context.Context) bool {
    80  	l.Lock()
    81  	defer l.Unlock()
    82  	return len(l.trackers) > 0
    83  }
    84  
    85  func (l *LiveLocationTracker) saveLocked(ctx context.Context) {
    86  	var trackers []*locationTrack
    87  	for _, t := range l.trackers {
    88  		trackers = append(trackers, t)
    89  	}
    90  	if err := l.storage.Save(ctx, trackers); err != nil {
    91  		l.Debug(ctx, "save: failed to save: %s", err)
    92  	}
    93  }
    94  
    95  func (l *LiveLocationTracker) restoreLocked(ctx context.Context) {
    96  	trackers, err := l.storage.Restore(ctx)
    97  	if err != nil {
    98  		l.Debug(ctx, "restoreLocked: failed to read, skipping: %s", err)
    99  		return
   100  	}
   101  	if len(trackers) == 0 {
   102  		return
   103  	}
   104  	l.Debug(ctx, "restoreLocked: restored %d trackers", len(trackers))
   105  	l.trackers = make(map[types.LiveLocationKey]*locationTrack)
   106  	for _, t := range trackers {
   107  		if t.IsStopped() {
   108  			continue
   109  		}
   110  		l.trackers[t.Key()] = t
   111  		myT := t
   112  		l.eg.Go(func() error {
   113  			return l.tracker(myT)
   114  		})
   115  	}
   116  }
   117  
   118  func (l *LiveLocationTracker) getChatUI(ctx context.Context) libkb.ChatUI {
   119  	ui, err := l.G().UIRouter.GetChatUI()
   120  	if err != nil || ui == nil {
   121  		l.Debug(ctx, "getChatUI: no chat UI found: err: %s", err)
   122  		return utils.NullChatUI{}
   123  	}
   124  	return ui
   125  }
   126  
   127  type unfurlNotifyListener struct {
   128  	globals.Contextified
   129  	utils.DebugLabeler
   130  	libkb.NoopNotifyListener
   131  
   132  	outboxID chat1.OutboxID
   133  	doneCh   chan struct{}
   134  }
   135  
   136  func newUnfurlNotifyListener(g *globals.Context, outboxID chat1.OutboxID, doneCh chan struct{}) *unfurlNotifyListener {
   137  	return &unfurlNotifyListener{
   138  		Contextified: globals.NewContextified(g),
   139  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "maps.unfurlNotifyListener", false),
   140  		outboxID:     outboxID,
   141  		doneCh:       doneCh,
   142  	}
   143  }
   144  
   145  func (n *unfurlNotifyListener) NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity,
   146  	source chat1.ChatActivitySource) {
   147  	ctx := context.Background()
   148  	st, err := activity.ActivityType()
   149  	if err != nil {
   150  		n.Debug(ctx, "NewChatActivity: failed to get type: %s", err)
   151  		return
   152  	}
   153  	switch st {
   154  	case chat1.ChatActivityType_INCOMING_MESSAGE:
   155  		msg := activity.IncomingMessage().Message
   156  		if msg.IsOutbox() {
   157  			return
   158  		}
   159  		if n.outboxID.Eq(msg.GetOutboxID()) {
   160  			n.doneCh <- struct{}{}
   161  		}
   162  	case chat1.ChatActivityType_FAILED_MESSAGE:
   163  		recs := activity.FailedMessage().OutboxRecords
   164  		for _, r := range recs {
   165  			if n.outboxID.Eq(&r.OutboxID) {
   166  				n.doneCh <- struct{}{}
   167  				break
   168  			}
   169  		}
   170  	}
   171  }
   172  
   173  func (l *LiveLocationTracker) updateMapUnfurl(ctx context.Context, t *locationTrack, done bool) (err error) {
   174  	ctx = globals.ChatCtx(ctx, l.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil)
   175  	defer l.Trace(ctx, &err, "updateMapUnfurl")()
   176  	msg, err := l.G().ChatHelper.GetMessage(ctx, l.uid, t.convID, t.msgID, true, nil)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	if !msg.IsValid() {
   181  		return errors.New("invalid message")
   182  	}
   183  	mvalid := msg.Valid()
   184  	var coords []chat1.Coordinate
   185  	trackerCoords := t.GetCoords()
   186  	if len(trackerCoords) == 0 {
   187  		if !l.lastCoord.IsZero() {
   188  			coords = []chat1.Coordinate{l.lastCoord}
   189  		} else {
   190  			return errors.New("no coordinates")
   191  		}
   192  	} else {
   193  		coords = trackerCoords
   194  	}
   195  	last := coords[len(coords)-1]
   196  	conv, err := utils.GetVerifiedConv(ctx, l.G(), l.uid, t.convID, types.InboxSourceDataSourceAll)
   197  	if err != nil {
   198  		return err
   199  	}
   200  
   201  	// Prefetch the next unfurl, and then delete any others. We do the prefetch so that there isn't
   202  	// a large lag after we delete the unfurl and when we post the next one. We link back to the
   203  	// tracker in the URL so we can get all the coordinates in the scraper. The cb param
   204  	// makes it so the unfurler doesn't think it has already unfurled this URL and skips it.
   205  	body := fmt.Sprintf("https://%s/?lat=%f&lon=%f&acc=%f&cb=%s&done=%v", types.MapsDomain,
   206  		last.Lat, last.Lon, last.Accuracy, libkb.RandStringB64(3), done)
   207  	if !t.getCurrentPosition {
   208  		body += fmt.Sprintf("&livekey=%s", t.Key())
   209  	}
   210  	l.G().Unfurler.Prefetch(ctx, l.uid, t.convID, body)
   211  	for unfurlMsgID := range mvalid.Unfurls {
   212  		// delete the old unfurl first to make way for the new
   213  		if err := l.G().ChatHelper.DeleteMsg(ctx, t.convID, conv.Info.TlfName, unfurlMsgID); err != nil {
   214  			return err
   215  		}
   216  	}
   217  
   218  	// Create a new unfurl on the new URL, and wait for it to complete before charging forward. This way
   219  	// we won't get into a state with multiple unfurls in the thread
   220  	mvalid.MessageBody = chat1.NewMessageBodyWithText(chat1.MessageText{
   221  		Body: body,
   222  	})
   223  	newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
   224  	unfurlDoneCh := make(chan struct{}, 10)
   225  	outboxID := storage.GetOutboxIDFromURL(body, t.convID, newMsg)
   226  	listenerID := l.G().NotifyRouter.AddListener(newUnfurlNotifyListener(l.G(), outboxID, unfurlDoneCh))
   227  	l.G().Unfurler.UnfurlAndSend(ctx, l.uid, t.convID, newMsg)
   228  	select {
   229  	case <-unfurlDoneCh:
   230  	case <-time.After(time.Minute):
   231  		l.Debug(ctx, "updateMapUnfurl: timed out waiting for unfurl callback, charging...")
   232  	}
   233  	l.G().NotifyRouter.RemoveListener(listenerID)
   234  	return nil
   235  }
   236  
   237  func (l *LiveLocationTracker) startWatch(ctx context.Context, t *locationTrack) (watchID chat1.LocationWatchID, err error) {
   238  	// try this a couple times in case we are starting fresh and the UI isn't ready yet
   239  	maxWatchAttempts := 20
   240  	watchAttempts := 0
   241  	for {
   242  		if watchID, err = l.getChatUI(ctx).ChatWatchPosition(ctx, t.convID, t.perm); err != nil {
   243  			l.Debug(ctx, "startWatch: unable to watch position: attempt: %d msg: %s", watchAttempts, err)
   244  			if watchAttempts > maxWatchAttempts {
   245  				return 0, err
   246  			}
   247  		} else {
   248  			break
   249  		}
   250  		maxWatchAttempts++
   251  		time.Sleep(time.Second)
   252  	}
   253  	return watchID, nil
   254  }
   255  
   256  func (l *LiveLocationTracker) tracker(t *locationTrack) error {
   257  	ctx := context.Background()
   258  	// check to see if we are being asked to start a tracker that is already expired
   259  	if t.endTime.Before(l.clock.Now()) {
   260  		l.Lock()
   261  		defer l.Unlock()
   262  		delete(l.trackers, t.Key())
   263  		l.saveLocked(ctx)
   264  		l.Debug(ctx, "tracker: old tracker, not running and clearing")
   265  		return errors.New("tracker from the past")
   266  	}
   267  
   268  	// start up the OS watch routine
   269  	watchID, err := l.startWatch(ctx, t)
   270  	if err != nil {
   271  		return err
   272  	}
   273  	defer func() {
   274  		// drop everything when our live location ends
   275  		err := l.getChatUI(ctx).ChatClearWatch(ctx, watchID)
   276  		if err != nil {
   277  			l.Debug(ctx, "tracker[%v]: error clearing watch: %+v", watchID, err)
   278  		}
   279  		l.Lock()
   280  		defer l.Unlock()
   281  		delete(l.trackers, t.Key())
   282  		l.saveLocked(ctx)
   283  	}()
   284  	// if this is a live location request, just put whatever the last coord is on the screen, makes it
   285  	// feel more live
   286  	if !l.lastCoord.IsZero() {
   287  		l.Debug(ctx, "tracker[%v]: updating with last coord", watchID)
   288  		t.updateCh <- l.lastCoord
   289  	}
   290  	firstUpdate := true
   291  	shouldUpdate := false
   292  	nextUpdate := l.clock.Now().Add(l.updateInterval)
   293  	for {
   294  		select {
   295  		case coord := <-t.updateCh:
   296  			added := t.Drain(coord)
   297  			l.Debug(ctx, "tracker[%v]: got coords", watchID)
   298  			if firstUpdate {
   299  				l.Debug(ctx, "tracker[%v]: updating due to live location first update", watchID)
   300  				_ = l.updateMapUnfurl(ctx, t, false)
   301  			} else {
   302  				shouldUpdate = true
   303  			}
   304  			firstUpdate = false
   305  			l.Lock()
   306  			l.saveLocked(ctx)
   307  			l.Unlock()
   308  			l.Debug(ctx, "tracker[%v]: added %d coords", watchID, added)
   309  			if l.TestingCoordsAddedCh != nil {
   310  				for i := 0; i < added; i++ {
   311  					l.TestingCoordsAddedCh <- struct{}{}
   312  				}
   313  			}
   314  		case <-l.clock.AfterTime(nextUpdate):
   315  			// we update the map unfurl on a timer so we don't spam delete and recreate it
   316  			if shouldUpdate {
   317  				// drain anything in the buffer if we are being updated and posting at the same time
   318  				t.Drain(chat1.Coordinate{})
   319  				l.Debug(ctx, "tracker[%v]: updating due to next update", watchID)
   320  				_ = l.updateMapUnfurl(ctx, t, false)
   321  				shouldUpdate = false
   322  			}
   323  			nextUpdate = l.clock.Now().Add(l.updateInterval)
   324  		case <-l.clock.AfterTime(t.endTime):
   325  			l.Debug(ctx, "tracker[%v]: live location complete, updating", watchID)
   326  			added := t.Drain(chat1.Coordinate{})
   327  			if t.getCurrentPosition && !shouldUpdate && added == 0 {
   328  				// only bother for current position if we have a coordinate
   329  				return nil
   330  			}
   331  			_ = l.updateMapUnfurl(ctx, t, true)
   332  			return nil
   333  		case <-t.stopCh:
   334  			l.Debug(ctx, "tracker[%v]: stopped, updating with done status", watchID)
   335  			if t.getCurrentPosition {
   336  				// don't need to update here
   337  				return nil
   338  			}
   339  			_ = l.updateMapUnfurl(ctx, t, true)
   340  			return nil
   341  		}
   342  	}
   343  }
   344  
   345  func (l *LiveLocationTracker) GetCurrentPosition(ctx context.Context, convID chat1.ConversationID,
   346  	msgID chat1.MessageID) {
   347  	defer l.Trace(ctx, nil, "GetCurrentPosition")()
   348  	l.Lock()
   349  	defer l.Unlock()
   350  	// start up a live location tracker for a small amount of time to make sure we get a good
   351  	// coordinate
   352  	t := newLocationTrack(convID, msgID, l.clock.Now().Add(4*time.Second), true, l.maxCoords, false)
   353  	l.trackers[t.Key()] = t
   354  	l.saveLocked(ctx)
   355  	l.eg.Go(func() error { return l.tracker(t) })
   356  }
   357  
   358  func (l *LiveLocationTracker) StartTracking(ctx context.Context, convID chat1.ConversationID,
   359  	msgID chat1.MessageID, endTime time.Time) {
   360  	defer l.Trace(ctx, nil, "StartTracking")()
   361  	l.Lock()
   362  	defer l.Unlock()
   363  	t := newLocationTrack(convID, msgID, endTime, false, l.maxCoords, false)
   364  	l.trackers[t.Key()] = t
   365  	l.saveLocked(ctx)
   366  	l.eg.Go(func() error { return l.tracker(t) })
   367  }
   368  
   369  func (l *LiveLocationTracker) LocationUpdate(ctx context.Context, coord chat1.Coordinate) {
   370  	defer l.Trace(ctx, nil, "LocationUpdate")()
   371  	l.Lock()
   372  	defer l.Unlock()
   373  	if l.G().IsMobileAppType() {
   374  		// if the app is woken up as the result of a location update, and we think we are currently
   375  		// backgrounded, then go ahead and mark us as background active so that we can get
   376  		// location updates out
   377  		l.G().MobileAppState.UpdateWithCheck(keybase1.MobileAppState_BACKGROUNDACTIVE,
   378  			func(curState keybase1.MobileAppState) bool {
   379  				return curState == keybase1.MobileAppState_BACKGROUND
   380  			})
   381  	}
   382  	if l.lastCoord.Eq(coord) {
   383  		l.Debug(ctx, "LocationUpdate: ignoring dup coordinate")
   384  		return
   385  	}
   386  	l.lastCoord = coord
   387  	for _, t := range l.trackers {
   388  		select {
   389  		case t.updateCh <- coord:
   390  		default:
   391  			l.Debug(ctx, "LocationUpdate: failed to push coordinate, queue full")
   392  		}
   393  	}
   394  }
   395  
   396  func (l *LiveLocationTracker) GetCoordinates(ctx context.Context, key types.LiveLocationKey) (res []chat1.Coordinate) {
   397  	defer l.Trace(ctx, nil, "GetCoordinates")()
   398  	l.Lock()
   399  	defer l.Unlock()
   400  	if t, ok := l.trackers[key]; ok {
   401  		res = t.GetCoords()
   402  	}
   403  	if len(res) == 0 {
   404  		res = append(res, l.lastCoord)
   405  	}
   406  	return res
   407  }
   408  
   409  func (l *LiveLocationTracker) GetEndTime(ctx context.Context, key types.LiveLocationKey) *time.Time {
   410  	defer l.Trace(ctx, nil, "GetEndTime")()
   411  	l.Lock()
   412  	defer l.Unlock()
   413  	if t, ok := l.trackers[key]; ok {
   414  		return &t.endTime
   415  	}
   416  	return nil
   417  }
   418  
   419  func (l *LiveLocationTracker) StopAllTracking(ctx context.Context) {
   420  	defer l.Trace(ctx, nil, "StopAllTracking")()
   421  	l.Lock()
   422  	defer l.Unlock()
   423  	for _, t := range l.trackers {
   424  		t.Stop()
   425  	}
   426  	l.saveLocked(ctx)
   427  }
   428  
   429  func (l *LiveLocationTracker) SetClock(clock clockwork.Clock) {
   430  	l.clock = clock
   431  }