diff --git a/pkg/chat/chat_api.go b/pkg/chat/chat_api.go
index 62f3aa5..3e48a8d 100644
--- a/pkg/chat/chat_api.go
+++ b/pkg/chat/chat_api.go
@@ -37,9 +37,10 @@ func MaybeDisconnectUser(user *models.User) (bool, error) {
},
{
If: user.IsShy(),
- Message: because + "you had updated your nonshy profile to become too private.
" +
- "You may join the chat room after you have made your profile and (at least some) pictures " +
- "viewable on 'public' so that you won't appear to be a blank, faceless profile to others on the chat room.
" +
+ Message: because + "you had updated your nonshy profile to become too private, and now are considered to have a 'Shy Account.'
" +
+ "You may refresh the page to log back into chat as a Shy Account, where your ability to use webcams and share photos " +
+ "will be restricted. To regain full access to the chat room, please edit your profile settings to make sure that at least one 'public' " +
+ "photo is viewable to other members of the website.
" +
"Please see the Shy Account FAQ for more information.",
},
{
diff --git a/pkg/config/enum.go b/pkg/config/enum.go
index f02717b..5988a68 100644
--- a/pkg/config/enum.go
+++ b/pkg/config/enum.go
@@ -79,6 +79,7 @@ var (
"dm_privacy",
"blur_explicit",
"site_gallery_default", // default view on site gallery (friends-only or all certified?)
+ "chat_moderation_rules",
}
// Choices for the Contact Us subject
@@ -119,6 +120,30 @@ var (
regexp.MustCompile(`\b(telegram|whats\s*app|signal|kik|session)\b`),
regexp.MustCompile(`https?://(t.me|join.skype.com|zoom.us|whereby.com|meet.jit.si|wa.me)`),
}
+
+ // Chat Moderation Rules.
+ ChatModerationRules = []ChecklistOption{
+ {
+ Value: "redcam",
+ Label: "Red camera",
+ Help: "The user's camera is forced to 'explicit' when they are broadcasting.",
+ },
+ {
+ Value: "nobroadcast",
+ Label: "No broadcast",
+ Help: "The user can not broadcast their webcam, but may still watch other peoples' webcams.",
+ },
+ {
+ Value: "novideo",
+ Label: "No webcam privileges",
+ Help: "The user can not broadcast or watch any webcam. Note: this option supercedes all other video-related rules.",
+ },
+ {
+ Value: "noimage",
+ Label: "No image sharing privileges",
+ Help: "The user can not share or see any image shared on chat.",
+ },
+ }
)
// ContactUs choices for the subject drop-down.
@@ -133,6 +158,13 @@ type Option struct {
Label string
}
+// ChecklistOption for checkbox-lists.
+type ChecklistOption struct {
+ Value string
+ Label string
+ Help string
+}
+
// NotificationOptout field values (stored in user ProfileField table)
const (
NotificationOptOutFriendPhotos = "notif_optout_friends_photos"
diff --git a/pkg/controller/account/profile.go b/pkg/controller/account/profile.go
index 38b4ec9..7b9da46 100644
--- a/pkg/controller/account/profile.go
+++ b/pkg/controller/account/profile.go
@@ -3,7 +3,9 @@ package account
import (
"net/http"
"net/url"
+ "strings"
+ "code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/middleware"
"code.nonshy.com/nonshy/website/pkg/models"
@@ -101,6 +103,14 @@ func Profile() http.HandlerFunc {
log.Error("WhoLikes(user %d): %s", user.ID, err)
}
+ // Chat Moderation Rule: count of rules applied to the user, for admin view.
+ var chatModerationRules int
+ if currentUser.HasAdminScope(config.ScopeChatModerator) {
+ if rules := user.GetProfileField("chat_moderation_rules"); len(rules) > 0 {
+ chatModerationRules = len(strings.Split(rules, ","))
+ }
+ }
+
vars := map[string]interface{}{
"User": user,
"LikeMap": likeMap,
@@ -116,6 +126,9 @@ func Profile() http.HandlerFunc {
"LikeRemainder": likeRemainder,
"LikeTableName": "users",
"LikeTableID": user.ID,
+
+ // Admin numbers.
+ "NumChatModerationRules": chatModerationRules,
}
if err := tmpl.Execute(w, r, vars); err != nil {
diff --git a/pkg/controller/account/settings.go b/pkg/controller/account/settings.go
index 2784bc5..a6a77fe 100644
--- a/pkg/controller/account/settings.go
+++ b/pkg/controller/account/settings.go
@@ -62,6 +62,10 @@ func Settings() http.HandlerFunc {
// Are we POSTing?
if r.Method == http.MethodPost {
+
+ // Will they BECOME a Shy Account with this change?
+ var wasShy = user.IsShy()
+
intent := r.PostFormValue("intent")
switch intent {
case "profile":
@@ -472,8 +476,10 @@ func Settings() http.HandlerFunc {
}
// Maybe kick them from the chat room if they had become a Shy Account.
- if _, err := chat.MaybeDisconnectUser(user); err != nil {
- log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err)
+ if !wasShy && user.IsShy() {
+ if _, err := chat.MaybeDisconnectUser(user); err != nil {
+ log.Error("chat.MaybeDisconnectUser(%s#%d): %s", user.Username, user.ID, err)
+ }
}
templates.Redirect(w, r.URL.Path+hashtag+".")
diff --git a/pkg/controller/admin/user_actions.go b/pkg/controller/admin/user_actions.go
index fb0ee1b..769553d 100644
--- a/pkg/controller/admin/user_actions.go
+++ b/pkg/controller/admin/user_actions.go
@@ -128,6 +128,48 @@ func UserActions() http.HandlerFunc {
count, total := models.CountBlockedAdminUsers(user)
vars["AdminBlockCount"] = count
vars["AdminBlockTotal"] = total
+ case "chat.rules":
+ // Chat Moderation Rules.
+ if !currentUser.HasAdminScope(config.ScopeChatModerator) {
+ session.FlashError(w, r, "Missing admin scope: %s", config.ScopeChatModerator)
+ templates.Redirect(w, "/admin")
+ return
+ }
+
+ if r.Method == http.MethodPost {
+ // Rules list for the change log.
+ var newRules = "(none)"
+ if rule, ok := r.PostForm["rules"]; ok {
+ newRules = strings.Join(rule, ",")
+ user.SetProfileField("chat_moderation_rules", newRules)
+ } else {
+ user.DeleteProfileField("chat_moderation_rules")
+ }
+
+ if err := user.Save(); err != nil {
+ session.FlashError(w, r, "Error saving the user's chat rules: %s", err)
+ } else {
+ session.Flash(w, r, "Chat moderation rules have been updated!")
+ }
+ templates.Redirect(w, "/u/"+user.Username)
+
+ // Log the new rules to the changelog.
+ models.LogEvent(
+ user,
+ currentUser,
+ "updated",
+ "chat.rules",
+ user.ID,
+ fmt.Sprintf(
+ "An admin has updated the chat moderation rules for this user.\n\n"+
+ "The update rules are: %s",
+ newRules,
+ ),
+ )
+ return
+ }
+
+ vars["ChatModerationRules"] = config.ChatModerationRules
case "essays":
// Edit their profile essays easily.
if !currentUser.HasAdminScope(config.ScopePhotoModerator) {
diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go
index 8347aaf..d867a66 100644
--- a/pkg/controller/chat/chat.go
+++ b/pkg/controller/chat/chat.go
@@ -25,13 +25,14 @@ import (
// JWT claims.
type Claims struct {
// Custom claims.
- IsAdmin bool `json:"op,omitempty"`
- VIP bool `json:"vip,omitempty"`
- Avatar string `json:"img,omitempty"`
- ProfileURL string `json:"url,omitempty"`
- Nickname string `json:"nick,omitempty"`
- Emoji string `json:"emoji,omitempty"`
- Gender string `json:"gender,omitempty"`
+ IsAdmin bool `json:"op,omitempty"`
+ VIP bool `json:"vip,omitempty"`
+ Avatar string `json:"img,omitempty"`
+ ProfileURL string `json:"url,omitempty"`
+ Nickname string `json:"nick,omitempty"`
+ Emoji string `json:"emoji,omitempty"`
+ Gender string `json:"gender,omitempty"`
+ Rules []string `json:"rules,omitempty"`
// Standard claims. Notes:
// subject = username
@@ -76,16 +77,6 @@ func Landing() http.HandlerFunc {
return
}
- // 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)
@@ -120,6 +111,16 @@ func Landing() http.HandlerFunc {
emoji = "🍰 It's my birthday!"
}
+ // Apply chat moderation rules.
+ var rules = []string{}
+ if isShy {
+ // Shy account: no camera privileges.
+ rules = []string{"novideo", "noimage"}
+ } else if v := currentUser.GetProfileField("chat_moderation_rules"); len(v) > 0 {
+ // Specific mod rules applied to the current user.
+ rules = strings.Split(v, ",")
+ }
+
// Create the JWT claims.
claims := Claims{
IsAdmin: currentUser.HasAdminScope(config.ScopeChatModerator),
@@ -128,6 +129,8 @@ func Landing() http.HandlerFunc {
Nickname: currentUser.NameOrUsername(),
Emoji: emoji,
Gender: Gender(currentUser),
+ VIP: isShy, // "shy accounts" use the "VIP" status for special icon in chat
+ Rules: rules,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
IssuedAt: jwt.NewNumericDate(time.Now()),
diff --git a/pkg/controller/photo/edit_delete.go b/pkg/controller/photo/edit_delete.go
index 5425e40..9979fcf 100644
--- a/pkg/controller/photo/edit_delete.go
+++ b/pkg/controller/photo/edit_delete.go
@@ -71,6 +71,9 @@ func Edit() http.HandlerFunc {
// Are we saving the changes?
if r.Method == http.MethodPost {
+ // Record if this change is going to make them a Shy Account.
+ var wasShy = currentUser.IsShy()
+
var (
caption = strings.TrimSpace(r.FormValue("caption"))
altText = strings.TrimSpace(r.FormValue("alt_text"))
@@ -158,8 +161,11 @@ func Edit() http.HandlerFunc {
models.LogUpdated(currentUser, requestUser, "photos", photo.ID, "Updated the photo's settings.", diffs)
// Maybe kick them from the chat if this photo save makes them a Shy Account.
- if _, err := chat.MaybeDisconnectUser(currentUser); err != nil {
- log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err)
+ currentUser.FlushCaches()
+ if !wasShy && currentUser.IsShy() {
+ if _, err := chat.MaybeDisconnectUser(currentUser); err != nil {
+ log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err)
+ }
}
// If this picture has moved to Private, revoke any notification we gave about it before.
@@ -242,6 +248,9 @@ func Delete() http.HandlerFunc {
// Confirm deletion?
if r.Method == http.MethodPost {
+ // Record if this change is going to make them a Shy Account.
+ var wasShy = currentUser.IsShy()
+
confirm := r.PostFormValue("confirm") == "true"
if !confirm {
session.FlashError(w, r, "Confirm you want to delete this photo.")
@@ -286,8 +295,11 @@ func Delete() http.HandlerFunc {
session.Flash(w, r, "Photo deleted!")
// Maybe kick them from chat if this deletion makes them into a Shy Account.
- if _, err := chat.MaybeDisconnectUser(currentUser); err != nil {
- log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err)
+ currentUser.FlushCaches()
+ if !wasShy && currentUser.IsShy() {
+ if _, err := chat.MaybeDisconnectUser(currentUser); err != nil {
+ log.Error("chat.MaybeDisconnectUser(%s#%d): %s", currentUser.Username, currentUser.ID, err)
+ }
}
// Return the user to their gallery.
diff --git a/pkg/models/photo.go b/pkg/models/photo.go
index 2d5958c..ee422db 100644
--- a/pkg/models/photo.go
+++ b/pkg/models/photo.go
@@ -472,6 +472,13 @@ func (u *User) DistinctPhotoTypes() (result map[PhotoVisibility]struct{}) {
return
}
+// FlushCaches clears any cached attributes (such as distinct photo types) for the user.
+func (u *User) FlushCaches() {
+ u.cachePhotoTypes = nil
+ u.cacheBlockedUserIDs = nil
+ u.cachePhotoIDs = nil
+}
+
// Gallery config for the main Gallery paginator.
type Gallery struct {
Explicit string // Explicit filter
diff --git a/pkg/models/user.go b/pkg/models/user.go
index 6d49315..1e84f69 100644
--- a/pkg/models/user.go
+++ b/pkg/models/user.go
@@ -807,6 +807,15 @@ func (u *User) SetProfileField(name, value string) {
}
}
+// DeleteProfileField removes a stored profile field.
+func (u *User) DeleteProfileField(name string) error {
+ res := DB.Raw(
+ "DELETE FROM profile_fields WHERE user_id=? AND name=?",
+ u.ID, name,
+ )
+ return res.Error
+}
+
// GetProfileField returns the value of a profile field or blank string.
func (u *User) GetProfileField(name string) string {
for _, field := range u.ProfileField {
diff --git a/web/templates/account/profile.html b/web/templates/account/profile.html
index 4d51a4d..75fd6b1 100644
--- a/web/templates/account/profile.html
+++ b/web/templates/account/profile.html
@@ -492,6 +492,17 @@
+ You may use this page to add or remove chat moderation rules for this user account. +
+ ++ Moderation rules are useful to apply restrictions to certain problematic users who habitually break + the site rules. For example: somebody who insists on keeping their camera "blue" (non-explicit) while + always jerking off and resisting the admin request that their camera should be marked "red" can have that + choice taken away from them, and have their camera be forced red at all times when they are broadcasting. +
+ ++ Note: "Shy Accounts" automatically have the No webcam privileges + and No image sharing privileges rules applied when they log onto the chat room. +
++ {{.Help}} +
+diff --git a/web/templates/chat.html b/web/templates/chat.html index f7b57d6..dc62177 100644 --- a/web/templates/chat.html +++ b/web/templates/chat.html @@ -48,11 +48,27 @@ {{end}} {{if .IsShyUser}} -
+ You have a Shy Account, so you will experience + limited functionality on the chat room: +
+ ++ This is because, as a Shy Account, you are not sharing any public photos with the community on your profile + page, so to most other members of {{PrettyTitle}} you appear to be a "blank, faceless profile" and people on + the chat room generally feel uncomfortable having their webcams be watched by such a Shy Account. +
+ ++ Click here to learn more about your Shy Account, including steps on how to + resolve this. +
The chat room is available to all certified members who have public photos on their profile page. Shy Accounts who have private profiles or keep - all their pictures hidden are not permitted in the chat room at this time - but they may get - their own separate room later where they can bother other similarly shy members there. + all their pictures hidden MAY join the chat room, but have certain restrictions applied (such + as an inability to broadcast or watch any webcam, or share or see any picture shared on chat).
@@ -1400,9 +1400,9 @@ your Friend or have shared their private pictures with you.
+ On the chat room, many {{PrettyTitle}} members may be sharing their webcams and it is widely + regarded as awkward to have a "blank, faceless profile" silently lurking on your camera. So, a + Shy Account is allowed on the chat room but can not share or watch webcams, or share or view photos + posted by other members on the chat room. +
+@@ -1421,6 +1428,10 @@