github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/attachments/gallery.go (about) 1 package attachments 2 3 import ( 4 "sort" 5 6 "github.com/keybase/client/go/chat/globals" 7 "github.com/keybase/client/go/chat/utils" 8 "github.com/keybase/client/go/protocol/chat1" 9 "github.com/keybase/client/go/protocol/gregor1" 10 "golang.org/x/net/context" 11 "mvdan.cc/xurls/v2" 12 ) 13 14 type NextMessageOptions struct { 15 BackInTime bool 16 MessageType chat1.MessageType 17 AssetTypes []chat1.AssetMetadataType 18 UnfurlTypes []chat1.UnfurlType 19 FilterLinks bool 20 } 21 22 type Gallery struct { 23 globals.Contextified 24 utils.DebugLabeler 25 26 PrevStride, NextStride int 27 } 28 29 func NewGallery(g *globals.Context) *Gallery { 30 return &Gallery{ 31 Contextified: globals.NewContextified(g), 32 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Attachments.Gallery", false), 33 NextStride: 5, 34 PrevStride: 50, 35 } 36 } 37 38 func (g *Gallery) eligibleNextMessage(msg chat1.MessageUnboxed, typMap map[chat1.MessageType]bool, 39 assetMap map[chat1.AssetMetadataType]bool, unfurlMap map[chat1.UnfurlType]bool) bool { 40 if !msg.IsValid() { 41 return false 42 } 43 body := msg.Valid().MessageBody 44 typ, err := body.MessageType() 45 if err != nil { 46 return false 47 } 48 if !typMap[typ] { 49 return false 50 } 51 switch typ { 52 case chat1.MessageType_ATTACHMENT: 53 md := body.Attachment().Object.Metadata 54 atyp, err := md.AssetType() 55 if err != nil { 56 return false 57 } 58 if len(assetMap) > 0 && !assetMap[atyp] { 59 return false 60 } 61 case chat1.MessageType_UNFURL: 62 unfurl := body.Unfurl().Unfurl.Unfurl 63 typ, err := unfurl.UnfurlType() 64 if err != nil { 65 return false 66 } 67 if len(unfurlMap) > 0 && !unfurlMap[typ] { 68 return false 69 } 70 } 71 return true 72 } 73 74 var linkRegexp = xurls.Strict() 75 76 func (g *Gallery) searchForLinks(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 77 msgID chat1.MessageID, num int, uiCh chan chat1.UIMessage) (res []chat1.MessageUnboxed, last bool, err error) { 78 var hitCh chan chat1.ChatSearchHit 79 if uiCh != nil { 80 hitCh = make(chan chat1.ChatSearchHit) 81 doneCh := make(chan struct{}) 82 defer func() { <-doneCh }() 83 go func() { 84 for hit := range hitCh { 85 uiCh <- hit.HitMessage 86 } 87 close(doneCh) 88 }() 89 } 90 idcontrol := &chat1.MessageIDControl{ 91 Pivot: &msgID, 92 Mode: chat1.MessageIDControlMode_OLDERMESSAGES, 93 } 94 if _, res, err = g.G().RegexpSearcher.Search(ctx, uid, convID, linkRegexp, hitCh, chat1.SearchOpts{ 95 InitialPagination: utils.MessageIDControlToPagination(ctx, g.DebugLabeler, idcontrol, nil), 96 MaxHits: num, 97 }); err != nil { 98 return res, false, err 99 } 100 return res, len(res) < num, nil 101 } 102 103 func (g *Gallery) NextMessage(ctx context.Context, uid gregor1.UID, 104 convID chat1.ConversationID, msgID chat1.MessageID, opts NextMessageOptions) (res *chat1.MessageUnboxed, last bool, err error) { 105 msgs, last, err := g.NextMessages(ctx, uid, convID, msgID, 1, opts, nil) 106 if err != nil { 107 return res, false, err 108 } 109 if len(msgs) == 0 { 110 return nil, true, nil 111 } 112 return &msgs[0], last, nil 113 } 114 115 func (g *Gallery) makeMaps(opts NextMessageOptions) (typMap map[chat1.MessageType]bool, 116 assetMap map[chat1.AssetMetadataType]bool, unfurlMap map[chat1.UnfurlType]bool) { 117 typMap = make(map[chat1.MessageType]bool) 118 assetMap = make(map[chat1.AssetMetadataType]bool) 119 unfurlMap = make(map[chat1.UnfurlType]bool) 120 typMap[opts.MessageType] = true 121 for _, atyp := range opts.AssetTypes { 122 assetMap[atyp] = true 123 } 124 for _, utyp := range opts.UnfurlTypes { 125 unfurlMap[utyp] = true 126 } 127 return typMap, assetMap, unfurlMap 128 } 129 130 func (g *Gallery) getUnfurlHost(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 131 msg chat1.MessageUnboxed) (res chat1.MessageUnboxed, err error) { 132 if !msg.IsValid() { 133 return msg, nil 134 } 135 if !msg.Valid().MessageBody.IsType(chat1.MessageType_UNFURL) { 136 return msg, nil 137 } 138 hostMsgID := msg.Valid().MessageBody.Unfurl().MessageID 139 return g.G().ChatHelper.GetMessage(ctx, uid, convID, hostMsgID, true, nil) 140 } 141 142 func (g *Gallery) NextMessages(ctx context.Context, uid gregor1.UID, 143 convID chat1.ConversationID, msgID chat1.MessageID, num int, opts NextMessageOptions, 144 uiCh chan chat1.UIMessage) (res []chat1.MessageUnboxed, last bool, err error) { 145 defer g.Trace(ctx, &err, "NextMessages")() 146 defer func() { 147 if uiCh != nil { 148 close(uiCh) 149 } 150 }() 151 var reverseFn func(chat1.ThreadView) []chat1.MessageUnboxed 152 var nextPageFn func(*chat1.Pagination) *chat1.Pagination 153 pivot := msgID 154 mode := chat1.MessageIDControlMode_NEWERMESSAGES 155 if opts.BackInTime { 156 mode = chat1.MessageIDControlMode_OLDERMESSAGES 157 } 158 if opts.MessageType == chat1.MessageType_NONE { 159 opts.MessageType = chat1.MessageType_ATTACHMENT 160 } else if opts.MessageType == chat1.MessageType_TEXT && opts.FilterLinks { 161 return g.searchForLinks(ctx, uid, convID, msgID, num, uiCh) 162 } 163 typMap, assetMap, unfurlMap := g.makeMaps(opts) 164 pagination := utils.MessageIDControlToPagination(ctx, g.DebugLabeler, &chat1.MessageIDControl{ 165 Pivot: &pivot, 166 Mode: mode, 167 }, nil) 168 if opts.BackInTime { 169 reverseFn = func(tv chat1.ThreadView) []chat1.MessageUnboxed { 170 return tv.Messages 171 } 172 nextPageFn = func(p *chat1.Pagination) (res *chat1.Pagination) { 173 res = p 174 res.Num = g.NextStride 175 res.Previous = nil 176 return res 177 } 178 pagination.Num = g.NextStride 179 } else { 180 reverseFn = func(tv chat1.ThreadView) (res []chat1.MessageUnboxed) { 181 res = make([]chat1.MessageUnboxed, len(tv.Messages)) 182 copy(res, tv.Messages) 183 sort.Sort(sort.Reverse(utils.ByMsgUnboxedMsgID(res))) 184 return res 185 } 186 nextPageFn = func(p *chat1.Pagination) (res *chat1.Pagination) { 187 pivot += chat1.MessageID(g.PrevStride) 188 return utils.MessageIDControlToPagination(ctx, g.DebugLabeler, &chat1.MessageIDControl{ 189 Pivot: &pivot, 190 Num: g.PrevStride, 191 Mode: chat1.MessageIDControlMode_NEWERMESSAGES, 192 }, nil) 193 } 194 pagination.Num = g.PrevStride 195 } 196 197 for { 198 select { 199 case <-ctx.Done(): 200 return res, false, ctx.Err() 201 default: 202 } 203 g.Debug(ctx, "NextMessage: starting scan: p: %s pivot: %d", pagination, pivot) 204 tv, err := g.G().ConvSource.Pull(ctx, convID, uid, chat1.GetThreadReason_GENERAL, nil, 205 &chat1.GetThreadQuery{ 206 MessageTypes: []chat1.MessageType{opts.MessageType}, 207 }, pagination) 208 if err != nil { 209 return res, false, err 210 } 211 messages := reverseFn(tv) 212 for _, m := range messages { 213 if !g.eligibleNextMessage(m, typMap, assetMap, unfurlMap) { 214 continue 215 } 216 if m, err = g.getUnfurlHost(ctx, uid, convID, m); err != nil { 217 return res, false, err 218 } 219 res = append(res, m) 220 if uiCh != nil { 221 uiCh <- utils.PresentMessageUnboxed(ctx, g.G(), m, uid, convID) 222 } 223 if len(res) >= num { 224 g.Debug(ctx, "NextMessages: stopping on satisfied") 225 return res, false, nil 226 } 227 } 228 g.Debug(ctx, "NextMessages: still need more (%d < %d): len: %d", len(res), num, len(tv.Messages)) 229 if tv.Pagination == nil || tv.Pagination.Last { 230 g.Debug(ctx, "NextMessages: stopping on last page") 231 break 232 } 233 pagination = nextPageFn(tv.Pagination) 234 } 235 return res, true, nil 236 }