Shy Accounts

* Users with private profiles or no public photo at all are considered
  to be Shy Accounts and are isolated from the non-shy profiles.
* Restrictions include:
  * Site Gallery shows only them + their friends' photos.
  * User Galleries: must be a friend or had private photos granted to
    see a user's gallery page.
  * DMs: can not initiate a DM to a non-shy member (other shy members
    OK).
This commit is contained in:
Noah Petherbridge 2023-02-13 22:19:18 -08:00
parent c4600ff6ce
commit 7d17dce4d4
13 changed files with 444 additions and 18 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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,
}

View File

@ -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 {

View File

@ -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,

View File

@ -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)))
}

View File

@ -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 = ?")

View File

@ -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

View File

@ -68,6 +68,71 @@
</div>
{{end}}
<!-- "Very Private" Restricted Account warning for certified users -->
{{if and .CurrentUser.Certified .IsShyUser}}
<div class="card block">
<header class="card-header has-background-danger">
<p class="card-header-title has-text-light">
<span class="icon"><i class="fa fa-exclamation-triangle"></i></span>
<span>Your profile page is too private</span>
</p>
</header>
<div class="card-content">
<p class="block">
You are considered to be a <strong>Shy Account</strong> 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.
</p>
<p class="block">
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.
</p>
<p class="block">
<a href="/faq#shy-faqs">Click here to learn more</a> about your Shy Account. To
remedy this, please see the following steps:
</p>
<ul class="menu-list block">
<li>
<a href="/settings">
{{if eq .CurrentUser.Visibility "public"}}
<span class="icon"><i class="fa fa-circle-check has-text-success"></i></span>
{{else}}
<span class="icon"><i class="fa fa-circle has-text-danger"></i></span>
{{end}}
<span>
Have your profile visibility set to <strong>Public</strong>.
{{if not (eq .CurrentUser.Visibility "public")}}
<span class="icon"><i class="fa fa-external-link"></i></span>
{{end}}
</span>
</a>
</li>
<li>
<a href="/photo/u/{{.CurrentUser.Username}}">
{{if .HasPublicPhoto}}
<span class="icon"><i class="fa fa-circle-check has-text-success"></i></span>
{{else}}
<span class="icon"><i class="fa fa-circle has-text-danger"></i></span>
{{end}}
<span>
Have at least one <strong>Public</strong> photo on your profile.
{{if not .HasPublicPhoto}}
<span class="icon"><i class="fa fa-external-link"></i></span>
{{end}}
</span>
</a>
</li>
</ul>
</div>
</div>
{{end}}
<div class="card block">
<header class="card-header has-background-link">
<p class="card-header-title has-text-light">My Account</p>

View File

@ -228,6 +228,29 @@
{{if .GetProfileField "orientation"}}
<span class="mr-2">{{.GetProfileField "orientation"}}</span>
{{end}}
<!-- Show a subfooter based on ordered by -->
{{if eq $Root.Sort "last_login_at desc"}}
<div>
<small>
Last logged in:
<span title="On {{.LastLoginAt.Format "Jan _2 2006 15:04:05 MST"}}">
{{SincePrettyCoarse .LastLoginAt}} ago
</span>
</small>
</div>
{{end}}
{{if eq $Root.Sort "created_at desc"}}
<div>
<small>
Member since:
<span title="On {{.CreatedAt.Format "Jan _2 2006 15:04:05 MST"}}">
{{SincePrettyCoarse .CreatedAt}} ago
</span>
</small>
</div>
{{end}}
</p>
</div>
</div><!-- media-block -->

View File

@ -26,6 +26,15 @@
</div>
<div class="block p-4">
{{if .IsShyUser}}
<div class="notification is-danger is-light">
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong> 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.
<a href="/faq#shy-faqs">Learn more about how to resolve this issue. <small class="fa fa-external-link"></small></a>
</div>
{{end}}
<div class="content">
<p>
{{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.
</p>
<p>
Unfortunately, the chat room does not work on iPhones or iPads at this time.
</p>
</div>
<p>

View File

@ -53,6 +53,15 @@
</ul>
</li>
<li>
<a href="#shy-faqs">Shy Account FAQs</a>
<ul>
<li><a href="#shy-restrictions">What restrictions apply to Shy Accounts?</a></li>
<li><a href="#shy-cando">Why can Shy Accounts do?</a></li>
<li><a href="#shy-fixit">How do I fix it?</a></li>
</ul>
</li>
<li>
<a href="#technical-faqs">Technical FAQs</a>
<ul>
@ -499,6 +508,123 @@
</li>
</ul>
<h1 id="shy-faqs"><i class="fa fa-ghost"></i> Shy Account FAQs</h1>
<p>
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.
</p>
<p>
When your profile page or photos are <em>all</em> set to Private or Friends-only, you will
be considered to have a <strong>Shy Account.</strong>
A Shy Account can still interact on the forums but will have limited options to
interact with non-restricted ({{PrettyTitle}}) members.
</p>
<h3 id="shy-restrictions">What restrictions apply to Shy Accounts?</h3>
<p>
The limits placed on Shy Accounts are:
</p>
<ul>
<li>
The <i class="fa fa-image"></i> <strong>Site Gallery</strong> will only show you pictures
of people equally as shy as you are. That is, you may see your own pictures and those of
Friends you have added, but you don't see public shares of {{PrettyTitle}} people
who aren't your friends.
</li>
<li>
<strong><i class="fa fa-envelope"></i> Messages:</strong> you may slide into the DMs only
of other shy members but you can <strong>not</strong> initiate DMs with a {{PrettyTitle}} one who is not on
your Friends list. At their own discretion, they may initiate a chat with you and then you can reply to them.
</li>
<li>
You can view anybody's <i class="fa fa-user"></i> <strong>Profile Page</strong> but you
can <strong>not</strong> see a {{PrettyTitle}} account's Photo Gallery unless they are
your Friend or have shared their private pictures with you.
</li>
<li>
You can not join the <i class="fa fa-message"></i> <strong>Chat Room</strong>. You guys
may soon get your own chat room, though. Many of us {{PrettyTitle}} nudists would not
enjoy our webcams being watched by blank profiles.
</li>
</ul>
<p>
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.
</p>
<h3 id="shy-cando">What <em>can</em> Shy Accounts do?</h3>
<p>
There are still a lot things you <strong>can</strong> do with your <strong>certified</strong>
but Shy Account:
</p>
<ul>
<li>
You can still participate on the <i class="fa fa-comments"></i> <strong>Forums</strong> and meet new friends
that way - by contributing to discussions, ideally.
</li>
<li>
You can send a <i class="fa fa-user"></i> <strong>Friend request</strong> to anybody and if they accept you
can see their Photo Gallery and pictures appear in the Site Gallery.
</li>
<li>
You can send <i class="fa fa-envelope"></i> <strong>DMs</strong> to other shy people like yourself, and reply
to DMs that were sent by anybody who messages you first.
</li>
<li>
You can browse the <i class="fa fa-people-group"></i> <strong>Member Directory</strong> and view public
profile pages and send friend requests to whoever.
</li>
</ul>
<h3 id="shy-fixit">How do I fix it?</h3>
<p>
Leaving <strong>Shy Account</strong> territory is easy:
</p>
<ol>
<li>Don't have your profile page set to <strong>private.</strong> Only logged-in, certified members can see your page, anyway!</li>
<li>
Have at least one <strong>public</strong> 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.
</li>
</ol>
<p>
If you are new to all of this, here are some ideas how you can manage your
photo gallery to have at least one <strong>public</strong> picture to share:
</p>
<ul>
<li>You could have a single, "G-rated" face pic as your Public profile picture, and have the others on Friends-only or Private.</li>
<li>
You could upload all your "G-rated" face pics as Public, and have nudes (with your face cropped out if you need) on Friends-only
or Private.
</li>
<li>
You could have a non-public profile pic along with "anonymized" nudes on Public, full nudes w/ face on Friends-only, and
sexual stuff on Private that you unlock on a per-person basis.
</li>
<li>
You can <strong>opt-out</strong> of the Site Gallery by un-checking the Gallery box on the upload page. Your public
photos then would only been seen if somebody clicks <em>through</em> your profile page to see your gallery.
</li>
</ul>
<h1 id="technical-faqs">Technical FAQs</h1>
<h3 id="why">Why did you build a custom website?</h3>

View File

@ -72,6 +72,7 @@
<!-- Reusable pager -->
{{define "pager"}}
{{if .Pager.Total}}
<nav class="pagination" role="navigation" aria-label="pagination">
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Previous}}">Previous</a>
@ -91,6 +92,7 @@
</ul>
</nav>
{{end}}
{{end}}
<!-- Main content template -->
{{define "content"}}
@ -162,16 +164,34 @@
<button class="modal-close is-large" aria-label="close"></button>
</div>
<!-- Shy User alert banner (Site Gallery) -->
{{if and .IsSiteGallery .IsShyUser}}
<div class="notification is-danger is-light">
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong> so you will only see
pictures of you and your friends here. <a href="/faq#shy-faqs">Learn more <small class="fa fa-external-link"></small></a>
</div>
{{end}}
<!-- Shy User alert banner (User Gallery - IsShyFrom) -->
{{if .IsShyFrom}}
<div class="notification is-danger is-light">
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong> and you are not friends
with this person so can not see their gallery. <a href="/faq#shy-faqs">Learn more <small class="fa fa-external-link"></small></a>
</div>
{{end}}
<div class="block">
<div class="level{{if .IsOwnPhotos}}mb-0{{end}}">
<div class="level-left">
<div class="level-item">
{{if .Pager.Total}}
<span>
Found <strong>{{.Pager.Total}}</strong> photo{{Pluralize64 .Pager.Total}} (page {{.Pager.Page}} of {{.Pager.Pages}}).
{{if .ExplicitCount}}
{{.ExplicitCount}} explicit photo{{Pluralize64 .ExplicitCount}} hidden per your settings.
{{end}}
</span>
{{end}}
</div>
</div>