168 lines
5.0 KiB
Go
168 lines
5.0 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/prabhupadasays/spsays-bff/internal/model"
|
|
)
|
|
|
|
// maxMentionsPerPost caps how many users a post can tag.
|
|
const maxMentionsPerPost = 30
|
|
|
|
// MentionRepo handles post_mentions membership + mention suggestions.
|
|
type MentionRepo struct{ pg *Postgres }
|
|
|
|
func NewMentionRepo(pg *Postgres) *MentionRepo { return &MentionRepo{pg: pg} }
|
|
|
|
// Suggest returns up to `limit` users to mention, prioritised: people the
|
|
// viewer follows (rank 0), then people who follow the viewer (rank 1), then
|
|
// everyone else (rank 2). Excludes the viewer. Optional name/handle search.
|
|
// Offset paginates for the infinite-scroll popover.
|
|
func (r *MentionRepo) Suggest(ctx context.Context, viewerID, q string, limit, offset int) ([]model.UserSummary, error) {
|
|
like := "%" + q + "%"
|
|
rows, err := r.pg.Pool.Query(ctx, `
|
|
SELECT u.id, u.handle, u.name, u.avatar_url,
|
|
CASE
|
|
WHEN EXISTS (SELECT 1 FROM follows f
|
|
WHERE f.follower_id = $1 AND f.following_id = u.id
|
|
AND f.status = 'accepted') THEN 0
|
|
WHEN EXISTS (SELECT 1 FROM follows f
|
|
WHERE f.follower_id = u.id AND f.following_id = $1
|
|
AND f.status = 'accepted') THEN 1
|
|
ELSE 2
|
|
END AS rank
|
|
FROM users u
|
|
WHERE u.id <> $1
|
|
-- A suspended account can't be @mentioned into a post.
|
|
AND u.banned_at IS NULL
|
|
AND ($2 = '' OR u.name ILIKE $3 OR u.handle ILIKE $3)
|
|
ORDER BY rank, u.name, u.id
|
|
LIMIT $4 OFFSET $5`,
|
|
viewerID, q, like, limit, offset,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("suggest mentions: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
out := make([]model.UserSummary, 0, limit)
|
|
for rows.Next() {
|
|
var s model.UserSummary
|
|
var rank int
|
|
if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL, &rank); err != nil {
|
|
return nil, fmt.Errorf("scan suggestion: %w", err)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// AddMany tags the given users on a post (idempotent, capped). Used on create.
|
|
func (r *MentionRepo) AddMany(ctx context.Context, postID string, userIDs []string) error {
|
|
if len(userIDs) > maxMentionsPerPost {
|
|
userIDs = userIDs[:maxMentionsPerPost]
|
|
}
|
|
for _, uid := range userIDs {
|
|
if _, err := r.pg.Pool.Exec(ctx,
|
|
`INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2)
|
|
ON CONFLICT (post_id, user_id) DO NOTHING`,
|
|
postID, uid,
|
|
); err != nil {
|
|
return fmt.Errorf("add mention: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Add tags a single user (idempotent), enforcing the per-post cap.
|
|
func (r *MentionRepo) Add(ctx context.Context, postID, userID string) error {
|
|
var n int
|
|
if err := r.pg.Pool.QueryRow(ctx,
|
|
`SELECT count(*) FROM post_mentions WHERE post_id = $1`, postID,
|
|
).Scan(&n); err != nil {
|
|
return fmt.Errorf("count mentions: %w", err)
|
|
}
|
|
if n >= maxMentionsPerPost {
|
|
return fmt.Errorf("a post can mention at most %d people", maxMentionsPerPost)
|
|
}
|
|
if _, err := r.pg.Pool.Exec(ctx,
|
|
`INSERT INTO post_mentions (post_id, user_id) VALUES ($1, $2)
|
|
ON CONFLICT (post_id, user_id) DO NOTHING`,
|
|
postID, userID,
|
|
); err != nil {
|
|
return fmt.Errorf("add mention: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Remove untags a user from a post.
|
|
func (r *MentionRepo) Remove(ctx context.Context, postID, userID string) error {
|
|
if _, err := r.pg.Pool.Exec(ctx,
|
|
`DELETE FROM post_mentions WHERE post_id = $1 AND user_id = $2`,
|
|
postID, userID,
|
|
); err != nil {
|
|
return fmt.Errorf("remove mention: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListForPost returns the users tagged on a post (insertion order).
|
|
func (r *MentionRepo) ListForPost(ctx context.Context, postID string) ([]model.UserSummary, error) {
|
|
rows, err := r.pg.Pool.Query(ctx, `
|
|
SELECT u.id, u.handle, u.name, u.avatar_url
|
|
FROM post_mentions m JOIN users u ON u.id = m.user_id
|
|
WHERE m.post_id = $1
|
|
ORDER BY m.created_at ASC`, postID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list mentions: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
out := []model.UserSummary{}
|
|
for rows.Next() {
|
|
var s model.UserSummary
|
|
if err := rows.Scan(&s.ID, &s.Handle, &s.Name, &s.AvatarURL); err != nil {
|
|
return nil, fmt.Errorf("scan mention: %w", err)
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// HydrateCounts fills MentionsCount on the given posts (one grouped query).
|
|
func (r *MentionRepo) HydrateCounts(ctx context.Context, posts []*model.Post) error {
|
|
ids := make([]string, 0, len(posts))
|
|
for _, p := range posts {
|
|
if p != nil {
|
|
ids = append(ids, p.ID)
|
|
}
|
|
}
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
rows, err := r.pg.Pool.Query(ctx,
|
|
`SELECT post_id, count(*) FROM post_mentions WHERE post_id = ANY($1) GROUP BY post_id`, ids)
|
|
if err != nil {
|
|
return fmt.Errorf("hydrate mention counts: %w", err)
|
|
}
|
|
counts := make(map[string]int, len(ids))
|
|
for rows.Next() {
|
|
var id string
|
|
var c int
|
|
if err := rows.Scan(&id, &c); err != nil {
|
|
rows.Close()
|
|
return fmt.Errorf("scan mention count: %w", err)
|
|
}
|
|
counts[id] = c
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, p := range posts {
|
|
if p != nil {
|
|
p.MentionsCount = counts[p.ID]
|
|
}
|
|
}
|
|
return nil
|
|
}
|