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