1package storage 2 3import ( 4 "github.com/keybase/client/go/protocol/chat1" 5 "github.com/keybase/client/go/protocol/gregor1" 6 context "golang.org/x/net/context" 7) 8 9// For a given conversation, purge all ephemeral messages from 10// purgeInfo.MinUnexplodedID to the present, updating bookkeeping for the next 11// time we need to purge this conv. 12func (s *Storage) EphemeralPurge(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID, purgeInfo *chat1.EphemeralPurgeInfo) (newPurgeInfo *chat1.EphemeralPurgeInfo, explodedMsgs []chat1.MessageUnboxed, err Error) { 13 var ierr error 14 defer s.Trace(ctx, &ierr, "EphemeralPurge")() 15 defer func() { ierr = s.castInternalError(err) }() 16 lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String()) 17 defer lock.Release(ctx) 18 19 if purgeInfo == nil { 20 return nil, nil, nil 21 } 22 23 // Fetch secret key 24 key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG()) 25 if ierr != nil { 26 return nil, nil, MiscError{Msg: "unable to get secret key: " + ierr.Error()} 27 } 28 29 ctx, err = s.engine.Init(ctx, key, convID, uid) 30 if err != nil { 31 return nil, nil, err 32 } 33 34 maxMsgID, err := s.idtracker.getMaxMessageID(ctx, convID, uid) 35 if err != nil { 36 return nil, nil, err 37 } 38 39 // We don't care about holes. 40 maxHoles := int(maxMsgID-purgeInfo.MinUnexplodedID) + 1 41 var target int 42 if purgeInfo.MinUnexplodedID == 0 { 43 target = 0 // we need to traverse the whole conversation 44 } else { 45 target = maxHoles 46 } 47 rc := NewHoleyResultCollector(maxHoles, NewSimpleResultCollector(target, false)) 48 err = s.engine.ReadMessages(ctx, rc, convID, uid, maxMsgID, 0) 49 switch err.(type) { 50 case nil: 51 // ok 52 if len(rc.Result()) == 0 { 53 ierr := s.G().EphemeralTracker.InactivatePurgeInfo(ctx, convID, uid) 54 if ierr != nil { 55 return nil, nil, NewInternalError(ctx, s.DebugLabeler, "EphemeralTracker unable to InactivatePurgeInfo: %v", ierr) 56 } 57 return nil, nil, nil 58 } 59 case MissError: 60 // We don't have these messages in cache, so don't retry this 61 // conversation until further notice. 62 ierr := s.G().EphemeralTracker.InactivatePurgeInfo(ctx, convID, uid) 63 if ierr != nil { 64 return nil, nil, NewInternalError(ctx, s.DebugLabeler, "EphemeralTracker unable to InactivatePurgeInfo: %v", ierr) 65 } 66 return nil, nil, nil 67 default: 68 return nil, nil, err 69 } 70 newPurgeInfo, explodedMsgs, err = s.ephemeralPurgeHelper(ctx, convID, uid, rc.Result()) 71 if err != nil { 72 return nil, nil, err 73 } 74 ierr = s.G().EphemeralTracker.SetPurgeInfo(ctx, convID, uid, newPurgeInfo) 75 if ierr != nil { 76 return nil, nil, NewInternalError(ctx, s.DebugLabeler, "EphemeralTracker unable to SetPurgeInfo: %v", ierr) 77 } 78 return newPurgeInfo, explodedMsgs, err 79} 80 81func (s *Storage) explodeExpiredMessages(ctx context.Context, convID chat1.ConversationID, 82 uid gregor1.UID, msgs []chat1.MessageUnboxed) (explodedMsgs []chat1.MessageUnboxed, err Error) { 83 purgeInfo, explodedMsgs, err := s.ephemeralPurgeHelper(ctx, convID, uid, msgs) 84 if err != nil { 85 return nil, err 86 } 87 // We may only be merging in some subset of messages, we only update if the 88 // info we get is more restrictive that what we have already 89 ierr := s.G().EphemeralTracker.MaybeUpdatePurgeInfo(ctx, convID, uid, purgeInfo) 90 if ierr != nil { 91 return nil, NewInternalError(ctx, s.DebugLabeler, "EphemeralTracker unable to MaybeUpdatePurgeInfo: %v", ierr) 92 } 93 return explodedMsgs, nil 94} 95 96// Before adding or removing messages from storage, nuke any expired ones and 97// give info for our bookkeeping for the next time we have to purge. 98// requires msgs to be sorted by descending message ID 99func (s *Storage) ephemeralPurgeHelper(ctx context.Context, convID chat1.ConversationID, 100 uid gregor1.UID, msgs []chat1.MessageUnboxed) (purgeInfo *chat1.EphemeralPurgeInfo, explodedMsgs []chat1.MessageUnboxed, err Error) { 101 102 if len(msgs) == 0 { 103 return nil, nil, nil 104 } 105 106 nextPurgeTime := gregor1.Time(0) 107 minUnexplodedID := msgs[0].GetMessageID() 108 var allAssets []chat1.Asset 109 var allPurged []chat1.MessageUnboxed 110 var hasExploding bool 111 for i, msg := range msgs { 112 if !msg.IsValid() { 113 continue 114 } 115 mvalid := msg.Valid() 116 if mvalid.IsEphemeral() { 117 if !mvalid.IsEphemeralExpired(s.clock.Now()) { 118 hasExploding = true 119 // Keep track of the minimum ephemeral message that is not yet 120 // exploded. 121 if msg.GetMessageID() < minUnexplodedID { 122 minUnexplodedID = msg.GetMessageID() 123 } 124 // Keep track of the next time we'll have purge this conv. 125 if nextPurgeTime == 0 || mvalid.Etime() < nextPurgeTime { 126 nextPurgeTime = mvalid.Etime() 127 } 128 } else if mvalid.MessageBody.IsNil() { 129 // do nothing 130 } else { 131 msgPurged, assets := s.purgeMessage(mvalid) 132 allAssets = append(allAssets, assets...) 133 explodedMsgs = append(explodedMsgs, msgPurged) 134 allPurged = append(allPurged, msg) 135 msgs[i] = msgPurged 136 } 137 } 138 } 139 140 // queue asset deletions in the background 141 if s.assetDeleter != nil { 142 s.assetDeleter.DeleteAssets(ctx, uid, convID, allAssets) 143 } 144 // queue search index update in the background 145 go func() { 146 err := s.G().Indexer.Remove(ctx, convID, allPurged) 147 if err != nil { 148 s.Debug(ctx, "Error removing from indexer: %+v", err) 149 } 150 }() 151 152 if err = s.engine.WriteMessages(ctx, convID, uid, explodedMsgs); err != nil { 153 s.Debug(ctx, "write messages failed: %v", err) 154 return nil, nil, err 155 } 156 157 return &chat1.EphemeralPurgeInfo{ 158 ConvID: convID, 159 MinUnexplodedID: minUnexplodedID, 160 NextPurgeTime: nextPurgeTime, 161 IsActive: hasExploding, 162 }, explodedMsgs, nil 163} 164