gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }