website/pkg/chat/chat_api.go
Noah Petherbridge 066765d2dc 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)
2024-09-19 19:30:02 -07:00

225 lines
6.4 KiB
Go

package chat
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/log"
"code.nonshy.com/nonshy/website/pkg/models"
)
// MaybeDisconnectUser may send a DisconnectUserNow to BareRTC if the user should not be allowed in the chat room.
//
// For example, they have set their profile to private and become a shy account, or they deactivated or got banned.
//
// If the user is presently in the chat room, they will be removed and given an appropriate ChatServer message.
//
// Returns a boolean OK (they were online in chat, and were removed) with the error only returning in case of a
// communication or JSON encode error with BareRTC. If they were online and removed, an admin feedback notice is
// also generated for visibility and confirmation of success.
func MaybeDisconnectUser(user *models.User) (bool, error) {
// What reason to remove them? If a message is provided, the DisconnectUserNow API will be called.
var because = "You have been signed out of chat because "
var reasons = []struct {
If bool
Message string
}{
{
If: !user.Certified,
Message: because + "your nonshy account is not Certified, or its Certified status has been revoked.",
},
{
If: user.IsShy(),
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.",
},
{
If: user.Status == models.UserStatusDisabled,
Message: because + "you have deactivated your nonshy account.",
},
{
If: user.Status == models.UserStatusBanned,
Message: because + "your nonshy account has been banned.",
},
{
// Catch-all for any non-active user status.
If: user.Status != models.UserStatusActive,
Message: because + "your nonshy account is no longer eligible to remain in the chat room.",
},
}
for _, reason := range reasons {
if reason.If {
i, err := DisconnectUserNow(user, reason.Message)
if err != nil {
return false, err
}
// Were they online and were removed? Notify the admin for visibility.
if i > 0 {
fb := &models.Feedback{
Intent: "report",
Subject: "Auto-Disconnect from Chat",
UserID: user.ID,
TableName: "users",
TableID: user.ID,
Message: fmt.Sprintf(
"A user was automatically disconnected from the chat room!\n\n"+
"* Username: %s\n"+
"* Number of users removed: %d\n"+
"* Message sent to them: %s\n\n"+
"Note: this is an informative message only. Users are expected to be removed from "+
"chat when they do things such as deactivate their account, or private their profile "+
"or pictures, and thus become ineligible to remain in the chat room.",
user.Username,
i,
reason.Message,
),
}
// Save the feedback.
if err := models.CreateFeedback(fb); err != nil {
log.Error("Couldn't save feedback from user updating their DOB: %s", err)
}
}
// First removal reason wins.
break
}
}
return false, nil
}
// DisconnectUserNow tells the chat room to remove the user now if they are presently online.
func DisconnectUserNow(user *models.User, message string) (int, error) {
// API request struct for BareRTC /api/block/now endpoint.
var request = struct {
APIKey string
Usernames []string
Message string
Kick bool
}{
APIKey: config.Current.CronAPIKey,
Usernames: []string{
user.Username,
},
Message: message,
Kick: false,
}
type response struct {
OK bool
Removed int
Error string `json:",omitempty"`
}
// JSON request body.
jsonStr, err := json.Marshal(request)
if err != nil {
return 0, err
}
// Make the API request to BareRTC.
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/disconnect/now"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
// Ingest the JSON response to see the count and error.
var (
result response
body, _ = io.ReadAll(resp.Body)
)
err = json.Unmarshal(body, &result)
if err != nil {
return 0, err
}
if resp.StatusCode != http.StatusOK || !result.OK {
log.Error("DisconnectUserNow: error from BareRTC: status %d body %s", resp.StatusCode, body)
return result.Removed, errors.New(result.Error)
}
return result.Removed, nil
}
// EraseChatHistory tells the chat room to clear DMs history for this user.
func EraseChatHistory(username string) (int, error) {
// API request struct for BareRTC /api/message/clear endpoint.
var request = struct {
APIKey string
Username string
}{
APIKey: config.Current.CronAPIKey,
Username: username,
}
type response struct {
OK bool
MessagesErased int
Error string `json:",omitempty"`
}
// JSON request body.
jsonStr, err := json.Marshal(request)
if err != nil {
return 0, err
}
// Make the API request to BareRTC.
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/message/clear"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
// Ingest the JSON response to see the count and error.
var (
result response
body, _ = io.ReadAll(resp.Body)
)
err = json.Unmarshal(body, &result)
if err != nil {
return 0, err
}
if resp.StatusCode != http.StatusOK || !result.OK {
log.Error("EraseChatHistory: error from BareRTC: status %d body %s", resp.StatusCode, body)
return result.MessagesErased, errors.New(result.Error)
}
return result.MessagesErased, nil
}