timeline.go (10684B)
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 bundb 19 20 import ( 21 "context" 22 "fmt" 23 "time" 24 25 "github.com/superseriousbusiness/gotosocial/internal/db" 26 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 27 "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" 28 "github.com/superseriousbusiness/gotosocial/internal/id" 29 "github.com/superseriousbusiness/gotosocial/internal/log" 30 "github.com/superseriousbusiness/gotosocial/internal/state" 31 "github.com/uptrace/bun" 32 "golang.org/x/exp/slices" 33 ) 34 35 type timelineDB struct { 36 conn *DBConn 37 state *state.State 38 } 39 40 func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { 41 // Ensure reasonable 42 if limit < 0 { 43 limit = 0 44 } 45 46 // Make educated guess for slice size 47 var ( 48 statusIDs = make([]string, 0, limit) 49 frontToBack = true 50 ) 51 52 q := t.conn. 53 NewSelect(). 54 TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). 55 // Select only IDs from table 56 Column("status.id") 57 58 if maxID == "" || maxID >= id.Highest { 59 const future = 24 * time.Hour 60 61 var err error 62 63 // don't return statuses more than 24hr in the future 64 maxID, err = id.NewULIDFromTime(time.Now().Add(future)) 65 if err != nil { 66 return nil, err 67 } 68 } 69 70 // return only statuses LOWER (ie., older) than maxID 71 q = q.Where("? < ?", bun.Ident("status.id"), maxID) 72 73 if sinceID != "" { 74 // return only statuses HIGHER (ie., newer) than sinceID 75 q = q.Where("? > ?", bun.Ident("status.id"), sinceID) 76 } 77 78 if minID != "" { 79 // return only statuses HIGHER (ie., newer) than minID 80 q = q.Where("? > ?", bun.Ident("status.id"), minID) 81 82 // page up 83 frontToBack = false 84 } 85 86 if local { 87 // return only statuses posted by local account havers 88 q = q.Where("? = ?", bun.Ident("status.local"), local) 89 } 90 91 if limit > 0 { 92 // limit amount of statuses returned 93 q = q.Limit(limit) 94 } 95 96 if frontToBack { 97 // Page down. 98 q = q.Order("status.id DESC") 99 } else { 100 // Page up. 101 q = q.Order("status.id ASC") 102 } 103 104 // Subquery to select target (followed) account 105 // IDs from follows owned by given accountID. 106 subQ := t.conn. 107 NewSelect(). 108 TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")). 109 Column("follow.target_account_id"). 110 Where("? = ?", bun.Ident("follow.account_id"), accountID) 111 112 // Use the subquery in a WhereGroup here to specify that we want EITHER 113 // - statuses posted by accountID itself OR 114 // - statuses posted by accounts that accountID follows 115 q = q.WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { 116 return q. 117 Where("? = ?", bun.Ident("status.account_id"), accountID). 118 WhereOr("? IN (?)", bun.Ident("status.account_id"), subQ) 119 }) 120 121 if err := q.Scan(ctx, &statusIDs); err != nil { 122 return nil, t.conn.ProcessError(err) 123 } 124 125 if len(statusIDs) == 0 { 126 return nil, nil 127 } 128 129 // If we're paging up, we still want statuses 130 // to be sorted by ID desc, so reverse ids slice. 131 // https://zchee.github.io/golang-wiki/SliceTricks/#reversing 132 if !frontToBack { 133 for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { 134 statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] 135 } 136 } 137 138 statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) 139 for _, id := range statusIDs { 140 // Fetch status from db for ID 141 status, err := t.state.DB.GetStatusByID(ctx, id) 142 if err != nil { 143 log.Errorf(ctx, "error fetching status %q: %v", id, err) 144 continue 145 } 146 147 // Append status to slice 148 statuses = append(statuses, status) 149 } 150 151 return statuses, nil 152 } 153 154 func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, db.Error) { 155 // Ensure reasonable 156 if limit < 0 { 157 limit = 0 158 } 159 160 // Make educated guess for slice size 161 statusIDs := make([]string, 0, limit) 162 163 q := t.conn. 164 NewSelect(). 165 TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). 166 Column("status.id"). 167 Where("? = ?", bun.Ident("status.visibility"), gtsmodel.VisibilityPublic). 168 WhereGroup(" AND ", whereEmptyOrNull("status.boost_of_id")). 169 Order("status.id DESC") 170 171 if maxID == "" { 172 const future = 24 * time.Hour 173 174 var err error 175 176 // don't return statuses more than 24hr in the future 177 maxID, err = id.NewULIDFromTime(time.Now().Add(future)) 178 if err != nil { 179 return nil, err 180 } 181 } 182 183 // return only statuses LOWER (ie., older) than maxID 184 q = q.Where("? < ?", bun.Ident("status.id"), maxID) 185 186 if sinceID != "" { 187 q = q.Where("? > ?", bun.Ident("status.id"), sinceID) 188 } 189 190 if minID != "" { 191 q = q.Where("? > ?", bun.Ident("status.id"), minID) 192 } 193 194 if local { 195 q = q.Where("? = ?", bun.Ident("status.local"), local) 196 } 197 198 if limit > 0 { 199 q = q.Limit(limit) 200 } 201 202 if err := q.Scan(ctx, &statusIDs); err != nil { 203 return nil, t.conn.ProcessError(err) 204 } 205 206 statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) 207 208 for _, id := range statusIDs { 209 // Fetch status from db for ID 210 status, err := t.state.DB.GetStatusByID(ctx, id) 211 if err != nil { 212 log.Errorf(ctx, "error fetching status %q: %v", id, err) 213 continue 214 } 215 216 // Append status to slice 217 statuses = append(statuses, status) 218 } 219 220 return statuses, nil 221 } 222 223 // TODO optimize this query and the logic here, because it's slow as balls -- it takes like a literal second to return with a limit of 20! 224 // It might be worth serving it through a timeline instead of raw DB queries, like we do for Home feeds. 225 func (t *timelineDB) GetFavedTimeline(ctx context.Context, accountID string, maxID string, minID string, limit int) ([]*gtsmodel.Status, string, string, db.Error) { 226 // Ensure reasonable 227 if limit < 0 { 228 limit = 0 229 } 230 231 // Make educated guess for slice size 232 faves := make([]*gtsmodel.StatusFave, 0, limit) 233 234 fq := t.conn. 235 NewSelect(). 236 Model(&faves). 237 Where("? = ?", bun.Ident("status_fave.account_id"), accountID). 238 Order("status_fave.id DESC") 239 240 if maxID != "" { 241 fq = fq.Where("? < ?", bun.Ident("status_fave.id"), maxID) 242 } 243 244 if minID != "" { 245 fq = fq.Where("? > ?", bun.Ident("status_fave.id"), minID) 246 } 247 248 if limit > 0 { 249 fq = fq.Limit(limit) 250 } 251 252 err := fq.Scan(ctx) 253 if err != nil { 254 return nil, "", "", t.conn.ProcessError(err) 255 } 256 257 if len(faves) == 0 { 258 return nil, "", "", db.ErrNoEntries 259 } 260 261 // Sort by favourite ID rather than status ID 262 slices.SortFunc(faves, func(a, b *gtsmodel.StatusFave) bool { 263 return a.ID > b.ID 264 }) 265 266 statuses := make([]*gtsmodel.Status, 0, len(faves)) 267 268 for _, fave := range faves { 269 // Fetch status from db for corresponding favourite 270 status, err := t.state.DB.GetStatusByID(ctx, fave.StatusID) 271 if err != nil { 272 log.Errorf(ctx, "error fetching status for fave %q: %v", fave.ID, err) 273 continue 274 } 275 276 // Append status to slice 277 statuses = append(statuses, status) 278 } 279 280 nextMaxID := faves[len(faves)-1].ID 281 prevMinID := faves[0].ID 282 return statuses, nextMaxID, prevMinID, nil 283 } 284 285 func (t *timelineDB) GetListTimeline( 286 ctx context.Context, 287 listID string, 288 maxID string, 289 sinceID string, 290 minID string, 291 limit int, 292 ) ([]*gtsmodel.Status, error) { 293 // Ensure reasonable 294 if limit < 0 { 295 limit = 0 296 } 297 298 // Make educated guess for slice size 299 var ( 300 statusIDs = make([]string, 0, limit) 301 frontToBack = true 302 ) 303 304 // Fetch all listEntries entries from the database. 305 listEntries, err := t.state.DB.GetListEntries( 306 // Don't need actual follows 307 // for this, just the IDs. 308 gtscontext.SetBarebones(ctx), 309 listID, 310 "", "", "", 0, 311 ) 312 if err != nil { 313 return nil, fmt.Errorf("error getting entries for list %s: %w", listID, err) 314 } 315 316 // Extract just the IDs of each follow. 317 followIDs := make([]string, 0, len(listEntries)) 318 for _, listEntry := range listEntries { 319 followIDs = append(followIDs, listEntry.FollowID) 320 } 321 322 // Select target account IDs from follows. 323 subQ := t.conn. 324 NewSelect(). 325 TableExpr("? AS ?", bun.Ident("follows"), bun.Ident("follow")). 326 Column("follow.target_account_id"). 327 Where("? IN (?)", bun.Ident("follow.id"), bun.In(followIDs)) 328 329 // Select only status IDs created 330 // by one of the followed accounts. 331 q := t.conn. 332 NewSelect(). 333 TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")). 334 // Select only IDs from table 335 Column("status.id"). 336 Where("? IN (?)", bun.Ident("status.account_id"), subQ) 337 338 if maxID == "" || maxID >= id.Highest { 339 const future = 24 * time.Hour 340 341 var err error 342 343 // don't return statuses more than 24hr in the future 344 maxID, err = id.NewULIDFromTime(time.Now().Add(future)) 345 if err != nil { 346 return nil, err 347 } 348 } 349 350 // return only statuses LOWER (ie., older) than maxID 351 q = q.Where("? < ?", bun.Ident("status.id"), maxID) 352 353 if sinceID != "" { 354 // return only statuses HIGHER (ie., newer) than sinceID 355 q = q.Where("? > ?", bun.Ident("status.id"), sinceID) 356 } 357 358 if minID != "" { 359 // return only statuses HIGHER (ie., newer) than minID 360 q = q.Where("? > ?", bun.Ident("status.id"), minID) 361 362 // page up 363 frontToBack = false 364 } 365 366 if limit > 0 { 367 // limit amount of statuses returned 368 q = q.Limit(limit) 369 } 370 371 if frontToBack { 372 // Page down. 373 q = q.Order("status.id DESC") 374 } else { 375 // Page up. 376 q = q.Order("status.id ASC") 377 } 378 379 if err := q.Scan(ctx, &statusIDs); err != nil { 380 return nil, t.conn.ProcessError(err) 381 } 382 383 if len(statusIDs) == 0 { 384 return nil, nil 385 } 386 387 // If we're paging up, we still want statuses 388 // to be sorted by ID desc, so reverse ids slice. 389 // https://zchee.github.io/golang-wiki/SliceTricks/#reversing 390 if !frontToBack { 391 for l, r := 0, len(statusIDs)-1; l < r; l, r = l+1, r-1 { 392 statusIDs[l], statusIDs[r] = statusIDs[r], statusIDs[l] 393 } 394 } 395 396 statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) 397 for _, id := range statusIDs { 398 // Fetch status from db for ID 399 status, err := t.state.DB.GetStatusByID(ctx, id) 400 if err != nil { 401 log.Errorf(ctx, "error fetching status %q: %v", id, err) 402 continue 403 } 404 405 // Append status to slice 406 statuses = append(statuses, status) 407 } 408 409 return statuses, nil 410 }