diff --git a/pkg/controller/account/dashboard.go b/pkg/controller/account/dashboard.go index c3eefd4..1443036 100644 --- a/pkg/controller/account/dashboard.go +++ b/pkg/controller/account/dashboard.go @@ -43,10 +43,21 @@ func Dashboard() http.HandlerFunc { notifMap := models.MapNotifications(notifs) models.SetUserRelationshipsInNotifications(currentUser, notifs) + // Restricted profile warnings. + var ( + isShyUser = currentUser.IsShy() + photoTypes = currentUser.DistinctPhotoTypes() + _, hasPublic = photoTypes[models.PhotoPublic] + ) + var vars = map[string]interface{}{ "Notifications": notifs, "NotifMap": notifMap, "Pager": pager, + + // Show a warning to 'restricted' profiles who are especially private. + "IsShyUser": isShyUser, + "HasPublicPhoto": hasPublic, } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go index 3756c73..d7b4135 100644 --- a/pkg/controller/chat/chat.go +++ b/pkg/controller/chat/chat.go @@ -39,8 +39,21 @@ func Landing() http.HandlerFunc { } // Are they logging into the chat room? - var intent = r.FormValue("intent") + var ( + intent = r.FormValue("intent") + isShy = currentUser.IsShy() + ) if intent == "join" { + // If we are shy, block chat for now. + if isShy { + session.FlashError(w, r, + "You have a Shy Account and are not allowed in the chat room at this time where our non-shy members may "+ + "be on camera.", + ) + templates.Redirect(w, "/chat") + return + } + // Get our Chat JWT secret. var ( secret = []byte(config.Current.BareRTC.JWTSecret) @@ -91,7 +104,8 @@ func Landing() http.HandlerFunc { } var vars = map[string]interface{}{ - "ChatAPI": strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics", + "ChatAPI": strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/statistics", + "IsShyUser": isShy, } if err := tmpl.Execute(w, r, vars); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/controller/inbox/compose.go b/pkg/controller/inbox/compose.go index 7419943..08babcc 100644 --- a/pkg/controller/inbox/compose.go +++ b/pkg/controller/inbox/compose.go @@ -69,6 +69,18 @@ func Compose() http.HandlerFunc { return } + // On GET request (come from a user profile page): + // Do not allow a shy user to initiate DMs with a non-shy one. + var ( + imShy = currentUser.IsShy() + isShyFrom = currentUser.IsShyFrom(user) || (imShy && !models.AreFriends(currentUser.ID, user.ID)) + ) + if imShy && isShyFrom { + session.FlashError(w, r, "You have a Shy Account and can not initiate Direct Messages with a non-shy member.") + templates.Redirect(w, "/u/"+user.Username) + return + } + var vars = map[string]interface{}{ "User": user, } diff --git a/pkg/controller/photo/site_gallery.go b/pkg/controller/photo/site_gallery.go index 45dd8a2..9758233 100644 --- a/pkg/controller/photo/site_gallery.go +++ b/pkg/controller/photo/site_gallery.go @@ -54,6 +54,9 @@ func SiteGallery() http.HandlerFunc { session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser") } + // Is the current viewer shy? + var isShy = currentUser.IsShy() + // Get the page of photos. pager := &models.Pagination{ Page: 1, @@ -61,7 +64,12 @@ func SiteGallery() http.HandlerFunc { Sort: sort, } pager.ParsePage(r) - photos, err := models.PaginateGalleryPhotos(currentUser, filterExplicit, filterVisibility, adminView, pager) + photos, _ := models.PaginateGalleryPhotos(currentUser, models.Gallery{ + Explicit: filterExplicit, + Visibility: filterVisibility, + AdminView: adminView, + ShyView: isShy, + }, pager) // Bulk load the users associated with these photos. var userIDs = []uint64{} @@ -95,6 +103,9 @@ func SiteGallery() http.HandlerFunc { "FilterExplicit": filterExplicit, "FilterVisibility": filterVisibility, "AdminView": adminView, + + // Is the current user shy? + "IsShyUser": isShy, } if err := tmpl.Execute(w, r, vars); err != nil { diff --git a/pkg/controller/photo/user_gallery.go b/pkg/controller/photo/user_gallery.go index ae0157d..d87d039 100644 --- a/pkg/controller/photo/user_gallery.go +++ b/pkg/controller/photo/user_gallery.go @@ -43,7 +43,31 @@ func UserPhotos() http.HandlerFunc { if err != nil { session.FlashError(w, r, "Unexpected error: couldn't get CurrentUser") } - var isOwnPhotos = currentUser.ID == user.ID + var ( + isOwnPhotos = currentUser.ID == user.ID + isShy = currentUser.IsShy() + isShyFrom = currentUser.IsShyFrom(user) || (isShy && !models.AreFriends(currentUser.ID, user.ID)) + ) + + // Bail early if we are shy from this user. + if isShy && isShyFrom { + var vars = map[string]interface{}{ + "IsOwnPhotos": currentUser.ID == user.ID, + "IsShyUser": isShy, + "IsShyFrom": isShyFrom, + // "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? + // "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. + "User": user, + "Photos": []*models.Photo{}, + "PhotoCount": models.CountPhotos(user.ID), + "Pager": models.Pagination{}, + } + + if err := tmpl.Execute(w, r, vars); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return + } // Is either one blocking? if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin { @@ -107,6 +131,8 @@ func UserPhotos() http.HandlerFunc { var vars = map[string]interface{}{ "IsOwnPhotos": currentUser.ID == user.ID, + "IsShyUser": isShy, + "IsShyFrom": isShyFrom, "IsMyPrivateUnlockedFor": isGranted, // have WE granted THIS USER to see our private pics? "AreWeGrantedPrivate": isGrantee, // have THEY granted US private photo access. "User": user, diff --git a/pkg/models/pagination.go b/pkg/models/pagination.go index 2d2571d..9b67adf 100644 --- a/pkg/models/pagination.go +++ b/pkg/models/pagination.go @@ -85,6 +85,9 @@ func (p *Pagination) Iter() []Page { } func (p *Pagination) Pages() int { + if p.PerPage == 0 { + return 0 + } return int(math.Ceil(float64(p.Total) / float64(p.PerPage))) } diff --git a/pkg/models/photo.go b/pkg/models/photo.go index e0dd5ae..8322710 100644 --- a/pkg/models/photo.go +++ b/pkg/models/photo.go @@ -150,15 +150,57 @@ func CountExplicitPhotos(userID uint64, visibility []PhotoVisibility) (int64, er return count, result.Error } +// 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, filterExplicit, filterVisibility string, adminView bool, pager *Pagination) ([]*Photo, error) { +func PaginateGalleryPhotos(user *User, conf Gallery, pager *Pagination) ([]*Photo, error) { var ( - p = []*Photo{} - query *gorm.DB + 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 @@ -177,17 +219,31 @@ func PaginateGalleryPhotos(user *User, filterExplicit, filterVisibility string, // Include ourself in our friend IDs. friendIDs = append(friendIDs, userID) - // 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 = ?))", - ) - placeholders = append(placeholders, - friendIDs, PhotoVisibilityFriends, - privateUserIDs, PhotoVisibilityAll, - friendIDs, PhotoPublic, - ) + // 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, PhotoVisibilityAll, + ) + } 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 = ?))", + ) + placeholders = append(placeholders, + friendIDs, PhotoVisibilityFriends, + privateUserIDs, PhotoVisibilityAll, + friendIDs, PhotoPublic, + ) + } // Gallery photos only. wheres = append(wheres, "gallery = ?") diff --git a/pkg/models/user.go b/pkg/models/user.go index 53dfe50..9d751ed 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -37,6 +37,9 @@ type User struct { // Current user's relationship to this user -- not stored in DB. UserRelationship UserRelationship `gorm:"-"` + + // Caches + cachePhotoTypes map[PhotoVisibility]struct{} } type UserVisibility string @@ -132,6 +135,49 @@ func FindUser(username string) (*User, error) { return u, result.Error } +// IsShy returns whether the user might have an "empty" profile from the perspective of anybody. +// +// An empty profile means their profile is Private or else ALL of their photos are non-public; so that +// somebody viewing their page might see nothing at all from them and consider them a "blank" profile. +func (u *User) IsShy() bool { + // Non-certified users are considered empty. + if !u.Certified { + return true + } + + // Private profile automatically applies. + if u.Visibility == UserVisibilityPrivate { + return true + } + + // If ALL of our photos are non-public, that counts too. + var photoTypes = u.DistinctPhotoTypes() + if _, ok := photoTypes[PhotoPublic]; !ok { + log.Info("IsEmptyProfile: true because visibilities %+v did not include public", photoTypes) + return true + } + + return false +} + +// 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 +} + // UserSearch config. type UserSearch struct { EmailOrUsername string diff --git a/web/templates/account/dashboard.html b/web/templates/account/dashboard.html index 1c1f3c8..d343e42 100644 --- a/web/templates/account/dashboard.html +++ b/web/templates/account/dashboard.html @@ -68,6 +68,71 @@ {{end}} + + {{if and .CurrentUser.Certified .IsShyUser}} +
+
+

+ + Your profile page is too private +

+
+ +
+

+ You are considered to be a Shy Account because your profile and + photos are all set to Private or Friends-only visibility, so that to other members + of {{PrettyTitle}} you appear like a blank, faceless profile. +

+ +

+ While in this restricted state, you are grouped into a cohort with other members who + are as shy as you are and have limited contact options to connect with our other, + {{PrettyTitle}} members who are sharing their nudes on public. +

+ +

+ Click here to learn more about your Shy Account. To + remedy this, please see the following steps: +

+ + +
+
+ {{end}} +
diff --git a/web/templates/chat.html b/web/templates/chat.html index ad5652b..95f1d8b 100644 --- a/web/templates/chat.html +++ b/web/templates/chat.html @@ -26,6 +26,15 @@
+ {{if .IsShyUser}} +
+ You have a Shy Account and you may not enter + the chat room at this time, where our {{PrettyTitle}} members may be sharing their cameras. You are + sharing no public photos with the community, so you get limited access to ours. + Learn more about how to resolve this issue. +
+ {{end}} +

{{PrettyTitle}} has a new chat room! Come and check it out. It features some public rooms, direct @@ -60,6 +69,10 @@ yet for Safari. Use Firefox or Chrome (or other Chromium browser of your choice) for better odds of video support.

+ +

+ Unfortunately, the chat room does not work on iPhones or iPads at this time. +

diff --git a/web/templates/faq.html b/web/templates/faq.html index f3bd7e6..565d72d 100644 --- a/web/templates/faq.html +++ b/web/templates/faq.html @@ -53,6 +53,15 @@ +

  • + Shy Account FAQs + +
  • Technical FAQs +

    Shy Account FAQs

    + +

    + One of the things that {{PrettyTitle}} wishes to avoid is the dreaded "blank profile" + that slides into our DMs and gets creepy and weird on us. You are encouraged to participate + on this site and share at least one public photo with the community. You may opt to have + only "G-rated face pics" on public and nudes on private, or keep your face on private and + share some body shots with your face cropped out on public - but share at least one good + picture on public. +

    + +

    + When your profile page or photos are all set to Private or Friends-only, you will + be considered to have a Shy Account. + A Shy Account can still interact on the forums but will have limited options to + interact with non-restricted ({{PrettyTitle}}) members. +

    + +

    What restrictions apply to Shy Accounts?

    + +

    + The limits placed on Shy Accounts are: +

    + + + +

    + The idea is to keep the shy members isolated from the non-shy ones. We nudists are sharing + what we can and we don't want creepers to be ogling our nudes and not sharing anything in + return. If all your pics are private, you look like a blank profile to us - and you will be + kept with the other blank profiles until you choose to participate. +

    + +

    What can Shy Accounts do?

    + +

    + There are still a lot things you can do with your certified + but Shy Account: +

    + + + +

    How do I fix it?

    + +

    + Leaving Shy Account territory is easy: +

    + +
      +
    1. Don't have your profile page set to private. Only logged-in, certified members can see your page, anyway!
    2. +
    3. + Have at least one public picture to share with the class. Ideally, it will be your profile picture that + shows your face, but we'll settle for a good headless body shot. We're all sharing our nudes here, we'd like it if you + participated as well. +
    4. +
    + +

    + If you are new to all of this, here are some ideas how you can manage your + photo gallery to have at least one public picture to share: +

    + + +

    Technical FAQs

    Why did you build a custom website?

    diff --git a/web/templates/photo/gallery.html b/web/templates/photo/gallery.html index 33673b9..106bd96 100644 --- a/web/templates/photo/gallery.html +++ b/web/templates/photo/gallery.html @@ -72,6 +72,7 @@ {{define "pager"}} +{{if .Pager.Total}} {{end}} +{{end}} {{define "content"}} @@ -162,16 +164,34 @@
  • + + {{if and .IsSiteGallery .IsShyUser}} +
    + You have a Shy Account so you will only see + pictures of you and your friends here. Learn more +
    + {{end}} + + + {{if .IsShyFrom}} +
    + You have a Shy Account and you are not friends + with this person so can not see their gallery. Learn more +
    + {{end}} +
    + {{if .Pager.Total}} Found {{.Pager.Total}} photo{{Pluralize64 .Pager.Total}} (page {{.Pager.Page}} of {{.Pager.Pages}}). {{if .ExplicitCount}} {{.ExplicitCount}} explicit photo{{Pluralize64 .ExplicitCount}} hidden per your settings. {{end}} + {{end}}