Chat Moderation Rules + Shy Accounts on Chat
* Add chat moderation rules to the website, so admins can apply selective rules to problematic users. Available rules are: * redcam: user's camera is always NSFW. * nobroadcast: user can not broadcast their camera. * novideo: user can not broadcast OR watch any video. * noimage: user can not share OR see any shared image on chat. * The page to manage a user's active rules is available on their admin card of their profile page. When the user has rules active, a yellow counter is shown by the link to manage their rules. * Only chat moderator admins have access to the page or can see the yellow counter to know whether rules are active. * "Shy Accounts" are now permitted on the chat room! With some moderation rules automatically applied to them: novideo,noimage. * Update the Shy Account FAQ and messaging on the chat landing page. * Update the auto-kick from chat behavior regarding shy accounts: * They are kicked from chat only when an update to their profile settings will transition then FROM a non-shy into a shy account. * For example: when saving their profile settings (going private) or when editing or deleting a photo (if they will have no more public photos left)
This commit is contained in:
parent
ae84ddf449
commit
066765d2dc
|
@ -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.<br><br>" +
|
||||
"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.<br><br>" +
|
||||
Message: because + "you had updated your nonshy profile to become too private, and now are considered to have a 'Shy Account.'<br><br>" +
|
||||
"You may <strong>refresh</strong> 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.<br><br>" +
|
||||
"Please see the <a href=\"https://www.nonshy.com/faq#shy-faqs\">Shy Account FAQ</a> for more information.",
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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+".")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -492,6 +492,17 @@
|
|||
</li>
|
||||
|
||||
<p class="menu-label">Admin Actions</p>
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=chat.rules&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||
<span>
|
||||
Chat Moderation Rules
|
||||
{{if .NumChatModerationRules}}
|
||||
<span class="tag is-warning ml-2">{{.NumChatModerationRules}}</span>
|
||||
{{end}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/admin/user-action?intent=essays&user_id={{.User.ID}}">
|
||||
<span class="icon"><i class="fa fa-pencil"></i></span>
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{{$Root := .}}
|
||||
|
||||
<div class="block p-4">
|
||||
<div class="columns is-centered">
|
||||
<div class="column is-half">
|
||||
|
@ -22,6 +24,9 @@
|
|||
{{if eq .Intent "impersonate"}}
|
||||
<i class="mr-2 fa fa-ghost"></i>
|
||||
Impersonate User
|
||||
{{else if eq .Intent "chat.rules"}}
|
||||
<i class="mr-2 fa fa-gavel"></i>
|
||||
Chat Moderation Rules
|
||||
{{else if eq .Intent "essays"}}
|
||||
<i class="mr-2 fa fa-pencil"></i>
|
||||
Edit Profile Text
|
||||
|
@ -171,6 +176,45 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else if eq .Intent "chat.rules"}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
You may use this page to add or remove <strong>chat moderation rules</strong> for this user account.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Note:</strong> <a href="/faq#shy-faqs">"Shy Accounts"</a> automatically have the <strong>No webcam privileges</strong>
|
||||
and <strong>No image sharing privileges</strong> rules applied when they log onto the chat room.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{range .ChatModerationRules}}
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
name="rules"
|
||||
value="{{.Value}}"
|
||||
{{if $Root.User.ProfileFieldIn "chat_moderation_rules" .Value}}checked{{end}}>
|
||||
{{.Label}}
|
||||
</label>
|
||||
<p class="help">
|
||||
{{.Help}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="field has-text-centered">
|
||||
<button type="submit" class="button is-success">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
{{else if eq .Intent "essays"}}
|
||||
<div class="block content">
|
||||
<p>
|
||||
|
|
|
@ -48,11 +48,27 @@
|
|||
{{end}}
|
||||
|
||||
{{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 class="notification is-warning is-light content">
|
||||
<p>
|
||||
<i class="fa fa-exclamation-triangle"></i> You have a <strong>Shy Account</strong>, so you will experience
|
||||
limited functionality on the chat room:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>You may not broadcast or watch any webcam on chat.</li>
|
||||
<li>You may not share or see pictures shared by other members on chat.</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="/faq#shy-faqs">Click here to learn more</a> about your Shy Account, including steps on how to
|
||||
resolve this.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
|
|
@ -1131,8 +1131,8 @@
|
|||
<p>
|
||||
The chat room is available to all <strong>certified</strong> members who have public photos
|
||||
on their profile page. <a href="#shy-faqs">Shy Accounts</a> 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).
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
@ -1400,9 +1400,9 @@
|
|||
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.
|
||||
You <strong>can</strong> join the <i class="fa fa-message"></i> <strong>Chat Room</strong>, however
|
||||
some features will be restricted to Shy Accounts: you will not be able to broadcast OR watch any webcam
|
||||
on chat, nor can you share OR view any photo shared by others on the chat room.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -1413,6 +1413,13 @@
|
|||
kept with the other blank profiles until you choose to participate.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<h3 id="shy-cando">What <em>can</em> Shy Accounts do?</h3>
|
||||
|
||||
<p>
|
||||
|
@ -1421,6 +1428,10 @@
|
|||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
You can still join the <i class="fa fa-message"></i> <strong>Chat Room</strong> and have text-based
|
||||
conversations with people (with just webcam and image sharing support restricted).
|
||||
</li>
|
||||
<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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user