package db import ( "context" "time" "emperror.dev/errors" "github.com/georgysavva/scany/pgxscan" "github.com/jackc/pgx/v4" "github.com/rs/xid" ) type Report struct { ID int64 `json:"id"` UserID xid.ID `json:"user_id"` UserName string `json:"user_name"` MemberID xid.ID `json:"member_id"` MemberName *string `json:"member_name"` Reason string `json:"reason"` ReporterID xid.ID `json:"reporter_id"` CreatedAt time.Time `json:"created_at"` ResolvedAt *time.Time `json:"resolved_at"` AdminID xid.ID `json:"admin_id"` AdminComment *string `json:"admin_comment"` } const ReportPageSize = 100 const ErrReportNotFound = errors.Sentinel("report not found") func (db *DB) Reports(ctx context.Context, closed bool, before int) (rs []Report, err error) { builder := sq.Select("*", "(SELECT username FROM users WHERE id = reports.user_id) AS user_name", "(SELECT name FROM members WHERE id = reports.member_id) AS member_name"). From("reports"). Limit(ReportPageSize). OrderBy("id DESC") if before != 0 { builder = builder.Where("id < ?", before) } if closed { builder = builder.Where("resolved_at IS NOT NULL") } else { builder = builder.Where("resolved_at IS NULL") } sql, args, err := builder.ToSql() if err != nil { return nil, errors.Wrap(err, "building sql") } err = pgxscan.Select(ctx, db, &rs, sql, args...) if err != nil { return nil, errors.Wrap(err, "executing query") } if len(rs) == 0 { return []Report{}, nil } return rs, nil } func (db *DB) ReportsByUser(ctx context.Context, userID xid.ID, before int) (rs []Report, err error) { builder := sq.Select("*").From("reports").Where("user_id = ?", userID).Limit(ReportPageSize).OrderBy("id DESC") if before != 0 { builder = builder.Where("id < ?", before) } sql, args, err := builder.ToSql() if err != nil { return nil, errors.Wrap(err, "building sql") } err = pgxscan.Select(ctx, db, &rs, sql, args...) if err != nil { return nil, errors.Wrap(err, "executing query") } if len(rs) == 0 { return []Report{}, nil } return rs, nil } func (db *DB) ReportsByReporter(ctx context.Context, reporterID xid.ID, before int) (rs []Report, err error) { builder := sq.Select("*").From("reports").Where("reporter_id = ?", reporterID).Limit(ReportPageSize).OrderBy("id DESC") if before != 0 { builder = builder.Where("id < ?", before) } sql, args, err := builder.ToSql() if err != nil { return nil, errors.Wrap(err, "building sql") } err = pgxscan.Select(ctx, db, &rs, sql, args...) if err != nil { return nil, errors.Wrap(err, "executing query") } if len(rs) == 0 { return []Report{}, nil } return rs, nil } func (db *DB) Report(ctx context.Context, tx pgx.Tx, id int64) (r Report, err error) { sql, args, err := sq.Select("*").From("reports").Where("id = ?", id).ToSql() if err != nil { return r, errors.Wrap(err, "building sql") } err = pgxscan.Get(ctx, tx, &r, sql, args...) if err != nil { if errors.Cause(err) == pgx.ErrNoRows { return r, ErrReportNotFound } return r, errors.Wrap(err, "executing query") } return r, nil } func (db *DB) CreateReport(ctx context.Context, reporterID, userID xid.ID, memberID *xid.ID, reason string) (r Report, err error) { sql, args, err := sq.Insert("reports").SetMap(map[string]any{ "user_id": userID, "reporter_id": reporterID, "member_id": memberID, "reason": reason, }).Suffix("RETURNING *").ToSql() if err != nil { return r, errors.Wrap(err, "building sql") } err = pgxscan.Get(ctx, db, &r, sql, args...) if err != nil { return r, errors.Wrap(err, "executing query") } return r, nil } func (db *DB) ResolveReport(ctx context.Context, ex Execer, id int64, adminID xid.ID, comment string) error { sql, args, err := sq.Update("reports"). Set("admin_id", adminID). Set("admin_comment", comment). Set("resolved_at", time.Now().UTC()). Where("id = ?", id).ToSql() if err != nil { return errors.Wrap(err, "building sql") } _, err = ex.Exec(ctx, sql, args...) if err != nil { return errors.Wrap(err, "executing query") } return nil } type Warning struct { ID int64 `json:"id"` UserID xid.ID `json:"-"` Reason string `json:"reason"` CreatedAt time.Time `json:"created_at"` ReadAt *time.Time `json:"-"` } func (db *DB) CreateWarning(ctx context.Context, tx pgx.Tx, userID xid.ID, reason string) (w Warning, err error) { sql, args, err := sq.Insert("warnings").SetMap(map[string]any{ "user_id": userID, "reason": reason, }).ToSql() if err != nil { return w, errors.Wrap(err, "building sql") } err = pgxscan.Get(ctx, tx, &w, sql, args...) if err != nil { return w, errors.Wrap(err, "executing query") } return w, nil } func (db *DB) Warnings(ctx context.Context, userID xid.ID, unread bool) (ws []Warning, err error) { builder := sq.Select("*").From("warnings").Where("user_id = ?", userID).OrderBy("id DESC") if unread { builder = builder.Where("read_at IS NULL") } sql, args, err := builder.ToSql() if err != nil { return ws, errors.Wrap(err, "building sql") } err = pgxscan.Select(ctx, db, &ws, sql, args...) if err != nil { return nil, errors.Wrap(err, "executing query") } if len(ws) == 0 { return []Warning{}, nil } return ws, nil } func (db *DB) AckWarning(ctx context.Context, userID xid.ID, id int64) (ok bool, err error) { sql, args, err := sq.Update("warnings"). Set("read_at", time.Now().UTC()). Where("user_id = ?", userID). Where("id = ?", id). Where("read_at IS NULL").ToSql() if err != nil { return false, errors.Wrap(err, "building sql") } ct, err := db.Exec(ctx, sql, args...) if err != nil { return false, errors.Wrap(err, "executing query") } if ct.RowsAffected() == 0 { return false, nil } return true, nil }