package db import ( "fmt" "time" "github.com/bluesky-social/indigo/atproto/syntax" "tangled.org/core/appview/models" "tangled.org/core/orm" ) func UpsertReaction(e Execer, reaction models.Reaction) error { _, err := e.Exec( `insert into reactions (did, rkey, subject_at, kind, created) values (?, ?, ?, ?, ?) on conflict(did, rkey) do update set subject_at = excluded.subject_at, kind = excluded.kind, created = excluded.created`, reaction.ReactedByDid, reaction.Rkey, reaction.ThreadAt, reaction.Kind, reaction.Created.Format(time.RFC3339), ) return err } // Remove a reaction func DeleteReaction(e Execer, did syntax.DID, subjectAt syntax.ATURI, kind models.ReactionKind) ([]syntax.ATURI, error) { var deleted []syntax.ATURI rows, err := e.Query( `delete from reactions where did = ? and subject_at = ? and kind = ? returning at_uri`, did, subjectAt, kind, ) if err != nil { return nil, fmt.Errorf("deleting stars: %w", err) } defer rows.Close() for rows.Next() { var aturi syntax.ATURI if err := rows.Scan(&aturi); err != nil { return nil, fmt.Errorf("scanning at_uri: %w", err) } deleted = append(deleted, aturi) } return deleted, nil } // Remove a reaction func DeleteReactionByRkey(e Execer, did string, rkey string) error { _, err := e.Exec(`delete from reactions where did = ? and rkey = ?`, did, rkey) return err } func GetReactionCount(e Execer, subjectAt syntax.ATURI) (int, error) { count := 0 err := e.QueryRow(`select count(did) from reactions where subject_at = ?`, subjectAt).Scan(&count) if err != nil { return 0, err } return count, nil } func GetReactionCountByKind(e Execer, subjectAt syntax.ATURI, kind models.ReactionKind) (int, error) { count := 0 err := e.QueryRow( `select count(did) from reactions where subject_at = ? and kind = ?`, subjectAt, kind).Scan(&count) if err != nil { return 0, err } return count, nil } // GetReactionDisplayDataMap returns map of [models.ReactionKind]->[models.ReactionDisplayData] func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{subjectAt}, userLimit) return reactionMaps[subjectAt], err } // ListReactionDisplayDataMap returns map of [syntax.ATURI]->[models.ReactionKind]->[models.ReactionDisplayData] func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) (map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData, error) { if len(threads) == 0 { return nil, nil } filter := orm.FilterIn("subject_at", threads) args := filter.Arg() args = append(args, userLimit) rows, err := e.Query( fmt.Sprintf( `with ranked_reactions as ( select subject_at, kind, did, row_number() over (partition by subject_at, kind order by created asc) as rn, count(*) over (partition by subject_at, kind) as total from reactions where %s ) select subject_at, kind, did, total from ranked_reactions where rn <= ? order by subject_at, kind, rn asc`, filter.Condition(), ), args..., ) if err != nil { return nil, fmt.Errorf("querying: %w", err) } defer rows.Close() // aturi -> kind -> {count,users} result := make(map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData) for rows.Next() { var aturi syntax.ATURI var kind models.ReactionKind var did syntax.DID var count int if err := rows.Scan(&aturi, &kind, &did, &count); err != nil { return nil, fmt.Errorf("scanning row: %w", err) } if _, ok := result[aturi]; !ok { result[aturi] = make(map[models.ReactionKind]models.ReactionDisplayData) } data := result[aturi][kind] data.Count = count data.Users = append(data.Users, did.String()) result[aturi][kind] = data } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate rows: %w", err) } return result, nil } // GetReactionStatusMap returns map of [models.ReactionKind]->[bool] func GetReactionStatusMap(e Execer, userDid syntax.DID, subjectAt syntax.ATURI) (map[models.ReactionKind]bool, error) { reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{subjectAt}, userDid) return reactionMaps[subjectAt], err } // ListReactionStatusMap returns map of [syntax.ATURI]->[models.ReactionKind]->[bool] func ListReactionStatusMap(e Execer, threads []syntax.ATURI, userDid syntax.DID) (map[syntax.ATURI]map[models.ReactionKind]bool, error) { if len(threads) == 0 { return nil, nil } filter := orm.FilterIn("subject_at", threads) args := []any{userDid} args = append(args, filter.Arg()...) rows, err := e.Query( fmt.Sprintf( `select subject_at, kind from reactions where did = ? and %s`, filter.Condition(), ), args..., ) if err != nil { return nil, err } defer rows.Close() // aturi -> kind -> bool result := make(map[syntax.ATURI]map[models.ReactionKind]bool) for rows.Next() { var aturi syntax.ATURI var kind models.ReactionKind if err := rows.Scan(&aturi, &kind); err != nil { return nil, fmt.Errorf("scanning row: %w", err) } if _, ok := result[aturi]; !ok { result[aturi] = make(map[models.ReactionKind]bool) } result[aturi][kind] = true } return result, nil }