Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

tangled-core / appview / db / reaction.go
5.3 kB 195 lines
1package db 2 3import ( 4 "fmt" 5 "time" 6 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 "tangled.org/core/appview/models" 9 "tangled.org/core/orm" 10) 11 12func UpsertReaction(e Execer, reaction models.Reaction) error { 13 _, err := e.Exec( 14 `insert into reactions (did, rkey, subject_at, kind, created) 15 values (?, ?, ?, ?, ?) 16 on conflict(did, rkey) do update set 17 subject_at = excluded.subject_at, 18 kind = excluded.kind, 19 created = excluded.created`, 20 reaction.ReactedByDid, 21 reaction.Rkey, 22 reaction.ThreadAt, 23 reaction.Kind, 24 reaction.Created.Format(time.RFC3339), 25 ) 26 return err 27} 28 29// Remove a reaction 30func DeleteReaction(e Execer, did syntax.DID, subjectAt syntax.ATURI, kind models.ReactionKind) ([]syntax.ATURI, error) { 31 var deleted []syntax.ATURI 32 rows, err := e.Query( 33 `delete from reactions 34 where did = ? and subject_at = ? and kind = ? 35 returning at_uri`, 36 did, 37 subjectAt, 38 kind, 39 ) 40 if err != nil { 41 return nil, fmt.Errorf("deleting stars: %w", err) 42 } 43 defer rows.Close() 44 45 for rows.Next() { 46 var aturi syntax.ATURI 47 if err := rows.Scan(&aturi); err != nil { 48 return nil, fmt.Errorf("scanning at_uri: %w", err) 49 } 50 deleted = append(deleted, aturi) 51 } 52 return deleted, nil 53} 54 55// Remove a reaction 56func DeleteReactionByRkey(e Execer, did string, rkey string) error { 57 _, err := e.Exec(`delete from reactions where did = ? and rkey = ?`, did, rkey) 58 return err 59} 60 61func GetReactionCount(e Execer, subjectAt syntax.ATURI) (int, error) { 62 count := 0 63 err := e.QueryRow(`select count(did) from reactions where subject_at = ?`, subjectAt).Scan(&count) 64 if err != nil { 65 return 0, err 66 } 67 return count, nil 68} 69 70func GetReactionCountByKind(e Execer, subjectAt syntax.ATURI, kind models.ReactionKind) (int, error) { 71 count := 0 72 err := e.QueryRow( 73 `select count(did) from reactions where subject_at = ? and kind = ?`, subjectAt, kind).Scan(&count) 74 if err != nil { 75 return 0, err 76 } 77 return count, nil 78} 79 80// GetReactionDisplayDataMap returns map of [models.ReactionKind]->[models.ReactionDisplayData] 81func GetReactionMap(e Execer, userLimit int, subjectAt syntax.ATURI) (map[models.ReactionKind]models.ReactionDisplayData, error) { 82 reactionMaps, err := ListReactionDisplayDataMap(e, []syntax.ATURI{subjectAt}, userLimit) 83 return reactionMaps[subjectAt], err 84} 85 86// ListReactionDisplayDataMap returns map of [syntax.ATURI]->[models.ReactionKind]->[models.ReactionDisplayData] 87func ListReactionDisplayDataMap(e Execer, threads []syntax.ATURI, userLimit int) (map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData, error) { 88 if len(threads) == 0 { 89 return nil, nil 90 } 91 92 filter := orm.FilterIn("subject_at", threads) 93 args := filter.Arg() 94 args = append(args, userLimit) 95 rows, err := e.Query( 96 fmt.Sprintf( 97 `with ranked_reactions as ( 98 select 99 subject_at, 100 kind, 101 did, 102 row_number() over (partition by subject_at, kind order by created asc) as rn, 103 count(*) over (partition by subject_at, kind) as total 104 from reactions 105 where %s 106 ) 107 select subject_at, kind, did, total 108 from ranked_reactions 109 where rn <= ? 110 order by subject_at, kind, rn asc`, 111 filter.Condition(), 112 ), 113 args..., 114 ) 115 if err != nil { 116 return nil, fmt.Errorf("querying: %w", err) 117 } 118 defer rows.Close() 119 120 // aturi -> kind -> {count,users} 121 result := make(map[syntax.ATURI]map[models.ReactionKind]models.ReactionDisplayData) 122 123 for rows.Next() { 124 var aturi syntax.ATURI 125 var kind models.ReactionKind 126 var did syntax.DID 127 var count int 128 129 if err := rows.Scan(&aturi, &kind, &did, &count); err != nil { 130 return nil, fmt.Errorf("scanning row: %w", err) 131 } 132 133 if _, ok := result[aturi]; !ok { 134 result[aturi] = make(map[models.ReactionKind]models.ReactionDisplayData) 135 } 136 data := result[aturi][kind] 137 data.Count = count 138 data.Users = append(data.Users, did.String()) 139 result[aturi][kind] = data 140 } 141 142 if err := rows.Err(); err != nil { 143 return nil, fmt.Errorf("iterate rows: %w", err) 144 } 145 146 return result, nil 147} 148 149// GetReactionStatusMap returns map of [models.ReactionKind]->[bool] 150func GetReactionStatusMap(e Execer, userDid syntax.DID, subjectAt syntax.ATURI) (map[models.ReactionKind]bool, error) { 151 reactionMaps, err := ListReactionStatusMap(e, []syntax.ATURI{subjectAt}, userDid) 152 return reactionMaps[subjectAt], err 153} 154 155// ListReactionStatusMap returns map of [syntax.ATURI]->[models.ReactionKind]->[bool] 156func ListReactionStatusMap(e Execer, threads []syntax.ATURI, userDid syntax.DID) (map[syntax.ATURI]map[models.ReactionKind]bool, error) { 157 if len(threads) == 0 { 158 return nil, nil 159 } 160 161 filter := orm.FilterIn("subject_at", threads) 162 args := []any{userDid} 163 args = append(args, filter.Arg()...) 164 rows, err := e.Query( 165 fmt.Sprintf( 166 `select subject_at, kind from reactions 167 where did = ? and %s`, 168 filter.Condition(), 169 ), 170 args..., 171 ) 172 if err != nil { 173 return nil, err 174 } 175 defer rows.Close() 176 177 // aturi -> kind -> bool 178 result := make(map[syntax.ATURI]map[models.ReactionKind]bool) 179 180 for rows.Next() { 181 var aturi syntax.ATURI 182 var kind models.ReactionKind 183 184 if err := rows.Scan(&aturi, &kind); err != nil { 185 return nil, fmt.Errorf("scanning row: %w", err) 186 } 187 188 if _, ok := result[aturi]; !ok { 189 result[aturi] = make(map[models.ReactionKind]bool) 190 } 191 result[aturi][kind] = true 192 } 193 194 return result, nil 195}