This repository has no description
0

Configure Feed

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

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