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 }