package models import ( "errors" "strings" "time" "code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/log" "gorm.io/gorm" ) // Photo table. type Photo struct { ID uint64 `gorm:"primaryKey"` UserID uint64 `gorm:"index"` Filename string CroppedFilename string // if cropped, e.g. for profile photo Filesize int64 Caption string Flagged bool // photo has been reported by the community Visibility PhotoVisibility Gallery bool // photo appears in the public gallery (if public) Explicit bool // is an explicit photo CreatedAt time.Time UpdatedAt time.Time } // PhotoVisibility settings. type PhotoVisibility string const ( PhotoPublic PhotoVisibility = "public" // on profile page and/or public gallery PhotoFriends PhotoVisibility = "friends" // only friends can see it PhotoPrivate PhotoVisibility = "private" // private PhotoInnerCircle PhotoVisibility = "circle" // inner circle // Special visibility in case, on User Gallery view, user applies a filter // for friends-only picture but they are not friends with the user. PhotoNotAvailable PhotoVisibility = "not_available" ) // PhotoVisibility preset settings. var ( PhotoVisibilityAll = []PhotoVisibility{ PhotoPublic, PhotoFriends, PhotoPrivate, } // "All" but also for Inner Circle members. PhotoVisibilityCircle = []PhotoVisibility{ PhotoPublic, PhotoFriends, PhotoPrivate, PhotoInnerCircle, } // Site Gallery visibility for when your friends show up in the gallery. // Or: "Friends + Gallery" photos can appear to your friends in the Site Gallery. PhotoVisibilityFriends = []string{ string(PhotoPublic), string(PhotoFriends), } ) // CreatePhoto with most of the settings you want (not ID or timestamps) in the database. func CreatePhoto(tmpl Photo) (*Photo, error) { if tmpl.UserID == 0 { return nil, errors.New("UserID required") } p := &Photo{ UserID: tmpl.UserID, Filename: tmpl.Filename, CroppedFilename: tmpl.CroppedFilename, Filesize: tmpl.Filesize, Caption: tmpl.Caption, Visibility: tmpl.Visibility, Gallery: tmpl.Gallery, Explicit: tmpl.Explicit, } result := DB.Create(p) return p, result.Error } // GetPhoto by ID. func GetPhoto(id uint64) (*Photo, error) { p := &Photo{} result := DB.First(&p, id) return p, result.Error } // GetPhotos by an array of IDs, mapped to their IDs. func GetPhotos(IDs []uint64) (map[uint64]*Photo, error) { var ( mp = map[uint64]*Photo{} ps = []*Photo{} ) result := DB.Model(&Photo{}).Where("id IN ?", IDs).Find(&ps) for _, row := range ps { mp[row.ID] = row } return mp, result.Error } // UserGallery configuration for filtering gallery pages. type UserGallery struct { Explicit string // "", "true", "false" Visibility []PhotoVisibility } /* PaginateUserPhotos gets a page of photos belonging to a user ID. */ func PaginateUserPhotos(userID uint64, conf UserGallery, pager *Pagination) ([]*Photo, error) { var ( p = []*Photo{} wheres = []string{} placeholders = []interface{}{} ) var explicit = []bool{} switch conf.Explicit { case "true": explicit = []bool{true} case "false": explicit = []bool{false} } wheres = append(wheres, "user_id = ? AND visibility IN ?") placeholders = append(placeholders, userID, conf.Visibility) if len(explicit) > 0 { wheres = append(wheres, "explicit = ?") placeholders = append(placeholders, explicit[0]) } query := DB.Where( strings.Join(wheres, " AND "), placeholders..., ).Order( pager.Sort, ) // Get the total count. query.Model(&Photo{}).Count(&pager.Total) result := query.Offset( pager.GetOffset(), ).Limit(pager.PerPage).Find(&p) return p, result.Error } // CountPhotos returns the total number of photos on a user's account. func CountPhotos(userID uint64) int64 { var count int64 result := DB.Where( "user_id = ?", userID, ).Model(&Photo{}).Count(&count) if result.Error != nil { log.Error("CountPhotos(%d): %s", userID, result.Error) } return count } // CountPhotosICanSee returns the number of photos on an account which can be seen by the given viewer. func CountPhotosICanSee(user *User, viewer *User) int64 { // Visibility filters to query by. var visibilities = []PhotoVisibility{ PhotoPublic, } // Is the viewer in the inner circle? if viewer.IsInnerCircle() { visibilities = append(visibilities, PhotoInnerCircle) } // Is the viewer friends with the target? if AreFriends(user.ID, viewer.ID) { visibilities = append(visibilities, PhotoFriends) } // Is the viewer granted private access? if IsPrivateUnlocked(user.ID, viewer.ID) { visibilities = append(visibilities, PhotoPrivate) } // Get the photo count now. var count int64 result := DB.Where( "user_id = ? AND visibility IN ?", user.ID, visibilities, ).Model(&Photo{}).Count(&count) if result.Error != nil { log.Error("CountPhotosICanSee(%d, %d): %s", user.ID, viewer.ID, result.Error) } return count } // MapPhotoCounts returns a mapping of user ID to the CountPhotos()-equivalent result for each. // It's used on the member directory to show photo counts on each user card. func MapPhotoCounts(users []*User) PhotoCountMap { var ( userIDs = []uint64{} result = PhotoCountMap{} ) for _, user := range users { userIDs = append(userIDs, user.ID) } type group struct { UserID uint64 PhotoCount int64 } var groups = []group{} if res := DB.Table( "photos", ).Select( "user_id, count(id) AS photo_count", ).Where( "user_id IN ? AND visibility = ?", userIDs, PhotoPublic, ).Group("user_id").Scan(&groups); res.Error != nil { log.Error("CountPhotosForUsers: %s", res.Error) } // Map the results in. for _, row := range groups { result[row.UserID] = row.PhotoCount } return result } // MapPhotoCounts returns a mapping of user ID to the CountPhotosICanSee()-equivalent result for each. // It's used on the member directory to show photo counts on each user card. /* TODO: under construction.. func MapPhotoCounts(users []*User, viewer *User) PhotoCountMap { var ( userIDs = []uint64{} result = PhotoCountMap{} wheres = []string{} placeholders = []interface{}{} // User ID filters for the viewer's context. myFriendIDs = FriendIDs(viewer.ID) myPrivateGrantedIDs = PrivateGrantedUserIDs(viewer.ID) isInnerCircle = viewer.IsInnerCircle() ) // Define "all photos visibilities" var ( photosPublic = []PhotoVisibility{ PhotoPublic, } photosFriends = []PhotoVisibility{ PhotoPublic, PhotoFriends, } photosPrivate = []PhotoVisibility{ PhotoPublic, PhotoPrivate, } ) if isInnerCircle { photosPublic = append(photosPublic, PhotoInnerCircle) photosFriends = append(photosFriends, PhotoInnerCircle) } // Flatten the userIDs of all passed in users. for _, user := range users { userIDs = append(userIDs, user.ID) } // Build the where clause. wheres = append(wheres, "user_id IN ?") placeholders = append(placeholders, userIDs) log.Error("FRIEND IDS: %+v", myFriendIDs) // Filter by which photos are visible to us. wheres = append(wheres, "((user_id IN ? AND visibility IN ?) OR "+ "(user_id IN ? AND visibility IN ?) OR "+ "(user_id NOT IN ? AND visibility IN ?))", ) placeholders = append(placeholders, myFriendIDs, photosFriends, myPrivateGrantedIDs, photosPrivate, myFriendIDs, photosPublic, ) type group struct { UserID uint64 PhotoCount int64 } var groups = []group{} if res := DB.Table( "photos", ).Select( "user_id, count(id) AS photo_count", ).Where( strings.Join(wheres, " AND "), placeholders..., ).Group("user_id").Scan(&groups); res.Error != nil { log.Error("CountPhotosForUsers: %s", res.Error) } // Map the results in. for _, row := range groups { result[row.UserID] = row.PhotoCount } return result } */ type PhotoCountMap map[uint64]int64 // Get a photo count for the given user ID from the map. func (pc PhotoCountMap) Get(id uint64) int64 { if value, ok := pc[id]; ok { return value } return 0 } // CountExplicitPhotos returns the number of explicit photos a user has (so non-explicit viewers can see some do exist) func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, error) { query := DB.Where( "user_id = ? AND visibility IN ? AND explicit = ?", userID, visibility, true, ) var count int64 result := query.Model(&Photo{}).Count(&count) return count, result.Error } // CountPublicPhotos returns the number of public photos on a user's page (to control whether the "invite to circle" prompt can appear). func CountPublicPhotos(userID uint64) int64 { query := DB.Where( "user_id = ? AND visibility = ?", userID, PhotoPublic, ) var count int64 result := query.Model(&Photo{}).Count(&count) if result.Error != nil { log.Error("CountPublicPhotos(%d): %s", userID, result.Error) } return count } // DistinctPhotoTypes returns types of photos the user has: a set of public, friends, or private. // // The result is cached on the User the first time it's queried. func (u *User) DistinctPhotoTypes() (result map[PhotoVisibility]struct{}) { if u.cachePhotoTypes != nil { return u.cachePhotoTypes } result = map[PhotoVisibility]struct{}{} var results = []*Photo{} query := DB.Model(&Photo{}). Select("DISTINCT photos.visibility"). Where("user_id = ?", u.ID). Group("photos.visibility"). Find(&results) if query.Error != nil { log.Error("User.DistinctPhotoTypes(%s): %s", u.Username, query.Error) return } for _, row := range results { log.Warn("DistinctPhotoTypes(%s): got %+v", u.Username, row) result[row.Visibility] = struct{}{} } u.cachePhotoTypes = result return } // Gallery config for the main Gallery paginator. type Gallery struct { Explicit string // Explicit filter Visibility string // Visibility filter AdminView bool // Show all images ShyView bool // Current user is shy (self/friends only) } /* PaginateGalleryPhotos gets a page of all public user photos for the site gallery. Admin view returns ALL photos regardless of Gallery status. */ func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Photo, error) { var ( filterExplicit = conf.Explicit filterVisibility = conf.Visibility adminView = conf.AdminView isShy = conf.ShyView p = []*Photo{} query *gorm.DB // Get the user ID and their preferences. userID = user.ID explicitOK = user.Explicit // User opted-in for explicit content blocklist = BlockedUserIDs(user) friendIDs = FriendIDs(userID) privateUserIDs = PrivateGrantedUserIDs(userID) wheres = []string{} placeholders = []interface{}{} ) // Define "all photos visibilities" var ( photosAll = PhotoVisibilityAll photosPublic = []PhotoVisibility{ PhotoPublic, } photosFriends = []PhotoVisibility{ PhotoPublic, PhotoFriends, } ) if user.IsInnerCircle() { photosAll = PhotoVisibilityCircle photosPublic = append(photosPublic, PhotoInnerCircle) photosFriends = append(photosFriends, PhotoInnerCircle) } // Admins see everything on the site (only an admin user can get an admin view). adminView = user.HasAdminScope(config.ScopePhotoModerator) && adminView // Include ourself in our friend IDs. friendIDs = append(friendIDs, userID) // Whose photos can you see on the Site Gallery? if isShy { // Shy users can only see their Friends photos (public or friends visibility) // and any Private photos to whom they were granted access. wheres = append(wheres, "((user_id IN ? AND visibility IN ?) OR "+ "(user_id IN ? AND visibility IN ?))", ) placeholders = append(placeholders, friendIDs, PhotoVisibilityFriends, privateUserIDs, photosAll, ) } else { // You can see friends' Friend photos but only public for non-friends. wheres = append(wheres, "((user_id IN ? AND visibility IN ?) OR "+ "(user_id IN ? AND visibility IN ?) OR "+ "(user_id NOT IN ? AND visibility IN ?))", ) placeholders = append(placeholders, friendIDs, photosFriends, privateUserIDs, photosAll, friendIDs, photosPublic, ) } // Gallery photos only. wheres = append(wheres, "gallery = ?") placeholders = append(placeholders, true) // Filter blocked users. if len(blocklist) > 0 { wheres = append(wheres, "user_id NOT IN ?") placeholders = append(placeholders, blocklist) } // Non-explicit pics unless the user opted in. Allow explicit filter setting to override. if filterExplicit != "" { wheres = append(wheres, "explicit = ?") placeholders = append(placeholders, filterExplicit == "true") } else if !explicitOK { wheres = append(wheres, "explicit = ?") placeholders = append(placeholders, false) } // Is the user furthermore clamping the visibility filter? if filterVisibility != "" { wheres = append(wheres, "visibility = ?") placeholders = append(placeholders, filterVisibility) } // Only certified (and not banned) user photos. wheres = append(wheres, "EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true AND status='active')", ) // Exclude private users' photos. wheres = append(wheres, "NOT EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND visibility = 'private')", ) // Admin view: get ALL PHOTOS on the site, period. if adminView { query = DB // Admin may filter too. if filterVisibility != "" { query = query.Where("visibility = ?", filterVisibility) } if filterExplicit != "" { query = query.Where("explicit = ?", filterExplicit == "true") } } else { query = DB.Where( strings.Join(wheres, " AND "), placeholders..., ) } query = query.Order(pager.Sort) query.Model(&Photo{}).Count(&pager.Total) result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&p) return p, result.Error } // Save photo. func (p *Photo) Save() error { result := DB.Save(p) return result.Error } // Delete photo. func (p *Photo) Delete() error { result := DB.Delete(p) return result.Error }