/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/chat/maps/livelocation.go
434 строки
13 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package maps import ( "context" "errors" "fmt" "sync" "time" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/storage" "github.com/keybase/client/go/chat/types" "github.com/keybase/client/go/chat/utils" "github.com/keybase/client/go/libkb" "github.com/keybase/client/go/protocol/chat1" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" "github.com/keybase/clockwork" "golang.org/x/sync/errgroup" ) type LiveLocationTracker struct { globals.Contextified utils.DebugLabeler sync.Mutex clock clockwork.Clock storage *trackStorage updateInterval time.Duration uid gregor1.UID eg errgroup.Group trackers map[types.LiveLocationKey]*locationTrack lastCoord chat1.Coordinate maxCoords int // testing only TestingCoordsAddedCh chan struct{} } func NewLiveLocationTracker(g *globals.Context) *LiveLocationTracker { return &LiveLocationTracker{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "LiveLocationTracker", false), storage: newTrackStorage(g), trackers: make(map[types.LiveLocationKey]*locationTrack), updateInterval: 30 * time.Second, maxCoords: 500, clock: clockwork.NewRealClock(), } } func (l *LiveLocationTracker) Start(ctx context.Context, uid gregor1.UID) { defer l.Trace(ctx, nil, "Start")() l.Lock() defer l.Unlock() l.uid = uid // bring back any trackers that we have stored. This is most relevant when being woken // up on iOS due to a location update. THe app might need to recreate all of its trackers // if the app had been killed. l.restoreLocked(ctx) } func (l *LiveLocationTracker) Stop(ctx context.Context) chan struct{} { defer l.Trace(ctx, nil, "Stop")() l.Lock() defer l.Unlock() ch := make(chan struct{}) for _, t := range l.trackers { t.Stop() } go func() { _ = l.eg.Wait() close(ch) }() return ch } func (l *LiveLocationTracker) ActivelyTracking(ctx context.Context) bool { l.Lock() defer l.Unlock() return len(l.trackers) > 0 } func (l *LiveLocationTracker) saveLocked(ctx context.Context) { var trackers []*locationTrack for _, t := range l.trackers { trackers = append(trackers, t) } if err := l.storage.Save(ctx, trackers); err != nil { l.Debug(ctx, "save: failed to save: %s", err) } } func (l *LiveLocationTracker) restoreLocked(ctx context.Context) { trackers, err := l.storage.Restore(ctx) if err != nil { l.Debug(ctx, "restoreLocked: failed to read, skipping: %s", err) return } if len(trackers) == 0 { return } l.Debug(ctx, "restoreLocked: restored %d trackers", len(trackers)) l.trackers = make(map[types.LiveLocationKey]*locationTrack) for _, t := range trackers { if t.IsStopped() { continue } l.trackers[t.Key()] = t myT := t l.eg.Go(func() error { return l.tracker(myT) }) } } func (l *LiveLocationTracker) getChatUI(ctx context.Context) libkb.ChatUI { ui, err := l.G().UIRouter.GetChatUI() if err != nil || ui == nil { l.Debug(ctx, "getChatUI: no chat UI found: err: %s", err) return utils.NullChatUI{} } return ui } type unfurlNotifyListener struct { globals.Contextified utils.DebugLabeler libkb.NoopNotifyListener outboxID chat1.OutboxID doneCh chan struct{} } func newUnfurlNotifyListener(g *globals.Context, outboxID chat1.OutboxID, doneCh chan struct{}) *unfurlNotifyListener { return &unfurlNotifyListener{ Contextified: globals.NewContextified(g), DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "maps.unfurlNotifyListener", false), outboxID: outboxID, doneCh: doneCh, } } func (n *unfurlNotifyListener) NewChatActivity(uid keybase1.UID, activity chat1.ChatActivity, source chat1.ChatActivitySource, ) { ctx := context.Background() st, err := activity.ActivityType() if err != nil { n.Debug(ctx, "NewChatActivity: failed to get type: %s", err) return } switch st { case chat1.ChatActivityType_INCOMING_MESSAGE: msg := activity.IncomingMessage().Message if msg.IsOutbox() { return } if n.outboxID.Eq(msg.GetOutboxID()) { n.doneCh <- struct{}{} } case chat1.ChatActivityType_FAILED_MESSAGE: recs := activity.FailedMessage().OutboxRecords for _, r := range recs { if n.outboxID.Eq(&r.OutboxID) { n.doneCh <- struct{}{} break } } } } func (l *LiveLocationTracker) updateMapUnfurl(ctx context.Context, t *locationTrack, done bool) (err error) { ctx = globals.ChatCtx(ctx, l.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, nil) defer l.Trace(ctx, &err, "updateMapUnfurl")() msg, err := l.G().ChatHelper.GetMessage(ctx, l.uid, t.convID, t.msgID, true, nil) if err != nil { return err } if !msg.IsValid() { return errors.New("invalid message") } mvalid := msg.Valid() var coords []chat1.Coordinate trackerCoords := t.GetCoords() if len(trackerCoords) == 0 { if !l.lastCoord.IsZero() { coords = []chat1.Coordinate{l.lastCoord} } else { return errors.New("no coordinates") } } else { coords = trackerCoords } last := coords[len(coords)-1] conv, err := utils.GetVerifiedConv(ctx, l.G(), l.uid, t.convID, types.InboxSourceDataSourceAll) if err != nil { return err } // Prefetch the next unfurl, and then delete any others. We do the prefetch so that there isn't // a large lag after we delete the unfurl and when we post the next one. We link back to the // tracker in the URL so we can get all the coordinates in the scraper. The cb param // makes it so the unfurler doesn't think it has already unfurled this URL and skips it. body := fmt.Sprintf("https://%s/?lat=%f&lon=%f&acc=%f&cb=%s&done=%v", types.MapsDomain, last.Lat, last.Lon, last.Accuracy, libkb.RandStringB64(3), done) if !t.getCurrentPosition { body += fmt.Sprintf("&livekey=%s", t.Key()) } l.G().Unfurler.Prefetch(ctx, l.uid, t.convID, body) for unfurlMsgID := range mvalid.Unfurls { // delete the old unfurl first to make way for the new if err := l.G().ChatHelper.DeleteMsg(ctx, t.convID, conv.Info.TlfName, unfurlMsgID); err != nil { return err } } // Create a new unfurl on the new URL, and wait for it to complete before charging forward. This way // we won't get into a state with multiple unfurls in the thread mvalid.MessageBody = chat1.NewMessageBodyWithText(chat1.MessageText{ Body: body, }) newMsg := chat1.NewMessageUnboxedWithValid(mvalid) unfurlDoneCh := make(chan struct{}, 10) outboxID := storage.GetOutboxIDFromURL(body, t.convID, newMsg) listenerID := l.G().NotifyRouter.AddListener(newUnfurlNotifyListener(l.G(), outboxID, unfurlDoneCh)) l.G().Unfurler.UnfurlAndSend(ctx, l.uid, t.convID, newMsg) select { case <-unfurlDoneCh: case <-time.After(time.Minute): l.Debug(ctx, "updateMapUnfurl: timed out waiting for unfurl callback, charging...") } l.G().NotifyRouter.RemoveListener(listenerID) return nil } func (l *LiveLocationTracker) startWatch(ctx context.Context, t *locationTrack) (watchID chat1.LocationWatchID, err error) { // try this a couple times in case we are starting fresh and the UI isn't ready yet maxWatchAttempts := 20 watchAttempts := 0 for { if watchID, err = l.getChatUI(ctx).ChatWatchPosition(ctx, t.convID, t.perm); err != nil { l.Debug(ctx, "startWatch: unable to watch position: attempt: %d msg: %s", watchAttempts, err) if watchAttempts > maxWatchAttempts { return 0, err } } else { break } maxWatchAttempts++ time.Sleep(time.Second) } return watchID, nil } func (l *LiveLocationTracker) tracker(t *locationTrack) error { ctx := context.Background() // check to see if we are being asked to start a tracker that is already expired if t.endTime.Before(l.clock.Now()) { l.Lock() defer l.Unlock() delete(l.trackers, t.Key()) l.saveLocked(ctx) l.Debug(ctx, "tracker: old tracker, not running and clearing") return errors.New("tracker from the past") } // start up the OS watch routine watchID, err := l.startWatch(ctx, t) if err != nil { return err } defer func() { // drop everything when our live location ends err := l.getChatUI(ctx).ChatClearWatch(ctx, watchID) if err != nil { l.Debug(ctx, "tracker[%v]: error clearing watch: %+v", watchID, err) } l.Lock() defer l.Unlock() delete(l.trackers, t.Key()) l.saveLocked(ctx) }() // if this is a live location request, just put whatever the last coord is on the screen, makes it // feel more live if !l.lastCoord.IsZero() { l.Debug(ctx, "tracker[%v]: updating with last coord", watchID) t.updateCh <- l.lastCoord } firstUpdate := true shouldUpdate := false nextUpdate := l.clock.Now().Add(l.updateInterval) for { select { case coord := <-t.updateCh: added := t.Drain(coord) l.Debug(ctx, "tracker[%v]: got coords", watchID) if firstUpdate { l.Debug(ctx, "tracker[%v]: updating due to live location first update", watchID) _ = l.updateMapUnfurl(ctx, t, false) } else { shouldUpdate = true } firstUpdate = false l.Lock() l.saveLocked(ctx) l.Unlock() l.Debug(ctx, "tracker[%v]: added %d coords", watchID, added) if l.TestingCoordsAddedCh != nil { for range added { l.TestingCoordsAddedCh <- struct{}{} } } case <-l.clock.AfterTime(nextUpdate): // we update the map unfurl on a timer so we don't spam delete and recreate it if shouldUpdate { // drain anything in the buffer if we are being updated and posting at the same time t.Drain(chat1.Coordinate{}) l.Debug(ctx, "tracker[%v]: updating due to next update", watchID) _ = l.updateMapUnfurl(ctx, t, false) shouldUpdate = false } nextUpdate = l.clock.Now().Add(l.updateInterval) case <-l.clock.AfterTime(t.endTime): l.Debug(ctx, "tracker[%v]: live location complete, updating", watchID) added := t.Drain(chat1.Coordinate{}) if t.getCurrentPosition && !shouldUpdate && added == 0 { // only bother for current position if we have a coordinate return nil } _ = l.updateMapUnfurl(ctx, t, true) return nil case <-t.stopCh: l.Debug(ctx, "tracker[%v]: stopped, updating with done status", watchID) if t.getCurrentPosition { // don't need to update here return nil } _ = l.updateMapUnfurl(ctx, t, true) return nil } } } func (l *LiveLocationTracker) GetCurrentPosition(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, ) { defer l.Trace(ctx, nil, "GetCurrentPosition")() l.Lock() defer l.Unlock() // start up a live location tracker for a small amount of time to make sure we get a good // coordinate t := newLocationTrack(convID, msgID, l.clock.Now().Add(4*time.Second), true, l.maxCoords, false) l.trackers[t.Key()] = t l.saveLocked(ctx) l.eg.Go(func() error { return l.tracker(t) }) } func (l *LiveLocationTracker) StartTracking(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID, endTime time.Time, ) { defer l.Trace(ctx, nil, "StartTracking")() l.Lock() defer l.Unlock() t := newLocationTrack(convID, msgID, endTime, false, l.maxCoords, false) l.trackers[t.Key()] = t l.saveLocked(ctx) l.eg.Go(func() error { return l.tracker(t) }) } func (l *LiveLocationTracker) LocationUpdate(ctx context.Context, coord chat1.Coordinate) { defer l.Trace(ctx, nil, "LocationUpdate")() l.Lock() defer l.Unlock() if l.G().IsMobileAppType() { // if the app is woken up as the result of a location update, and we think we are currently // backgrounded, then go ahead and mark us as background active so that we can get // location updates out l.G().MobileAppState.UpdateWithCheck(keybase1.MobileAppState_BACKGROUNDACTIVE, func(curState keybase1.MobileAppState) bool { return curState == keybase1.MobileAppState_BACKGROUND }) } if l.lastCoord.Eq(coord) { l.Debug(ctx, "LocationUpdate: ignoring dup coordinate") return } l.lastCoord = coord for _, t := range l.trackers { select { case t.updateCh <- coord: default: l.Debug(ctx, "LocationUpdate: failed to push coordinate, queue full") } } } func (l *LiveLocationTracker) GetCoordinates(ctx context.Context, key types.LiveLocationKey) (res []chat1.Coordinate) { defer l.Trace(ctx, nil, "GetCoordinates")() l.Lock() defer l.Unlock() if t, ok := l.trackers[key]; ok { res = t.GetCoords() } if len(res) == 0 { res = append(res, l.lastCoord) } return res } func (l *LiveLocationTracker) GetEndTime(ctx context.Context, key types.LiveLocationKey) *time.Time { defer l.Trace(ctx, nil, "GetEndTime")() l.Lock() defer l.Unlock() if t, ok := l.trackers[key]; ok { return &t.endTime } return nil } func (l *LiveLocationTracker) StopAllTracking(ctx context.Context) { defer l.Trace(ctx, nil, "StopAllTracking")() l.Lock() defer l.Unlock() for _, t := range l.trackers { t.Stop() } l.saveLocked(ctx) } func (l *LiveLocationTracker) SetClock(clock clockwork.Clock) { l.clock = clock }