get.go (13591B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package timeline 19 20 import ( 21 "container/list" 22 "context" 23 "errors" 24 "time" 25 26 "codeberg.org/gruf/go-kv" 27 "github.com/superseriousbusiness/gotosocial/internal/db" 28 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 29 "github.com/superseriousbusiness/gotosocial/internal/id" 30 "github.com/superseriousbusiness/gotosocial/internal/log" 31 ) 32 33 func (t *timeline) LastGot() time.Time { 34 t.Lock() 35 defer t.Unlock() 36 return t.lastGot 37 } 38 39 func (t *timeline) Get(ctx context.Context, amount int, maxID string, sinceID string, minID string, prepareNext bool) ([]Preparable, error) { 40 l := log.WithContext(ctx). 41 WithFields(kv.Fields{ 42 {"accountID", t.timelineID}, 43 {"amount", amount}, 44 {"maxID", maxID}, 45 {"sinceID", sinceID}, 46 {"minID", minID}, 47 }...) 48 l.Trace("entering get and updating t.lastGot") 49 50 // Regardless of what happens below, update the 51 // last time Get was called for this timeline. 52 t.Lock() 53 t.lastGot = time.Now() 54 t.Unlock() 55 56 var ( 57 items []Preparable 58 err error 59 ) 60 61 switch { 62 case maxID == "" && sinceID == "" && minID == "": 63 // No params are defined so just fetch from the top. 64 // This is equivalent to a user starting to view 65 // their timeline from newest -> older posts. 66 items, err = t.getXBetweenIDs(ctx, amount, id.Highest, id.Lowest, true) 67 68 // Cache expected next query to speed up scrolling. 69 // Assume the user will be scrolling downwards from 70 // the final ID in items. 71 if prepareNext && err == nil && len(items) != 0 { 72 nextMaxID := items[len(items)-1].GetID() 73 t.prepareNextQuery(amount, nextMaxID, "", "") 74 } 75 76 case maxID != "" && sinceID == "" && minID == "": 77 // Only maxID is defined, so fetch from maxID onwards. 78 // This is equivalent to a user paging further down 79 // their timeline from newer -> older posts. 80 items, err = t.getXBetweenIDs(ctx, amount, maxID, id.Lowest, true) 81 82 // Cache expected next query to speed up scrolling. 83 // Assume the user will be scrolling downwards from 84 // the final ID in items. 85 if prepareNext && err == nil && len(items) != 0 { 86 nextMaxID := items[len(items)-1].GetID() 87 t.prepareNextQuery(amount, nextMaxID, "", "") 88 } 89 90 // In the next cases, maxID is defined, and so are 91 // either sinceID or minID. This is equivalent to 92 // a user opening an in-progress timeline and asking 93 // for a slice of posts somewhere in the middle, or 94 // trying to "fill in the blanks" between two points, 95 // paging either up or down. 96 case maxID != "" && sinceID != "": 97 items, err = t.getXBetweenIDs(ctx, amount, maxID, sinceID, true) 98 99 // Cache expected next query to speed up scrolling. 100 // We can assume the caller is scrolling downwards. 101 // Guess id.Lowest as sinceID, since we don't actually 102 // know what the next sinceID would be. 103 if prepareNext && err == nil && len(items) != 0 { 104 nextMaxID := items[len(items)-1].GetID() 105 t.prepareNextQuery(amount, nextMaxID, id.Lowest, "") 106 } 107 108 case maxID != "" && minID != "": 109 items, err = t.getXBetweenIDs(ctx, amount, maxID, minID, false) 110 111 // Cache expected next query to speed up scrolling. 112 // We can assume the caller is scrolling upwards. 113 // Guess id.Highest as maxID, since we don't actually 114 // know what the next maxID would be. 115 if prepareNext && err == nil && len(items) != 0 { 116 prevMinID := items[0].GetID() 117 t.prepareNextQuery(amount, id.Highest, "", prevMinID) 118 } 119 120 // In the final cases, maxID is not defined, but 121 // either sinceID or minID are. This is equivalent to 122 // a user either "pulling up" at the top of their timeline 123 // to refresh it and check if newer posts have come in, or 124 // trying to scroll upwards from an old post to see what 125 // they missed since then. 126 // 127 // In these calls, we use the highest possible ulid as 128 // behindID because we don't have a cap for newest that 129 // we're interested in. 130 case maxID == "" && sinceID != "": 131 items, err = t.getXBetweenIDs(ctx, amount, id.Highest, sinceID, true) 132 133 // We can't cache an expected next query for this one, 134 // since presumably the caller is at the top of their 135 // timeline already. 136 137 case maxID == "" && minID != "": 138 items, err = t.getXBetweenIDs(ctx, amount, id.Highest, minID, false) 139 140 // Cache expected next query to speed up scrolling. 141 // We can assume the caller is scrolling upwards. 142 // Guess id.Highest as maxID, since we don't actually 143 // know what the next maxID would be. 144 if prepareNext && err == nil && len(items) != 0 { 145 prevMinID := items[0].GetID() 146 t.prepareNextQuery(amount, id.Highest, "", prevMinID) 147 } 148 149 default: 150 err = gtserror.New("switch statement exhausted with no results") 151 } 152 153 return items, err 154 } 155 156 // getXBetweenIDs returns x amount of items somewhere between (not including) the given IDs. 157 // 158 // If frontToBack is true, items will be served paging down from behindID. 159 // This corresponds to an api call to /timelines/home?max_id=WHATEVER&since_id=WHATEVER 160 // 161 // If frontToBack is false, items will be served paging up from beforeID. 162 // This corresponds to an api call to /timelines/home?max_id=WHATEVER&min_id=WHATEVER 163 func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID string, beforeID string, frontToBack bool) ([]Preparable, error) { 164 l := log. 165 WithContext(ctx). 166 WithFields(kv.Fields{ 167 {"amount", amount}, 168 {"behindID", behindID}, 169 {"beforeID", beforeID}, 170 {"frontToBack", frontToBack}, 171 }...) 172 l.Trace("entering getXBetweenID") 173 174 // Assume length we need to return. 175 items := make([]Preparable, 0, amount) 176 177 if beforeID >= behindID { 178 // This is an impossible situation, we 179 // can't serve anything between these. 180 return items, nil 181 } 182 183 // Try to ensure we have enough items prepared. 184 if err := t.prepareXBetweenIDs(ctx, amount, behindID, beforeID, frontToBack); err != nil { 185 // An error here doesn't necessarily mean we 186 // can't serve anything, so log + keep going. 187 l.Debugf("error calling prepareXBetweenIDs: %s", err) 188 } 189 190 var ( 191 beforeIDMark *list.Element 192 served int 193 // Our behavior while ranging through the 194 // list changes depending on if we're 195 // going front-to-back or back-to-front. 196 // 197 // To avoid checking which one we're doing 198 // in each loop iteration, define our range 199 // function here outside the loop. 200 // 201 // The bool indicates to the caller whether 202 // iteration should continue (true) or stop 203 // (false). 204 rangeF func(e *list.Element) (bool, error) 205 // If we get certain errors on entries as we're 206 // looking through, we might want to cheekily 207 // remove their elements from the timeline. 208 // Everything added to this slice will be removed. 209 removeElements = []*list.Element{} 210 ) 211 212 defer func() { 213 for _, e := range removeElements { 214 t.items.data.Remove(e) 215 } 216 }() 217 218 if frontToBack { 219 // We're going front-to-back, which means we 220 // don't need to look for a mark per se, we 221 // just keep serving items until we've reached 222 // a point where the items are out of the range 223 // we're interested in. 224 rangeF = func(e *list.Element) (bool, error) { 225 entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert 226 227 if entry.itemID >= behindID { 228 // ID of this item is too high, 229 // just keep iterating. 230 l.Trace("item is too new, continuing") 231 return true, nil 232 } 233 234 if entry.itemID <= beforeID { 235 // We've gone as far as we can through 236 // the list and reached entries that are 237 // now too old for us, stop here. 238 l.Trace("reached older items, breaking") 239 return false, nil 240 } 241 242 l.Trace("entry is just right") 243 244 if entry.prepared == nil { 245 // Whoops, this entry isn't prepared yet; some 246 // race condition? That's OK, we can do it now. 247 prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID) 248 if err != nil { 249 if errors.Is(err, db.ErrNoEntries) { 250 // ErrNoEntries means something has been deleted, 251 // so we'll likely not be able to ever prepare this. 252 // This means we can remove it and skip past it. 253 l.Debugf("db.ErrNoEntries while trying to prepare %s; will remove from timeline", entry.itemID) 254 removeElements = append(removeElements, e) 255 return true, nil 256 } 257 // We've got a proper db error. 258 err = gtserror.Newf("db error while trying to prepare %s: %w", entry.itemID, err) 259 return false, err 260 } 261 entry.prepared = prepared 262 } 263 264 items = append(items, entry.prepared) 265 266 served++ 267 return served < amount, nil 268 } 269 } else { 270 // Iterate through the list from the top, until 271 // we reach an item with id smaller than beforeID; 272 // ie., an item OLDER than beforeID. At that point, 273 // we can stop looking because we're not interested 274 // in older entries. 275 rangeF = func(e *list.Element) (bool, error) { 276 // Move the mark back one place each loop. 277 beforeIDMark = e 278 279 //nolint:forcetypeassert 280 if entry := e.Value.(*indexedItemsEntry); entry.itemID <= beforeID { 281 // We've gone as far as we can through 282 // the list and reached entries that are 283 // now too old for us, stop here. 284 l.Trace("reached older items, breaking") 285 return false, nil 286 } 287 288 return true, nil 289 } 290 } 291 292 // Iterate through the list until the function 293 // we defined above instructs us to stop. 294 for e := t.items.data.Front(); e != nil; e = e.Next() { 295 keepGoing, err := rangeF(e) 296 if err != nil { 297 return nil, err 298 } 299 300 if !keepGoing { 301 break 302 } 303 } 304 305 if frontToBack || beforeIDMark == nil { 306 // If we're serving front to back, then 307 // items should be populated by now. If 308 // we're serving back to front but didn't 309 // find any items newer than beforeID, 310 // we can just return empty items. 311 return items, nil 312 } 313 314 // We're serving back to front, so iterate upwards 315 // towards the front of the list from the mark we found, 316 // until we either get to the front, serve enough 317 // items, or reach behindID. 318 // 319 // To preserve ordering, we need to reverse the slice 320 // when we're finished. 321 for e := beforeIDMark; e != nil; e = e.Prev() { 322 entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert 323 324 if entry.itemID == beforeID { 325 // Don't include the beforeID 326 // entry itself, just continue. 327 l.Trace("entry item ID is equal to beforeID, skipping") 328 continue 329 } 330 331 if entry.itemID >= behindID { 332 // We've reached items that are 333 // newer than what we're looking 334 // for, just stop here. 335 l.Trace("reached newer items, breaking") 336 break 337 } 338 339 if entry.prepared == nil { 340 // Whoops, this entry isn't prepared yet; some 341 // race condition? That's OK, we can do it now. 342 prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID) 343 if err != nil { 344 if errors.Is(err, db.ErrNoEntries) { 345 // ErrNoEntries means something has been deleted, 346 // so we'll likely not be able to ever prepare this. 347 // This means we can remove it and skip past it. 348 l.Debugf("db.ErrNoEntries while trying to prepare %s; will remove from timeline", entry.itemID) 349 removeElements = append(removeElements, e) 350 continue 351 } 352 // We've got a proper db error. 353 err = gtserror.Newf("db error while trying to prepare %s: %w", entry.itemID, err) 354 return nil, err 355 } 356 entry.prepared = prepared 357 } 358 359 items = append(items, entry.prepared) 360 361 served++ 362 if served >= amount { 363 break 364 } 365 } 366 367 // Reverse order of items. 368 // https://zchee.github.io/golang-wiki/SliceTricks/#reversing 369 for l, r := 0, len(items)-1; l < r; l, r = l+1, r-1 { 370 items[l], items[r] = items[r], items[l] 371 } 372 373 return items, nil 374 } 375 376 func (t *timeline) prepareNextQuery(amount int, maxID string, sinceID string, minID string) { 377 var ( 378 // We explicitly use context.Background() rather than 379 // accepting a context param because we don't want this 380 // to stop/break when the calling context finishes. 381 ctx = context.Background() 382 err error 383 ) 384 385 // Always perform this async so caller doesn't have to wait. 386 go func() { 387 switch { 388 case maxID == "" && sinceID == "" && minID == "": 389 err = t.prepareXBetweenIDs(ctx, amount, id.Highest, id.Lowest, true) 390 case maxID != "" && sinceID == "" && minID == "": 391 err = t.prepareXBetweenIDs(ctx, amount, maxID, id.Lowest, true) 392 case maxID != "" && sinceID != "": 393 err = t.prepareXBetweenIDs(ctx, amount, maxID, sinceID, true) 394 case maxID != "" && minID != "": 395 err = t.prepareXBetweenIDs(ctx, amount, maxID, minID, false) 396 case maxID == "" && sinceID != "": 397 err = t.prepareXBetweenIDs(ctx, amount, id.Highest, sinceID, true) 398 case maxID == "" && minID != "": 399 err = t.prepareXBetweenIDs(ctx, amount, id.Highest, minID, false) 400 default: 401 err = gtserror.New("switch statement exhausted with no results") 402 } 403 404 if err != nil { 405 log. 406 WithContext(ctx). 407 WithFields(kv.Fields{ 408 {"amount", amount}, 409 {"maxID", maxID}, 410 {"sinceID", sinceID}, 411 {"minID", minID}, 412 }...). 413 Warnf("error preparing next query: %s", err) 414 } 415 }() 416 }