ed008a99e6
An admin must have the admin.user.ban scope in order to search for banned or disabled users in the member directory.
775 lines
20 KiB
Go
775 lines
20 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.nonshy.com/nonshy/website/pkg/config"
|
|
"code.nonshy.com/nonshy/website/pkg/log"
|
|
"code.nonshy.com/nonshy/website/pkg/utility"
|
|
"golang.org/x/crypto/bcrypt"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// User account table.
|
|
type User struct {
|
|
ID uint64 `gorm:"primaryKey"`
|
|
Username string `gorm:"uniqueIndex"`
|
|
Email string `gorm:"uniqueIndex"`
|
|
HashedPassword string `json:"-"`
|
|
IsAdmin bool `gorm:"index"`
|
|
Status UserStatus `gorm:"index"` // active, disabled
|
|
Visibility UserVisibility `gorm:"index"` // public, private
|
|
Name *string
|
|
Birthdate time.Time
|
|
Certified bool
|
|
Explicit bool `gorm:"index"` // user has opted-in to see explicit content
|
|
InnerCircle bool `gorm:"index"` // user is in the inner circle
|
|
CreatedAt time.Time `gorm:"index"`
|
|
UpdatedAt time.Time `gorm:"index"`
|
|
LastLoginAt time.Time `gorm:"index"`
|
|
|
|
// Relational tables.
|
|
ProfileField []ProfileField `json:"-"`
|
|
ProfilePhotoID *uint64
|
|
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
|
AdminGroups []*AdminGroup `gorm:"many2many:admin_group_users;" json:"-"`
|
|
|
|
// Current user's relationship to this user -- not stored in DB.
|
|
UserRelationship UserRelationship `gorm:"-"`
|
|
|
|
// Caches
|
|
cachePhotoTypes map[PhotoVisibility]struct{}
|
|
cacheBlockedUserIDs []uint64
|
|
cachePhotoIDs []uint64
|
|
}
|
|
|
|
type UserVisibility string
|
|
|
|
const (
|
|
UserVisibilityPublic UserVisibility = "public"
|
|
UserVisibilityExternal UserVisibility = "external"
|
|
UserVisibilityPrivate UserVisibility = "private"
|
|
)
|
|
|
|
// All visibility options.
|
|
var UserVisibilityOptions = []UserVisibility{
|
|
UserVisibilityPublic,
|
|
UserVisibilityExternal,
|
|
UserVisibilityPrivate,
|
|
}
|
|
|
|
// Preload related tables for the user (classmethod).
|
|
func (u *User) Preload() *gorm.DB {
|
|
return DB.Preload("ProfileField").Preload("ProfilePhoto").Preload("AdminGroups.Scopes")
|
|
}
|
|
|
|
// UserStatus options.
|
|
type UserStatus string
|
|
|
|
const (
|
|
UserStatusActive = "active"
|
|
UserStatusDisabled = "disabled"
|
|
UserStatusBanned = "banned"
|
|
)
|
|
|
|
// CreateUser. It is assumed username and email are correctly formatted.
|
|
func CreateUser(username, email, password string) (*User, error) {
|
|
// Verify username and email are unique.
|
|
if _, err := FindUser(username); err == nil {
|
|
return nil, errors.New("That username already exists. Please try a different username.")
|
|
} else if _, err := FindUser(email); err == nil {
|
|
return nil, errors.New("That email address is already registered.")
|
|
}
|
|
|
|
u := &User{
|
|
Username: username,
|
|
Email: email,
|
|
Status: UserStatusActive,
|
|
Visibility: UserVisibilityPublic,
|
|
}
|
|
|
|
if err := u.HashPassword(password); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := DB.Create(u)
|
|
return u, result.Error
|
|
}
|
|
|
|
// GetUser by ID.
|
|
func GetUser(userId uint64) (*User, error) {
|
|
user := &User{}
|
|
result := user.Preload().First(&user, userId)
|
|
return user, result.Error
|
|
}
|
|
|
|
// GetUsers queries for multiple user IDs and returns users in the same order.
|
|
func GetUsers(currentUser *User, userIDs []uint64) ([]*User, error) {
|
|
userMap, err := MapUsers(currentUser, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Re-order them per the original sequence.
|
|
var users = []*User{}
|
|
for _, uid := range userIDs {
|
|
if user, ok := userMap[uid]; ok {
|
|
users = append(users, user)
|
|
}
|
|
}
|
|
|
|
return users, nil
|
|
}
|
|
|
|
// GetUsersByUsernames queries for multiple usernames and returns users in the same order.
|
|
func GetUsersByUsernames(currentUser *User, usernames []string) ([]*User, error) {
|
|
// Map the usernames.
|
|
var (
|
|
usermap = map[string]*User{}
|
|
set = map[string]interface{}{}
|
|
distinct = []string{}
|
|
)
|
|
|
|
// Uniqueify usernames.
|
|
for _, name := range usernames {
|
|
if _, ok := set[name]; ok {
|
|
continue
|
|
}
|
|
set[name] = nil
|
|
distinct = append(distinct, name)
|
|
}
|
|
|
|
var (
|
|
users = []*User{}
|
|
result = (&User{}).Preload().Where("username IN ?", distinct).Find(&users)
|
|
)
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
// Map users.
|
|
for _, user := range users {
|
|
usermap[user.Username] = user
|
|
}
|
|
|
|
// Inject relationships.
|
|
SetUserRelationships(currentUser, users)
|
|
|
|
// Re-order them per the original sequence.
|
|
var ordered = []*User{}
|
|
for _, name := range usernames {
|
|
if user, ok := usermap[name]; ok {
|
|
ordered = append(ordered, user)
|
|
}
|
|
}
|
|
|
|
return ordered, nil
|
|
}
|
|
|
|
// FindUser by username or email.
|
|
func FindUser(username string) (*User, error) {
|
|
if username == "" {
|
|
return nil, errors.New("username is required")
|
|
}
|
|
|
|
u := &User{}
|
|
if strings.ContainsRune(username, '@') {
|
|
result := u.Preload().Where("email = ?", username).Limit(1).First(u)
|
|
return u, result.Error
|
|
}
|
|
result := u.Preload().Where("username = ?", username).Limit(1).First(u)
|
|
return u, result.Error
|
|
}
|
|
|
|
// IsValidUsername checks if a username is available and not reserved.
|
|
func IsValidUsername(username string) error {
|
|
// Check the formatting of the name.
|
|
if !config.UsernameRegexp.MatchString(username) {
|
|
return errors.New("Your username must consist of only numbers, letters, - . and be 3-32 characters.")
|
|
}
|
|
|
|
// Reserved username check.
|
|
for _, cmp := range config.ReservedUsernames {
|
|
if username == cmp {
|
|
return errors.New("That username is reserved, please choose a different username.")
|
|
}
|
|
}
|
|
|
|
// Does the username already exist?
|
|
if _, err := FindUser(username); err == nil {
|
|
return errors.New("That username already exists. Please try a different username.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsShyFrom tells whether the user is shy from the perspective of the other user.
|
|
//
|
|
// That is, depending on our profile visibility and friendship status.
|
|
func (u *User) IsShyFrom(other *User) bool {
|
|
// If we are not a private profile, we're shy from nobody.
|
|
if u.Visibility != UserVisibilityPrivate {
|
|
return false
|
|
}
|
|
|
|
// Not shy from our friends.
|
|
if AreFriends(u.ID, other.ID) {
|
|
return false
|
|
}
|
|
|
|
// Our profile must be private & we are not friended, we are shy.
|
|
return true
|
|
}
|
|
|
|
// CanBeSeenBy checks whether the user can be seen to exist by the viewer.
|
|
//
|
|
// An admin viewer can always see them, but a user may be hidden to others when they are
|
|
// blocking, disabled or banned.
|
|
//
|
|
// The user should always be given a Not Found page so they can't tell the user even
|
|
// exists. The returned error will include a specific reason, for debugging purposes.
|
|
func (u *User) CanBeSeenBy(viewer *User) error {
|
|
if viewer.IsAdmin {
|
|
return nil
|
|
}
|
|
|
|
// Banned or disabled? Only admin can view then.
|
|
if u.Status != UserStatusActive {
|
|
return fmt.Errorf("user status is %s", u.Status)
|
|
}
|
|
|
|
// Is either one blocking?
|
|
if IsBlocking(viewer.ID, u.ID) && !viewer.IsAdmin {
|
|
return fmt.Errorf("users block each other")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UserSearch config.
|
|
type UserSearch struct {
|
|
Username string
|
|
Gender string
|
|
Orientation string
|
|
MaritalStatus string
|
|
HereFor string
|
|
Certified bool
|
|
NotCertified bool
|
|
InnerCircle bool
|
|
ShyAccounts bool
|
|
IsBanned bool
|
|
IsDisabled bool
|
|
IsAdmin bool // search for admin users
|
|
Friends bool
|
|
AgeMin int
|
|
AgeMax int
|
|
}
|
|
|
|
// SearchUsers from the perspective of a given user.
|
|
func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, error) {
|
|
if search == nil {
|
|
search = &UserSearch{}
|
|
}
|
|
|
|
var (
|
|
users = []*User{}
|
|
query *gorm.DB
|
|
joins string // GPS location join.
|
|
wheres = []string{}
|
|
placeholders = []interface{}{}
|
|
blockedUserIDs = BlockedUserIDs(user)
|
|
myLocation = GetUserLocation(user.ID)
|
|
)
|
|
|
|
// Sort by distance? Requires PostgreSQL.
|
|
if pager.Sort == "distance" {
|
|
if !config.Current.Database.IsPostgres {
|
|
return users, errors.New("ordering by distance requires PostgreSQL with the PostGIS extension")
|
|
}
|
|
|
|
// If the current user doesn't have their location on file, they can't do this.
|
|
if myLocation.Source == LocationSourceNone || (myLocation.Latitude == 0 && myLocation.Longitude == 0) {
|
|
return users, errors.New("can not sort members by distance because your location is not known")
|
|
}
|
|
|
|
// Only query for users who have locations.
|
|
joins = "JOIN user_locations ON (user_locations.user_id = users.id)"
|
|
wheres = append(wheres,
|
|
"user_locations.latitude IS NOT NULL",
|
|
"user_locations.longitude IS NOT NULL",
|
|
"user_locations.latitude <> 0",
|
|
"user_locations.longitude <> 0",
|
|
)
|
|
|
|
pager.Sort = fmt.Sprintf(`ST_Distance(
|
|
ST_MakePoint(user_locations.longitude, user_locations.latitude)::geography,
|
|
ST_MakePoint(%f, %f)::geography)`,
|
|
myLocation.Longitude, myLocation.Latitude,
|
|
)
|
|
}
|
|
|
|
if len(blockedUserIDs) > 0 {
|
|
wheres = append(wheres, "id NOT IN ?")
|
|
placeholders = append(placeholders, blockedUserIDs)
|
|
}
|
|
|
|
if search.Username != "" {
|
|
ilike := "%" + strings.TrimSpace(strings.ToLower(search.Username)) + "%"
|
|
wheres = append(wheres, "(username LIKE ? OR name ILIKE ?)")
|
|
placeholders = append(placeholders, ilike, ilike)
|
|
}
|
|
|
|
if search.Gender != "" {
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1 FROM profile_fields
|
|
WHERE user_id = users.id AND name = ? AND value = ?
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, "gender", search.Gender)
|
|
}
|
|
|
|
if search.Orientation != "" {
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1 FROM profile_fields
|
|
WHERE user_id = users.id AND name = ? AND value = ?
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, "orientation", search.Orientation)
|
|
}
|
|
|
|
if search.MaritalStatus != "" {
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1 FROM profile_fields
|
|
WHERE user_id = users.id AND name = ? AND value = ?
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, "marital_status", search.MaritalStatus)
|
|
}
|
|
|
|
if search.HereFor != "" {
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1 FROM profile_fields
|
|
WHERE user_id = users.id AND name = ? AND value LIKE ?
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, "here_for", "%"+search.HereFor+"%")
|
|
}
|
|
|
|
// Only admin user can show disabled/banned users.
|
|
var statuses = []string{}
|
|
if user.HasAdminScope(config.ScopeUserBan) {
|
|
if search.IsBanned {
|
|
statuses = append(statuses, UserStatusBanned)
|
|
}
|
|
if search.IsDisabled {
|
|
statuses = append(statuses, UserStatusDisabled)
|
|
}
|
|
}
|
|
|
|
// Non-admin user only ever sees active accounts.
|
|
if user.IsAdmin && len(statuses) > 0 {
|
|
wheres = append(wheres, "status IN ?")
|
|
placeholders = append(placeholders, statuses)
|
|
} else {
|
|
wheres = append(wheres, "status = ?")
|
|
placeholders = append(placeholders, UserStatusActive)
|
|
}
|
|
|
|
// Certified filter (including if Shy Accounts are asked for)
|
|
if search.Certified {
|
|
wheres = append(wheres, "certified = ?")
|
|
placeholders = append(placeholders, search.Certified)
|
|
}
|
|
|
|
// Expressly Not Certified filtering
|
|
if search.NotCertified {
|
|
wheres = append(wheres, "certified = ?")
|
|
placeholders = append(placeholders, false)
|
|
}
|
|
|
|
if search.IsAdmin {
|
|
wheres = append(wheres, "is_admin = true")
|
|
}
|
|
|
|
if search.InnerCircle {
|
|
wheres = append(wheres, "inner_circle = ? OR is_admin = ?")
|
|
placeholders = append(placeholders, true, true)
|
|
}
|
|
|
|
if search.ShyAccounts {
|
|
a, b := WhereClauseShyAccounts()
|
|
wheres = append(wheres, a)
|
|
placeholders = append(placeholders, b...)
|
|
}
|
|
|
|
if search.Friends {
|
|
wheres = append(wheres, `
|
|
EXISTS (
|
|
SELECT 1 FROM friends
|
|
WHERE source_user_id = ?
|
|
AND target_user_id = users.id
|
|
AND approved = ?
|
|
)
|
|
`)
|
|
placeholders = append(placeholders,
|
|
user.ID,
|
|
true,
|
|
)
|
|
}
|
|
|
|
if search.AgeMin > 0 {
|
|
date := time.Now().AddDate(-search.AgeMin, 0, 0)
|
|
wheres = append(wheres, `
|
|
birthdate <= ? AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM profile_fields
|
|
WHERE user_id = users.id
|
|
AND name = 'hide_age'
|
|
AND value = 'true'
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, date)
|
|
}
|
|
|
|
if search.AgeMax > 0 {
|
|
date := time.Now().AddDate(-search.AgeMax-1, 0, 0)
|
|
wheres = append(wheres, `
|
|
birthdate >= ? AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM profile_fields
|
|
WHERE user_id = users.id
|
|
AND name = 'hide_age'
|
|
AND value = 'true'
|
|
)
|
|
`)
|
|
placeholders = append(placeholders, date)
|
|
}
|
|
|
|
query = (&User{}).Preload()
|
|
if joins != "" {
|
|
query = query.Joins(joins)
|
|
}
|
|
query = query.Where(
|
|
strings.Join(wheres, " AND "),
|
|
placeholders...,
|
|
).Order(pager.Sort)
|
|
query.Model(&User{}).Count(&pager.Total)
|
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&users)
|
|
|
|
// Inject relationship booleans.
|
|
SetUserRelationships(user, users)
|
|
|
|
return users, result.Error
|
|
}
|
|
|
|
// UserMap helps map a set of users to look up by ID.
|
|
type UserMap map[uint64]*User
|
|
|
|
// MapUsers looks up a set of user IDs in bulk and returns a UserMap suitable for templates.
|
|
// Useful to avoid circular reference issues with Photos especially; the Site Gallery queries
|
|
// photos of ALL users and MapUsers helps stitch them together for the frontend.
|
|
func MapUsers(user *User, userIDs []uint64) (UserMap, error) {
|
|
var (
|
|
usermap = UserMap{}
|
|
set = map[uint64]interface{}{}
|
|
distinct = []uint64{}
|
|
)
|
|
|
|
// Uniqueify users.
|
|
for _, uid := range userIDs {
|
|
if _, ok := set[uid]; ok {
|
|
continue
|
|
}
|
|
set[uid] = nil
|
|
distinct = append(distinct, uid)
|
|
}
|
|
|
|
var (
|
|
users = []*User{}
|
|
result = (&User{}).Preload().Where("id IN ?", distinct).Find(&users)
|
|
)
|
|
|
|
// Inject user relationships.
|
|
if user != nil {
|
|
SetUserRelationships(user, users)
|
|
}
|
|
|
|
if result.Error == nil {
|
|
for _, row := range users {
|
|
usermap[row.ID] = row
|
|
}
|
|
}
|
|
|
|
return usermap, result.Error
|
|
}
|
|
|
|
// MapAdminUsers returns a MapUsers result for all admin user accounts on the site.
|
|
func MapAdminUsers(user *User) (UserMap, error) {
|
|
adminUsers, err := ListAdminUsers()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userIDs = []uint64{}
|
|
for _, user := range adminUsers {
|
|
userIDs = append(userIDs, user.ID)
|
|
}
|
|
return MapUsers(user, userIDs)
|
|
}
|
|
|
|
// Has a user ID in the map?
|
|
func (um UserMap) Has(id uint64) bool {
|
|
_, ok := um[id]
|
|
return ok
|
|
}
|
|
|
|
// Get a user from the UserMap.
|
|
func (um UserMap) Get(id uint64) *User {
|
|
if user, ok := um[id]; ok {
|
|
return user
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MapUsersByUsername looks up a set of users in bulk and returns a UsernameMap suitable for templates.
|
|
//
|
|
// It is like MapUsers but by username instead of ID.
|
|
func MapUsersByUsername(usernames []string) (UsernameMap, error) {
|
|
var (
|
|
usermap = UsernameMap{}
|
|
set = map[string]interface{}{}
|
|
distinct = []string{}
|
|
)
|
|
|
|
// Uniqueify users.
|
|
for _, uid := range usernames {
|
|
if _, ok := set[uid]; ok {
|
|
continue
|
|
}
|
|
set[uid] = nil
|
|
distinct = append(distinct, uid)
|
|
}
|
|
|
|
var (
|
|
users = []*User{}
|
|
result = (&User{}).Preload().Where("username IN ?", distinct).Find(&users)
|
|
)
|
|
|
|
if result.Error == nil {
|
|
for _, row := range users {
|
|
usermap[row.Username] = row
|
|
}
|
|
}
|
|
|
|
// Assert we got the expected count.
|
|
if len(usermap) != len(distinct) {
|
|
return usermap, fmt.Errorf("didn't get all expected users (expected %d, got %d)", len(distinct), len(usermap))
|
|
}
|
|
|
|
return usermap, result.Error
|
|
}
|
|
|
|
// UsernameMap helps map a set of users to look up by ID.
|
|
type UsernameMap map[string]*User
|
|
|
|
// NameOrUsername returns the name (if not null or empty) or the username.
|
|
func (u *User) NameOrUsername() string {
|
|
if u.Name != nil && len(*u.Name) > 0 {
|
|
return *u.Name
|
|
} else {
|
|
return u.Username
|
|
}
|
|
}
|
|
|
|
// VisibleAvatarURL returns a URL to the user's avatar taking into account
|
|
// their relationship with the current user. For example, if the avatar is
|
|
// friends-only and the current user can't see it, returns the path to the
|
|
// yellow placeholder avatar instead.
|
|
//
|
|
// Expects that UserRelationships are available on the user.
|
|
func (u *User) VisibleAvatarURL(currentUser *User) string {
|
|
canSee, visibility := u.CanSeeProfilePicture(currentUser)
|
|
if canSee {
|
|
return config.PhotoWebPath + "/" + u.ProfilePhoto.CroppedFilename
|
|
}
|
|
|
|
switch visibility {
|
|
case PhotoPrivate:
|
|
return "/static/img/shy-private.png"
|
|
case PhotoInnerCircle:
|
|
return "/static/img/shy-secret.png"
|
|
case PhotoFriends:
|
|
return "/static/img/shy-friends.png"
|
|
}
|
|
|
|
return "/static/img/shy.png"
|
|
}
|
|
|
|
// CanSeeProfilePicture returns whether the current user can see the user's profile picture.
|
|
//
|
|
// Returns a boolean (false if currentUser can't see) and the Visibility setting of the profile photo.
|
|
//
|
|
// If the user has no profile photo, returns (false, PhotoPublic) which should manifest as the blue shy.png placeholder image.
|
|
func (u *User) CanSeeProfilePicture(currentUser *User) (bool, PhotoVisibility) {
|
|
if !u.UserRelationship.Computed && currentUser != nil {
|
|
SetUserRelationships(currentUser, []*User{u})
|
|
}
|
|
|
|
visibility := u.ProfilePhoto.Visibility
|
|
if visibility == PhotoPrivate && !u.UserRelationship.IsPrivateGranted {
|
|
// Private photo
|
|
return false, visibility
|
|
} else if visibility == PhotoFriends && !u.UserRelationship.IsFriend {
|
|
// Friends only
|
|
return false, visibility
|
|
} else if visibility == PhotoInnerCircle && currentUser != nil && !currentUser.IsInnerCircle() {
|
|
// Inner circle only
|
|
return false, visibility
|
|
} else if u.ProfilePhoto.CroppedFilename != "" {
|
|
// Happy path
|
|
return true, visibility
|
|
}
|
|
return false, PhotoPublic
|
|
}
|
|
|
|
// HashPassword sets the user's hashed (bcrypt) password.
|
|
func (u *User) HashPassword(password string) error {
|
|
passwd, err := bcrypt.GenerateFromPassword([]byte(password), config.BcryptCost)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.HashedPassword = string(passwd)
|
|
return nil
|
|
}
|
|
|
|
// CheckPassword verifies the password is correct. Returns nil on success.
|
|
func (u *User) CheckPassword(password string) error {
|
|
return bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(password))
|
|
}
|
|
|
|
// SetProfileField sets or creates a named profile field.
|
|
func (u *User) SetProfileField(name, value string) {
|
|
// Check if it exists.
|
|
log.Debug("User(%s).SetProfileField(%s, %s)", u.Username, name, value)
|
|
var exists bool
|
|
for _, field := range u.ProfileField {
|
|
log.Debug("\tCheck existing field %s", field.Name)
|
|
if field.Name == name {
|
|
log.Debug("\tFound existing field!")
|
|
changed := field.Value != value
|
|
field.Value = value
|
|
exists = true
|
|
|
|
// Save it now. TODO: otherwise gorm doesn't know we changed
|
|
// it and it won't be inserted when the User is saved. But
|
|
// this is probably not performant to do!
|
|
if changed {
|
|
DB.Save(&field)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if exists {
|
|
return
|
|
}
|
|
|
|
log.Debug("User(%s): append ProfileField %s", u.Username, name)
|
|
u.ProfileField = append(u.ProfileField, ProfileField{
|
|
Name: name,
|
|
Value: value,
|
|
})
|
|
if err := u.Save(); err != nil {
|
|
log.Error("User(%s).SetProfileField(%s): error saving after append new field: %s", u.Username, name, err)
|
|
}
|
|
}
|
|
|
|
// GetProfileField returns the value of a profile field or blank string.
|
|
func (u *User) GetProfileField(name string) string {
|
|
for _, field := range u.ProfileField {
|
|
if field.Name == name {
|
|
return field.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GetProfileFieldOr returns a default string (like "n/a") if the profile field is not set.
|
|
func (u *User) GetProfileFieldOr(name, or string) string {
|
|
if value := u.GetProfileField(name); value != "" {
|
|
return value
|
|
}
|
|
return or
|
|
}
|
|
|
|
// GetDisplayAge returns the user's age dependent on their hide-my-age setting.
|
|
func (u *User) GetDisplayAge() string {
|
|
if !u.Birthdate.IsZero() && u.GetProfileField("hide_age") != "true" {
|
|
return fmt.Sprintf("%dyo", utility.Age(u.Birthdate))
|
|
}
|
|
|
|
return "n/a"
|
|
}
|
|
|
|
// IsBirthday returns whether it is currently the user's birthday (+- a day for time zones).
|
|
func (u *User) IsBirthday() bool {
|
|
if u.Birthdate.IsZero() {
|
|
return false
|
|
}
|
|
|
|
// Window of time to be valid.
|
|
var (
|
|
now, _ = time.Parse(time.DateOnly, time.Now().Format(time.DateOnly))
|
|
bday, _ = time.Parse(time.DateOnly, fmt.Sprintf("%d-%d-%d", now.Year(), u.Birthdate.Month(), u.Birthdate.Day()))
|
|
startDate = now.Add(-6 * time.Hour)
|
|
endDate = now.Add(60 * time.Hour)
|
|
)
|
|
|
|
return bday.Before(endDate) && bday.After(startDate)
|
|
}
|
|
|
|
// ProfileFieldIn checks if a substring is IN a profile field. Currently
|
|
// does a naive strings.Contains(), intended for the "here_for" field.
|
|
func (u *User) ProfileFieldIn(field, substr string) bool {
|
|
value := u.GetProfileField(field)
|
|
return strings.Contains(value, substr)
|
|
}
|
|
|
|
// RemoveProfilePhoto sets profile_photo_id=null to unset the foreign key.
|
|
func (u *User) RemoveProfilePhoto() error {
|
|
result := DB.Model(&User{}).Where("id = ?", u.ID).Update("profile_photo_id", nil)
|
|
return result.Error
|
|
}
|
|
|
|
// Save user.
|
|
func (u *User) Save() error {
|
|
result := DB.Save(u)
|
|
return result.Error
|
|
}
|
|
|
|
// Delete a user. NOTE: use the models/deletion/DeleteUser() function
|
|
// instead of this to do a deep scrub of all related data!
|
|
func (u *User) Delete() error {
|
|
return DB.Delete(u).Error
|
|
}
|
|
|
|
// Print user object as pretty JSON.
|
|
func (u *User) Print() string {
|
|
var (
|
|
buf bytes.Buffer
|
|
enc = json.NewEncoder(&buf)
|
|
)
|
|
enc.SetIndent("", " ")
|
|
enc.Encode(u)
|
|
return buf.String()
|
|
}
|