2022-11-21 08:01:51 -08:00
package member
import (
"fmt"
"net/http"
2023-03-18 15:00:44 -07:00
"strings"
2023-06-02 18:06:26 -07:00
"time"
2022-11-21 08:01:51 -08:00
2023-06-03 07:18:47 -07:00
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
2022-11-21 08:01:51 -08:00
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
2023-12-29 19:27:08 -08:00
"github.com/jackc/pgx/v5"
2022-11-21 08:01:51 -08:00
"github.com/rs/xid"
)
type PatchMemberRequest struct {
2023-01-30 15:50:17 -08:00
Name * string ` json:"name" `
Bio * string ` json:"bio" `
DisplayName * string ` json:"display_name" `
Links * [ ] string ` json:"links" `
Names * [ ] db . FieldEntry ` json:"names" `
Pronouns * [ ] db . PronounEntry ` json:"pronouns" `
Fields * [ ] db . Field ` json:"fields" `
Avatar * string ` json:"avatar" `
2023-04-01 08:20:59 -07:00
Unlisted * bool ` json:"unlisted" `
2023-05-25 06:21:50 -07:00
Flags * [ ] xid . ID ` json:"flags" `
2022-11-21 08:01:51 -08:00
}
func ( s * Server ) patchMember ( w http . ResponseWriter , r * http . Request ) error {
ctx := r . Context ( )
claims , _ := server . ClaimsFromContext ( ctx )
2023-03-30 07:58:35 -07:00
if ! claims . TokenWrite {
return server . APIError { Code : server . ErrMissingPermissions , Details : "This token is read-only" }
}
2023-04-19 03:00:21 -07:00
u , err := s . DB . User ( ctx , claims . UserID )
if err != nil {
return errors . Wrap ( err , "getting user" )
}
2023-09-06 16:43:05 -07:00
var m db . Member
if id , err := xid . FromString ( chi . URLParam ( r , "memberRef" ) ) ; err == nil {
log . Debugf ( "%v/%v is xid" , chi . URLParam ( r , "memberRef" ) , id )
m , err = s . DB . Member ( ctx , id )
if err != nil {
if err == db . ErrMemberNotFound {
return server . APIError { Code : server . ErrMemberNotFound }
}
return errors . Wrap ( err , "getting member" )
}
} else {
id , err := common . ParseSnowflake ( chi . URLParam ( r , "memberRef" ) )
if err != nil {
2022-11-21 08:01:51 -08:00
return server . APIError { Code : server . ErrMemberNotFound }
}
2023-09-06 16:43:05 -07:00
m , err = s . DB . MemberBySnowflake ( ctx , common . MemberID ( id ) )
if err != nil {
if err == db . ErrMemberNotFound {
return server . APIError { Code : server . ErrMemberNotFound }
}
return errors . Wrap ( err , "getting member" )
}
2022-11-21 08:01:51 -08:00
}
if m . UserID != claims . UserID {
return server . APIError { Code : server . ErrNotOwnMember }
}
var req PatchMemberRequest
err = render . Decode ( r , & req )
if err != nil {
return server . APIError { Code : server . ErrBadRequest }
}
// validate that *something* is set
if req . DisplayName == nil &&
req . Name == nil &&
req . Bio == nil &&
2023-04-01 08:20:59 -07:00
req . Unlisted == nil &&
2022-11-21 08:01:51 -08:00
req . Links == nil &&
req . Fields == nil &&
req . Names == nil &&
req . Pronouns == nil &&
2023-05-25 06:21:50 -07:00
req . Avatar == nil &&
req . Flags == nil {
2022-11-21 08:01:51 -08:00
return server . APIError {
Code : server . ErrBadRequest ,
Details : "Data must not be empty" ,
}
}
2023-03-18 15:00:44 -07:00
// trim whitespace from strings
if req . Name != nil {
* req . Name = strings . TrimSpace ( * req . Name )
}
if req . DisplayName != nil {
2023-03-18 20:05:11 -07:00
* req . DisplayName = strings . TrimSpace ( * req . DisplayName )
2023-03-18 15:00:44 -07:00
}
if req . Bio != nil {
* req . Bio = strings . TrimSpace ( * req . Bio )
}
2023-03-11 16:31:31 -08:00
if req . Name != nil && * req . Name == "" {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "Name must not be empty" ,
}
2023-03-18 15:00:44 -07:00
} else if req . Name != nil && len ( * req . Name ) > 100 {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "Name may not be longer than 100 characters" ,
}
}
// validate member name
if req . Name != nil {
if ! db . MemberNameValid ( * req . Name ) {
return server . APIError {
Code : server . ErrBadRequest ,
2023-05-11 16:39:02 -07:00
Details : "Member name cannot contain any of the following: @, \\, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), +, <, =, >, ^, |, ~, `, , and cannot be one or two periods." ,
2023-03-18 15:00:44 -07:00
}
}
2023-03-11 16:31:31 -08:00
}
2022-11-21 08:01:51 -08:00
// validate display name/bio
2023-04-02 13:50:22 -07:00
if common . StringLength ( req . Name ) > db . MaxMemberNameLength {
2022-11-21 08:01:51 -08:00
return server . APIError {
Code : server . ErrBadRequest ,
2023-04-02 13:50:22 -07:00
Details : fmt . Sprintf ( "Name name too long (max %d, current %d)" , db . MaxMemberNameLength , common . StringLength ( req . Name ) ) ,
2022-11-21 08:01:51 -08:00
}
}
2023-04-02 13:50:22 -07:00
if common . StringLength ( req . DisplayName ) > db . MaxDisplayNameLength {
2022-11-21 08:01:51 -08:00
return server . APIError {
Code : server . ErrBadRequest ,
2023-04-02 13:50:22 -07:00
Details : fmt . Sprintf ( "Display name too long (max %d, current %d)" , db . MaxDisplayNameLength , common . StringLength ( req . DisplayName ) ) ,
2022-11-21 08:01:51 -08:00
}
}
2023-04-02 13:50:22 -07:00
if common . StringLength ( req . Bio ) > db . MaxUserBioLength {
2022-11-21 08:01:51 -08:00
return server . APIError {
Code : server . ErrBadRequest ,
2023-04-02 13:50:22 -07:00
Details : fmt . Sprintf ( "Bio too long (max %d, current %d)" , db . MaxUserBioLength , common . StringLength ( req . Name ) ) ,
2022-11-21 08:01:51 -08:00
}
}
// validate links
if req . Links != nil {
if len ( * req . Links ) > db . MaxUserLinksLength {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Too many links (max %d, current %d)" , db . MaxUserLinksLength , len ( * req . Links ) ) ,
}
}
for i , link := range * req . Links {
if len ( link ) > db . MaxLinkLength {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Link %d too long (max %d, current %d)" , i , db . MaxLinkLength , len ( link ) ) ,
}
}
}
}
2023-05-28 17:59:15 -07:00
// validate flag length
if req . Flags != nil {
if len ( * req . Flags ) > db . MaxPrideFlags {
return server . APIError {
Code : server . ErrBadRequest ,
Details : fmt . Sprintf ( "Too many flags (max %d, current %d)" , len ( * req . Flags ) , db . MaxPrideFlags ) ,
}
}
}
2023-04-19 03:00:21 -07:00
if err := validateSlicePtr ( "name" , req . Names , u . CustomPreferences ) ; err != nil {
2023-03-13 17:30:46 -07:00
return * err
2022-11-21 08:01:51 -08:00
}
2023-04-19 03:00:21 -07:00
if err := validateSlicePtr ( "pronoun" , req . Pronouns , u . CustomPreferences ) ; err != nil {
2023-03-13 17:30:46 -07:00
return * err
2022-11-21 08:01:51 -08:00
}
2023-04-19 03:00:21 -07:00
if err := validateSlicePtr ( "field" , req . Fields , u . CustomPreferences ) ; err != nil {
2023-03-13 17:30:46 -07:00
return * err
2022-11-21 08:01:51 -08:00
}
// update avatar
2023-03-12 18:04:09 -07:00
var avatarHash * string = nil
2022-11-21 08:01:51 -08:00
if req . Avatar != nil {
2023-03-12 18:19:03 -07:00
if * req . Avatar == "" {
if m . Avatar != nil {
err = s . DB . DeleteMemberAvatar ( ctx , m . ID , * m . Avatar )
if err != nil {
log . Errorf ( "deleting member avatar: %v" , err )
return errors . Wrap ( err , "deleting avatar" )
2022-11-21 08:01:51 -08:00
}
}
2023-03-12 18:19:03 -07:00
avatarHash = req . Avatar
} else {
webp , jpg , err := s . DB . ConvertAvatar ( * req . Avatar )
if err != nil {
if err == db . ErrInvalidDataURI {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "invalid avatar data URI" ,
}
} else if err == db . ErrInvalidContentType {
return server . APIError {
Code : server . ErrBadRequest ,
Details : "invalid avatar content type" ,
}
}
2022-11-21 08:01:51 -08:00
2023-03-12 18:19:03 -07:00
log . Errorf ( "converting member avatar: %v" , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "converting member avatar" )
2023-03-12 18:19:03 -07:00
}
2022-11-21 08:01:51 -08:00
2023-03-26 20:00:16 -07:00
hash , err := s . DB . WriteMemberAvatar ( ctx , m . ID , webp , jpg )
2023-03-12 18:19:03 -07:00
if err != nil {
log . Errorf ( "uploading member avatar: %v" , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "writing member avatar" )
2023-03-12 18:19:03 -07:00
}
avatarHash = & hash
2023-03-23 02:07:51 -07:00
// delete current avatar if member has one
if m . Avatar != nil {
2023-03-26 20:00:16 -07:00
err = s . DB . DeleteMemberAvatar ( ctx , m . ID , * m . Avatar )
2023-03-23 02:07:51 -07:00
if err != nil {
log . Errorf ( "deleting existing avatar: %v" , err )
}
}
2022-11-21 08:01:51 -08:00
}
}
// start transaction
tx , err := s . DB . Begin ( ctx )
if err != nil {
log . Errorf ( "creating transaction: %v" , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "creating transaction" )
2022-11-21 08:01:51 -08:00
}
2023-12-29 19:27:08 -08:00
defer func ( ) {
err := tx . Rollback ( ctx )
if err != nil && ! errors . Is ( err , pgx . ErrTxClosed ) {
log . Error ( "rolling back transaction:" , err )
}
} ( )
2022-11-21 08:01:51 -08:00
2023-09-06 16:43:05 -07:00
m , err = s . DB . UpdateMember ( ctx , tx , m . ID , req . Name , req . DisplayName , req . Bio , req . Unlisted , req . Links , avatarHash )
2022-11-21 08:01:51 -08:00
if err != nil {
switch errors . Cause ( err ) {
case db . ErrNothingToUpdate :
case db . ErrMemberNameInUse :
return server . APIError { Code : server . ErrMemberNameInUse }
default :
log . Errorf ( "updating member: %v" , err )
return errors . Wrap ( err , "updating member in db" )
}
}
2023-01-30 15:50:17 -08:00
if req . Names != nil || req . Pronouns != nil {
names := m . Names
pronouns := m . Pronouns
2022-11-21 08:01:51 -08:00
2023-01-30 15:50:17 -08:00
if req . Names != nil {
names = * req . Names
2022-11-21 08:01:51 -08:00
}
2023-01-30 15:50:17 -08:00
if req . Pronouns != nil {
pronouns = * req . Pronouns
2022-11-21 08:01:51 -08:00
}
2023-09-06 16:43:05 -07:00
err = s . DB . SetMemberNamesPronouns ( ctx , tx , m . ID , names , pronouns )
2022-11-21 08:01:51 -08:00
if err != nil {
2023-09-06 16:43:05 -07:00
log . Errorf ( "setting names for member %v: %v" , m . ID , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "setting names/pronouns" )
2022-11-21 08:01:51 -08:00
}
2023-01-30 15:50:17 -08:00
m . Names = names
m . Pronouns = pronouns
2022-11-21 08:01:51 -08:00
}
2023-01-30 15:50:17 -08:00
var fields [ ] db . Field
2022-11-21 08:01:51 -08:00
if req . Fields != nil {
2023-09-06 16:43:05 -07:00
err = s . DB . SetMemberFields ( ctx , tx , m . ID , * req . Fields )
2022-11-21 08:01:51 -08:00
if err != nil {
2023-09-06 16:43:05 -07:00
log . Errorf ( "setting fields for member %v: %v" , m . ID , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "setting fields" )
2022-11-21 08:01:51 -08:00
}
fields = * req . Fields
} else {
2023-09-06 16:43:05 -07:00
fields , err = s . DB . MemberFields ( ctx , m . ID )
2022-11-21 08:01:51 -08:00
if err != nil {
2023-09-06 16:43:05 -07:00
log . Errorf ( "getting fields for member %v: %v" , m . ID , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "getting fields" )
2022-11-21 08:01:51 -08:00
}
}
2023-05-25 06:21:50 -07:00
// update flags
if req . Flags != nil {
err = s . DB . SetMemberFlags ( ctx , tx , m . ID , * req . Flags )
if err != nil {
if err == db . ErrInvalidFlagID {
return server . APIError { Code : server . ErrBadRequest , Details : "One or more flag IDs are unknown" }
}
log . Errorf ( "updating flags for member %v: %v" , m . ID , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "updating flags" )
2023-05-25 06:21:50 -07:00
}
}
2023-05-01 17:54:08 -07:00
// update last active time
err = s . DB . UpdateActiveTime ( ctx , tx , claims . UserID )
if err != nil {
log . Errorf ( "updating last active time for user %v: %v" , claims . UserID , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "updating last active time" )
2023-05-01 17:54:08 -07:00
}
2022-11-21 08:01:51 -08:00
err = tx . Commit ( ctx )
if err != nil {
log . Errorf ( "committing transaction: %v" , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "committing transaction" )
2022-11-21 08:01:51 -08:00
}
2023-05-25 06:21:50 -07:00
// get flags to return (we need to return full flag objects, not the array of IDs in the request body)
flags , err := s . DB . MemberFlags ( ctx , m . ID )
if err != nil {
log . Errorf ( "getting user flags: %v" , err )
2023-09-19 17:39:14 -07:00
return errors . Wrap ( err , "getting flags" )
2023-05-25 06:21:50 -07:00
}
2022-11-21 08:01:51 -08:00
// echo the updated member back on success
2023-05-25 06:21:50 -07:00
render . JSON ( w , r , dbMemberToMember ( u , m , fields , flags , true ) )
2022-11-21 08:01:51 -08:00
return nil
}
2023-06-02 18:06:26 -07:00
2023-09-07 08:01:31 -07:00
func ( s * Server ) rerollMemberSID ( w http . ResponseWriter , r * http . Request ) ( err error ) {
2023-06-02 18:06:26 -07:00
ctx := r . Context ( )
claims , _ := server . ClaimsFromContext ( ctx )
if ! claims . TokenWrite {
return server . APIError { Code : server . ErrMissingPermissions , Details : "This token is read-only" }
}
2023-09-07 08:01:31 -07:00
var m db . Member
if id , err := xid . FromString ( chi . URLParam ( r , "memberRef" ) ) ; err == nil {
m , err = s . DB . Member ( ctx , id )
if err != nil {
if err == db . ErrMemberNotFound {
return server . APIError { Code : server . ErrMemberNotFound }
}
log . Errorf ( "getting user %v: %v" , id , err )
return errors . Wrap ( err , "getting user" )
}
} else {
id , err := common . ParseSnowflake ( chi . URLParam ( r , "memberRef" ) )
if err != nil {
return server . APIError { Code : server . ErrMemberNotFound }
}
m , err = s . DB . MemberBySnowflake ( ctx , common . MemberID ( id ) )
if err != nil {
if err == db . ErrMemberNotFound {
return server . APIError { Code : server . ErrMemberNotFound }
}
log . Errorf ( "getting user %v: %v" , id , err )
return errors . Wrap ( err , "getting user" )
}
2023-06-02 18:06:26 -07:00
}
u , err := s . DB . User ( ctx , claims . UserID )
if err != nil {
return errors . Wrap ( err , "getting user" )
}
if m . UserID != claims . UserID {
return server . APIError { Code : server . ErrNotOwnMember }
}
if time . Now ( ) . Add ( - time . Hour ) . Before ( u . LastSIDReroll ) {
return server . APIError { Code : server . ErrRerollingTooQuickly }
}
newID , err := s . DB . RerollMemberSID ( ctx , u . ID , m . ID )
if err != nil {
return errors . Wrap ( err , "updating member SID" )
}
m . SID = newID
render . JSON ( w , r , dbMemberToMember ( u , m , nil , nil , true ) )
return nil
}