2022-09-20 03:55:00 -07:00
package db
import (
"context"
2023-03-18 15:00:44 -07:00
"regexp"
2023-08-10 09:26:53 -07:00
"strings"
2023-06-02 18:06:26 -07:00
"time"
2022-09-20 03:55:00 -07:00
2023-08-17 09:49:32 -07:00
"codeberg.org/pronounscc/pronouns.cc/backend/common"
2022-09-20 03:55:00 -07:00
"emperror.dev/errors"
2023-06-02 18:06:26 -07:00
"github.com/Masterminds/squirrel"
2023-04-03 19:11:03 -07:00
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
2022-09-20 03:55:00 -07:00
"github.com/rs/xid"
)
2022-11-21 08:01:51 -08:00
const (
MaxMemberCount = 500
MaxMemberNameLength = 100
)
2022-10-03 01:59:30 -07:00
2022-09-20 03:55:00 -07:00
type Member struct {
2022-11-20 12:09:29 -08:00
ID xid . ID
UserID xid . ID
2023-08-17 09:49:32 -07:00
SnowflakeID common . MemberID
2023-06-02 18:06:26 -07:00
SID string ` db:"sid" `
2022-11-20 12:09:29 -08:00
Name string
DisplayName * string
Bio * string
2023-03-12 18:04:09 -07:00
Avatar * string
2022-11-20 12:09:29 -08:00
Links [ ] string
2023-01-30 15:50:17 -08:00
Names [ ] FieldEntry
Pronouns [ ] PronounEntry
2023-04-01 08:20:59 -07:00
Unlisted bool
2022-09-20 03:55:00 -07:00
}
2022-10-03 01:59:30 -07:00
const (
ErrMemberNotFound = errors . Sentinel ( "member not found" )
ErrMemberNameInUse = errors . Sentinel ( "member name already in use" )
)
2022-09-20 03:55:00 -07:00
2023-03-18 15:00:44 -07:00
// member names must match this regex
2023-10-27 15:58:20 -07:00
var memberNameRegex = regexp . MustCompile ( "^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$" )
2023-03-18 15:00:44 -07:00
2023-08-10 09:26:53 -07:00
// List of member names that cannot be used because they would break routing or be inaccessible due to page conflicts.
var invalidMemberNames = [ ] string {
2023-10-27 15:58:20 -07:00
// these break routing outright
2023-08-10 09:26:53 -07:00
"." ,
".." ,
2023-10-27 15:58:20 -07:00
// the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible
2023-08-10 09:26:53 -07:00
"edit" ,
}
2023-03-18 15:00:44 -07:00
func MemberNameValid ( name string ) bool {
2023-08-10 09:26:53 -07:00
for i := range invalidMemberNames {
if strings . EqualFold ( name , invalidMemberNames [ i ] ) {
return false
}
2023-05-11 16:09:02 -07:00
}
2023-03-18 15:00:44 -07:00
return memberNameRegex . MatchString ( name )
}
2023-03-11 16:31:10 -08:00
func ( db * DB ) Member ( ctx context . Context , id xid . ID ) ( m Member , err error ) {
sql , args , err := sq . Select ( "*" ) . From ( "members" ) . Where ( "id = ?" , id ) . ToSql ( )
2022-09-20 03:55:00 -07:00
if err != nil {
2023-03-11 16:31:10 -08:00
return m , errors . Wrap ( err , "building sql" )
2022-09-20 03:55:00 -07:00
}
2023-03-11 16:31:10 -08:00
err = pgxscan . Get ( ctx , db , & m , sql , args ... )
2022-09-20 03:55:00 -07:00
if err != nil {
2023-03-11 16:31:10 -08:00
return m , errors . Wrap ( err , "executing query" )
2022-09-20 03:55:00 -07:00
}
return m , nil
}
2023-08-20 13:45:14 -07:00
func ( db * DB ) MemberBySnowflake ( ctx context . Context , id common . MemberID ) ( m Member , err error ) {
sql , args , err := sq . Select ( "*" ) . From ( "members" ) . Where ( "snowflake_id = ?" , id ) . ToSql ( )
if err != nil {
return m , errors . Wrap ( err , "building sql" )
}
err = pgxscan . Get ( ctx , db , & m , sql , args ... )
if err != nil {
return m , errors . Wrap ( err , "executing query" )
}
return m , nil
}
2022-11-22 04:31:42 -08:00
// UserMember returns a member scoped by user.
2022-09-20 03:55:00 -07:00
func ( db * DB ) UserMember ( ctx context . Context , userID xid . ID , memberRef string ) ( m Member , err error ) {
2023-09-02 07:34:51 -07:00
sf , _ := common . ParseSnowflake ( memberRef ) // error can be ignored as the zero value will never be used as an ID
sql , args , err := sq . Select ( "*" ) . From ( "members" ) . Where ( "user_id = ?" , userID ) . Where ( "(id = ? or snowflake_id = ? or name = ?)" , memberRef , sf , memberRef ) . ToSql ( )
2022-09-20 03:55:00 -07:00
if err != nil {
2023-03-11 16:31:10 -08:00
return m , errors . Wrap ( err , "building sql" )
2022-09-20 03:55:00 -07:00
}
2023-03-11 16:31:10 -08:00
err = pgxscan . Get ( ctx , db , & m , sql , args ... )
2022-09-20 03:55:00 -07:00
if err != nil {
2023-03-11 16:31:10 -08:00
return m , errors . Wrap ( err , "executing query" )
2022-09-20 03:55:00 -07:00
}
return m , nil
}
2023-06-02 18:06:26 -07:00
// MemberBySID gets a user by their short ID.
func ( db * DB ) MemberBySID ( ctx context . Context , sid string ) ( u Member , err error ) {
sql , args , err := sq . Select ( "*" ) . From ( "members" ) . Where ( "sid = ?" , sid ) . ToSql ( )
if err != nil {
return u , errors . Wrap ( err , "building sql" )
}
err = pgxscan . Get ( ctx , db , & u , sql , args ... )
if err != nil {
if errors . Cause ( err ) == pgx . ErrNoRows {
return u , ErrMemberNotFound
}
return u , errors . Wrap ( err , "getting members from db" )
}
return u , nil
}
2022-11-22 04:31:42 -08:00
// UserMembers returns all of a user's members, sorted by name.
2023-04-01 08:20:59 -07:00
func ( db * DB ) UserMembers ( ctx context . Context , userID xid . ID , showHidden bool ) ( ms [ ] Member , err error ) {
builder := sq . Select ( "*" ) .
2023-01-30 15:50:17 -08:00
From ( "members" ) . Where ( "user_id = ?" , userID ) .
2023-04-01 08:20:59 -07:00
OrderBy ( "name" , "id" )
if ! showHidden {
builder = builder . Where ( "unlisted = ?" , false )
}
sql , args , err := builder . ToSql ( )
2022-09-20 03:55:00 -07:00
if err != nil {
return nil , errors . Wrap ( err , "building sql" )
}
err = pgxscan . Select ( ctx , db , & ms , sql , args ... )
if err != nil {
return nil , errors . Wrap ( err , "retrieving members" )
}
if ms == nil {
ms = make ( [ ] Member , 0 )
}
return ms , nil
}
2022-10-03 01:59:30 -07:00
// CreateMember creates a member.
2023-03-11 16:31:10 -08:00
func ( db * DB ) CreateMember (
ctx context . Context , tx pgx . Tx , userID xid . ID ,
name string , displayName * string , bio string , links [ ] string ,
) ( m Member , err error ) {
2022-10-03 01:59:30 -07:00
sql , args , err := sq . Insert ( "members" ) .
2023-08-17 09:49:32 -07:00
Columns ( "user_id" , "snowflake_id" , "id" , "sid" , "name" , "display_name" , "bio" , "links" ) .
Values ( userID , common . GenerateID ( ) , xid . New ( ) , squirrel . Expr ( "find_free_member_sid()" ) , name , displayName , bio , links ) .
2023-03-11 16:31:10 -08:00
Suffix ( "RETURNING *" ) . ToSql ( )
2022-10-03 01:59:30 -07:00
if err != nil {
return m , errors . Wrap ( err , "building sql" )
}
2023-03-11 16:31:10 -08:00
err = pgxscan . Get ( ctx , tx , & m , sql , args ... )
2022-10-03 01:59:30 -07:00
if err != nil {
pge := & pgconn . PgError { }
if errors . As ( err , & pge ) {
2022-12-22 16:31:43 -08:00
// unique constraint violation
2023-05-25 04:40:15 -07:00
if pge . Code == uniqueViolation {
2022-10-03 01:59:30 -07:00
return m , ErrMemberNameInUse
}
}
return m , errors . Wrap ( err , "executing query" )
}
return m , nil
}
2022-11-22 04:31:42 -08:00
// DeleteMember deletes a member by the given ID. This is irreversible.
func ( db * DB ) DeleteMember ( ctx context . Context , id xid . ID ) ( err error ) {
sql , args , err := sq . Delete ( "members" ) . Where ( "id = ?" , id ) . ToSql ( )
if err != nil {
return errors . Wrap ( err , "building sql" )
}
_ , err = db . Exec ( ctx , sql , args ... )
if err != nil {
return errors . Wrap ( err , "deleting member" )
}
return nil
}
2022-10-03 01:59:30 -07:00
// MemberCount returns the number of members that the given user has.
func ( db * DB ) MemberCount ( ctx context . Context , userID xid . ID ) ( n int64 , err error ) {
sql , args , err := sq . Select ( "count(id)" ) . From ( "members" ) . Where ( "user_id = ?" , userID ) . ToSql ( )
if err != nil {
return 0 , errors . Wrap ( err , "building sql" )
}
err = db . QueryRow ( ctx , sql , args ... ) . Scan ( & n )
if err != nil {
return 0 , errors . Wrap ( err , "executing query" )
}
return n , nil
}
2022-11-21 08:01:51 -08:00
func ( db * DB ) UpdateMember (
ctx context . Context ,
tx pgx . Tx , id xid . ID ,
name , displayName , bio * string ,
2023-04-01 08:20:59 -07:00
unlisted * bool ,
2022-11-21 08:01:51 -08:00
links * [ ] string ,
2023-03-12 18:04:09 -07:00
avatar * string ,
2022-11-21 08:01:51 -08:00
) ( m Member , err error ) {
2023-03-12 18:04:09 -07:00
if name == nil && displayName == nil && bio == nil && links == nil && avatar == nil {
2023-03-11 16:31:10 -08:00
// get member
sql , args , err := sq . Select ( "*" ) . From ( "members" ) . Where ( "id = ?" , id ) . ToSql ( )
if err != nil {
return m , errors . Wrap ( err , "building sql" )
}
err = pgxscan . Get ( ctx , tx , & m , sql , args ... )
if err != nil {
return m , errors . Wrap ( err , "executing query" )
}
return m , nil
2022-11-21 08:01:51 -08:00
}
2023-03-11 16:31:10 -08:00
builder := sq . Update ( "members" ) . Where ( "id = ?" , id ) . Suffix ( "RETURNING *" )
2022-11-21 08:01:51 -08:00
if name != nil {
if * name == "" {
2023-03-14 09:06:35 -07:00
return m , errors . Wrap ( err , "name was empty" )
2022-11-21 08:01:51 -08:00
} else {
2023-03-14 09:06:35 -07:00
builder = builder . Set ( "name" , * name )
2022-11-21 08:01:51 -08:00
}
}
if displayName != nil {
if * displayName == "" {
builder = builder . Set ( "display_name" , nil )
} else {
builder = builder . Set ( "display_name" , * displayName )
}
}
if bio != nil {
if * bio == "" {
builder = builder . Set ( "bio" , nil )
} else {
builder = builder . Set ( "bio" , * bio )
}
}
if links != nil {
2023-03-11 16:31:10 -08:00
builder = builder . Set ( "links" , * links )
2022-11-21 08:01:51 -08:00
}
2023-04-01 08:20:59 -07:00
if unlisted != nil {
builder = builder . Set ( "unlisted" , * unlisted )
}
2022-11-21 08:01:51 -08:00
2023-03-12 18:04:09 -07:00
if avatar != nil {
if * avatar == "" {
builder = builder . Set ( "avatar" , nil )
} else {
builder = builder . Set ( "avatar" , avatar )
}
2022-11-21 08:01:51 -08:00
}
2023-01-30 15:50:17 -08:00
sql , args , err := builder . ToSql ( )
2022-11-21 08:01:51 -08:00
if err != nil {
return m , errors . Wrap ( err , "building sql" )
}
2023-03-11 16:31:10 -08:00
err = pgxscan . Get ( ctx , tx , & m , sql , args ... )
2022-11-21 08:01:51 -08:00
if err != nil {
pge := & pgconn . PgError { }
if errors . As ( err , & pge ) {
2023-05-25 04:40:15 -07:00
if pge . Code == uniqueViolation {
2022-11-21 08:01:51 -08:00
return m , ErrMemberNameInUse
}
}
return m , errors . Wrap ( err , "executing sql" )
}
return m , nil
}
2023-06-02 18:06:26 -07:00
func ( db * DB ) RerollMemberSID ( ctx context . Context , userID , memberID xid . ID ) ( newID string , err error ) {
tx , err := db . Begin ( ctx )
if err != nil {
return "" , errors . Wrap ( err , "beginning transaction" )
}
defer tx . Rollback ( ctx )
sql , args , err := sq . Update ( "members" ) .
Set ( "sid" , squirrel . Expr ( "find_free_member_sid()" ) ) .
Where ( "id = ?" , memberID ) .
Suffix ( "RETURNING sid" ) . ToSql ( )
if err != nil {
return "" , errors . Wrap ( err , "building sql" )
}
err = tx . QueryRow ( ctx , sql , args ... ) . Scan ( & newID )
if err != nil {
return "" , errors . Wrap ( err , "executing query" )
}
sql , args , err = sq . Update ( "users" ) .
Set ( "last_sid_reroll" , time . Now ( ) ) .
Where ( "id = ?" , userID ) . ToSql ( )
if err != nil {
return "" , errors . Wrap ( err , "building sql" )
}
_ , err = tx . Exec ( ctx , sql , args ... )
if err != nil {
return "" , errors . Wrap ( err , "executing query" )
}
err = tx . Commit ( ctx )
if err != nil {
return "" , errors . Wrap ( err , "committing transaction" )
}
return newID , nil
}