New Op commands and fixes with blocking admin users

Add moderation rules:

* You can apply rules in the settings.toml to enforce moderator restrictions on
  certain users, e.g. to force their camera to always be NSFW or bar them from
  sharing their webcam at all anymore.

Chat UI improvements around users blocking admin accounts:

* When a main website block is in place, the DMs button in the Who List shows
  as greyed out with a cross through, as if that user had closed their DMs.
* Admin users are always able to watch the camera of people who have blocked
  them. The broadcaster is not notified about the watch.

New operator commands:

* /cut username: to tell a user to turn off their webcam.
* /unmute-all: to lift all mutes on your side, e.g. so your moderator chatbot
  can still see public messages from users who have blocked it.
* /help-advanced: moved the more dangerous admin command documentation here.

Miscellaneous fixes:

* The admin commands now tolerate an @ prefix in front of usernames.
* The /nsfw command won't fire unless the user's camera is actually active and
  not marked as explicit.
master
Noah 2024-05-16 23:33:19 -07:00
parent b74edd1512
commit 9c77bdb62e
14 changed files with 296 additions and 52 deletions

View File

@ -313,6 +313,19 @@ The server passes the watch/unwatch message to the broadcaster.
}
```
## Cut
Sent by: Server.
The server tells the client to turn off their camera. This is done in response to a `/cut` command being sent by an admin user: to remotely cause another user on chat to turn off their camera and stop broadcasting.
```javascript
// Server Cut
{
"action": "cut"
}
```
## Mute, Unmute
Sent by: Client.

View File

@ -8,17 +8,20 @@ BareRTC is a simple WebRTC-based chat room application. It is especially designe
It is very much in the style of the old-school Flash based webcam chat rooms of the early 2000's: a multi-user chat room with DMs and _some_ users may broadcast video and others may watch multiple video feeds in an asynchronous manner. I thought that this should be such an obvious free and open source app that should exist, but it did not and so I had to write it myself.
* [Features](#features)
* [Configuration](#configuration)
* [Authentication](#authentication)
* [JWT Strict Mode](#jwt-strict-mode)
* [Running Without Authentication](#running-without-authentication)
* [Known Bugs Running Without Authentication](#known-bugs-running-without-authentication)
* [Moderator Commands](#moderator-commands)
* [JSON APIs](#json-apis)
* [Tour of the Codebase](#tour-of-the-codebase)
* [Deploying This App](#deploying-this-app)
* [License](#license)
- [BareRTC](#barertc)
- [Features](#features)
- [Configuration](#configuration)
- [Authentication](#authentication)
- [Moderator Commands](#moderator-commands)
- [JSON APIs](#json-apis)
- [Webhook URLs](#webhook-urls)
- [Chatbot](#chatbot)
- [Tour of the Codebase](#tour-of-the-codebase)
- [Backend files](#backend-files)
- [Frontend files](#frontend-files)
- [Deploying This App](#deploying-this-app)
- [Developing This App](#developing-this-app)
- [License](#license)
# Features
@ -34,14 +37,7 @@ It is very much in the style of the old-school Flash based webcam chat rooms of
* WebRTC means peer-to-peer video streaming so cheap on hosting costs!
* Simple integration with your existing userbase via signed JWT tokens.
* User configurable sound effects to be notified of DMs or users entering/exiting the room.
* Operator commands
* [x] /kick users
* [x] /ban users (and /unban, /bans to list)
* [x] /nsfw to tag a user's camera as explicit
* [x] /shutdown to gracefully reboot the server
* [x] /kickall to kick EVERYBODY off the server (e.g., for mandatory front-end reload for new features)
* [x] /op and /deop users (give temporary mod control)
* [x] /help to get in-chat help for moderator commands
* Operator commands to kick, ban users, mark cameras NSFW, etc.
The BareRTC project also includes a [Chatbot implementation](docs/Chatbot.md) so you can provide an official chatbot for fun & games & to auto moderate your chat room!

View File

@ -36,6 +36,7 @@ type Client struct {
OnOpen HandlerFunc
OnWatch HandlerFunc
OnUnwatch HandlerFunc
OnCut HandlerFunc
OnError HandlerFunc
OnDisconnect HandlerFunc
OnPing HandlerFunc
@ -129,6 +130,8 @@ func (c *Client) Run() error {
c.Handle(msg, c.OnWatch)
case messages.ActionUnwatch:
c.Handle(msg, c.OnUnwatch)
case messages.ActionCut:
c.Handle(msg, c.OnCut)
case messages.ActionError:
c.Handle(msg, c.OnError)
case messages.ActionKick:

View File

@ -109,6 +109,7 @@ func (c *Client) SetupChatbot() error {
c.OnOpen = handler.OnOpen
c.OnWatch = handler.OnWatch
c.OnUnwatch = handler.OnUnwatch
c.OnCut = handler.OnCut
c.OnError = handler.OnError
c.OnDisconnect = handler.OnDisconnect
c.OnPing = handler.OnPing
@ -134,6 +135,12 @@ func (h *BotHandlers) OnMe(msg messages.Message) {
log.Error("OnMe: the server has renamed us to '%s'", msg.Username)
h.client.claims.Subject = msg.Username
}
// Send the /unmute-all command to lift any mutes imposed by users blocking the chatbot.
h.client.Send(messages.Message{
Action: messages.ActionMessage,
Message: "/unmute-all",
})
}
// Buffer a message seen on chat for a while.
@ -233,7 +240,6 @@ func (h *BotHandlers) OnMessage(msg messages.Message) {
// Set their user variables.
h.SetUserVariables(msg)
reply, err := h.rs.Reply(msg.Username, msg.Message)
log.Error("REPLY: %s", reply)
if NoReply(reply) {
return
}
@ -384,6 +390,11 @@ func (h *BotHandlers) OnUnwatch(msg messages.Message) {
}
// OnCut handles an admin telling us to cut our camera.
func (h *BotHandlers) OnCut(msg messages.Message) {
}
// OnError handles ChatServer messages from the backend.
func (h *BotHandlers) OnError(msg messages.Message) {
log.Error("[%s] %s", msg.Username, msg.Message)
@ -396,5 +407,9 @@ func (h *BotHandlers) OnDisconnect(msg messages.Message) {
// OnPing handles server keepalive pings.
func (h *BotHandlers) OnPing(msg messages.Message) {
// Send the /unmute-all command to lift any mutes imposed by users blocking the chatbot.
h.client.Send(messages.Message{
Action: messages.ActionMessage,
Message: "/unmute-all",
})
}

View File

@ -37,6 +37,16 @@ PreviewImageWidth = 360
Name = "Off Topic"
WelcomeMessages = ["Welcome to the Off Topic channel!"]
[[WebhookURLs]]
Name = "report"
Enabled = true
URL = "https://www.example.com/v1/barertc/report"
[[WebhookURLs]]
Name = "profile"
Enabled = true
URL = "https://www.example.com/v1/barertc/profile"
[VIP]
Name = "VIP"
Branding = "<em>VIP Members</em>"
@ -56,6 +66,23 @@ PreviewImageWidth = 360
ForwardMessage = false
ReportMessage = false
ChatServerResponse = "Watch your language."
[[ModerationRule]]
Username = "example"
CameraAlwaysNSFW = true
DisableCamera = false
[DirectMessageHistory]
Enabled = true
SQLiteDatabase = "database.sqlite"
RetentionDays = 90
DisclaimerMessage = "Reminder: please conduct yourself honorable in DMs."
[Logging]
Enabled = true
Directory = "./logs"
Channels = ["lobby"]
Usernames = []
```
A description of the config directives includes:
@ -119,3 +146,35 @@ Options for the `[[MessageFilters]]` section include:
* **ForwardMessage** (bool): whether to repeat the message to the other chatters. If false, the sender will see their own message echo (possibly censored) but other chatters will not get their message at all.
* **ReportMessage** (bool): if true, report the message along with the recent context (previous 10 messages in that conversation) to your website's report webhook (if configured).
* **ChatServerResponse** (str): optional - you can have ChatServer send a message to the sender (in the same channel) after the filter has been run. An empty string will not send a ChatServer message.
## Moderation Rules
This section of the config file allows you to place certain moderation rules on specific users of your chat room. For example: if somebody perpetually needs to be reminded to label their camera as NSFW, you can enforce a moderation rule on that user which _always_ forces their camera to be NSFW.
Settings in the `[[ModerationRule]]` array include:
* **Username** (string): the username on chat to apply the rule to.
* **CameraAlwaysNSFW** (bool): if true, the user's camera is forced to NSFW and they will receive a ChatServer message when they try and remove the flag themselves.
* **DisableCamera** (bool): if true, the user is not allowed to share their webcam and the server will send them a 'cut' message any time they go live, along with a ChatServer message informing them of this.
## Direct Message History
You can allow BareRTC to retain temporary DM history for your users so they can remember where they left off with people.
Settings for this include:
* **Enabled** (bool): set to true to log chat DMs history.
* **SQLiteDatabase** (string): the name of the .sqlite DB file to store their DMs in.
* **RetentionDays** (int): how many days of history to record before old chats are erased. Set to zero for no limit.
* **DisclaimerMessage** (string): a custom banner message to show at the top of DM threads. HTML is supported. A good use is to remind your users of your local site rules.
## Logging
This feature can enable logging of public channels and user DMs to text files on disk. It is useful to keep a log of your public channels so you can look back at the context of a reported public chat if you weren't available when it happened, or to selectively log the DMs of specific users to investigate a problematic user.
Settings include:
* **Enabled** (bool): to enable or disable the logging feature.
* **Directory** (string): a folder on disk to save logs into. Public channels will save directly as text files here (e.g. "lobby.txt"), while DMs will create a subfolder for the monitored user.
* **Channels** ([]string): array of public channel IDs to monitor.
* **Usernames** ([]string): array of chat usernames to monitor.

View File

@ -4,6 +4,7 @@ import (
"fmt"
"os"
"strconv"
"strings"
"time"
"git.kirsle.net/apps/barertc/pkg/config"
@ -47,20 +48,34 @@ func (s *Server) ProcessCommand(sub *Subscriber, msg messages.Message) bool {
case "/nsfw":
s.NSFWCommand(words, sub)
return true
case "/cut":
s.CutCommand(words, sub)
return true
case "/unmute-all":
s.UnmuteAllCommand(words, sub)
return true
case "/help":
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
sub.ChatServer(RenderMarkdown("The most common moderator commands on chat are:\n\n" +
"* `/kick <username>` to kick from chat\n" +
"* `/ban <username> <duration>` to ban from chat (default duration is 24 (hours))\n" +
"* `/unban <username>` to list the ban on a user\n" +
"* `/bans` to list current banned users and their expiration date\n" +
"* `/nsfw <username>` to mark their camera NSFW\n" +
"* `/cut <username>` to make them turn off their camera\n" +
"* `/unmute-all` to lift all mutes on your side\n" +
"* `/help` to show this message\n" +
"* `/help-advanced` to show advanced admin commands\n\n" +
"Note: shell-style quoting is supported, if a username has a space in it, quote the whole username, e.g.: `/kick \"username 2\"`",
))
return true
case "/help-advanced":
sub.ChatServer(RenderMarkdown("The following are **dangerous** commands that you should not use unless you know what you're doing:\n\n" +
"* `/op <username>` to grant operator rights to a user\n" +
"* `/deop <username>` to remove operator rights from a user\n" +
"* `/shutdown` to gracefully shut down (reboot) the chat server\n" +
"* `/kickall` to kick EVERYBODY off and force them to log back in\n" +
"* `/reconfigure` to dynamically reload the chat server settings file\n" +
"* `/help` to show this message\n\n" +
"Note: shell-style quoting is supported, if a username has a space in it, quote the whole username, e.g.: `/kick \"username 2\"`",
"* `/help-advanced` to show this message",
))
return true
case "/shutdown":
@ -109,14 +124,23 @@ func (s *Server) NSFWCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
sub.ChatServer("Usage: `/nsfw username` to add the NSFW flag to their camera.")
}
username := words[1]
username := strings.TrimPrefix(words[1], "@")
other, err := s.GetSubscriber(username)
if err != nil {
sub.ChatServer("/nsfw: username not found: %s", username)
} else {
// Sanity check that the target user is presently on a blue camera.
if !(other.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive) {
sub.ChatServer("/nsfw: %s's camera was not currently enabled.", username)
return
} else if other.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW {
sub.ChatServer("/nsfw: %s's camera was already marked as explicit.", username)
return
}
// The message to deliver to the target.
var message = "Just a friendly reminder to mark your camera as 'Explicit' by using the button at the top " +
"of the page if you are going to be sexual on webcam. "
"of the page if you are going to be sexual on webcam.<br><br>"
// If the admin who marked it was previously booted
if other.Boots(sub.Username) {
@ -133,6 +157,41 @@ func (s *Server) NSFWCommand(words []string, sub *Subscriber) {
}
}
// CutCommand handles the `/cut` operator command (force a user's camera to turn off).
func (s *Server) CutCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
sub.ChatServer("Usage: `/cut username` to turn their camera off.")
}
username := strings.TrimPrefix(words[1], "@")
other, err := s.GetSubscriber(username)
if err != nil {
sub.ChatServer("/cut: username not found: %s", username)
} else {
// Sanity check that the target user is presently on a blue camera.
if !(other.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive) {
sub.ChatServer("/cut: %s's camera was not currently enabled.", username)
return
}
other.SendCut()
sub.ChatServer("%s has been told to turn off their camera.", username)
}
}
// UnmuteAllCommand handles the `/unmute-all` operator command (remove all mutes for the current user).
//
// It enables an operator to see public messages from any user who muted/blocked them. Note: from the
// other side of the mute, the operator's public messages may still be hidden from those users.
//
// It is useful for an operator chatbot if you want users to be able to block it but still retain the
// bot's ability to moderate public channel messages, and send warnings in DMs to misbehaving users
// even despite a mute being in place.
func (s *Server) UnmuteAllCommand(words []string, sub *Subscriber) {
count := len(sub.muted)
sub.muted = map[string]struct{}{}
sub.ChatServer("Your mute on %d users has been lifted.", count)
}
// KickCommand handles the `/kick` operator command.
func (s *Server) KickCommand(words []string, sub *Subscriber) {
if len(words) == 1 {
@ -141,7 +200,7 @@ func (s *Server) KickCommand(words []string, sub *Subscriber) {
))
return
}
username := words[1]
username := strings.TrimPrefix(words[1], "@")
other, err := s.GetSubscriber(username)
if err != nil {
sub.ChatServer("/kick: username not found: %s", username)
@ -218,7 +277,7 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) {
// Parse the command.
var (
username = words[1]
username = strings.TrimPrefix(words[1], "@")
duration = 24 * time.Hour
)
if len(words) >= 3 {
@ -261,7 +320,7 @@ func (s *Server) UnbanCommand(words []string, sub *Subscriber) {
}
// Parse the command.
var username = words[1]
var username = strings.TrimPrefix(words[1], "@")
if UnbanUser(username) {
sub.ChatServer("The ban on %s has been lifted.", username)
@ -299,7 +358,7 @@ func (s *Server) OpCommand(words []string, sub *Subscriber) {
}
// Parse the command.
var username = words[1]
var username = strings.TrimPrefix(words[1], "@")
if other, err := s.GetSubscriber(username); err != nil {
sub.ChatServer("/op: user %s was not found.", username)
} else {
@ -329,7 +388,7 @@ func (s *Server) DeopCommand(words []string, sub *Subscriber) {
}
// Parse the command.
var username = words[1]
var username = strings.TrimPrefix(words[1], "@")
if other, err := s.GetSubscriber(username); err != nil {
sub.ChatServer("/deop: user %s was not found.", username)
} else {

View File

@ -13,7 +13,7 @@ import (
// Version of the config format - when new fields are added, it will attempt
// to write the settings.toml to disk so new defaults populate.
var currentVersion = 12
var currentVersion = 13
// Config for your BareRTC app.
type Config struct {
@ -50,6 +50,7 @@ type Config struct {
VIP VIP
MessageFilters []*MessageFilter
ModerationRule []*ModerationRule
DirectMessageHistory DirectMessageHistory
@ -119,6 +120,13 @@ type Logging struct {
Usernames []string
}
// ModerationRule applies certain rules to moderate specific users.
type ModerationRule struct {
Username string
CameraAlwaysNSFW bool
DisableCamera bool
}
// Current loaded configuration.
var Current = DefaultConfig()
@ -197,6 +205,11 @@ func DefaultConfig() Config {
ChatServerResponse: "Watch your language.",
},
},
ModerationRule: []*ModerationRule{
{
Username: "example",
},
},
DirectMessageHistory: DirectMessageHistory{
Enabled: false,
SQLiteDatabase: "database.sqlite",
@ -253,3 +266,13 @@ func WriteSettings() error {
}
return os.WriteFile("./settings.toml", buf.Bytes(), 0644)
}
// GetModerationRule returns a matching ModerationRule for the given user, or nil if no rule is found.
func (c Config) GetModerationRule(username string) *ModerationRule {
for _, rule := range c.ModerationRule {
if rule.Username == username {
return rule
}
}
return nil
}

View File

@ -214,7 +214,7 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
}
// If the sender already mutes the recipient, reply back with the error.
if err == nil && sub.Mutes(rcpt.Username) {
if err == nil && sub.Mutes(rcpt.Username) && !sub.IsAdmin() {
sub.ChatServer("You have muted %s and so your message has not been sent.", rcpt.Username)
return
}
@ -386,8 +386,36 @@ func (s *Server) OnFile(sub *Subscriber, msg messages.Message) {
// OnMe handles current user state updates.
func (s *Server) OnMe(sub *Subscriber, msg messages.Message) {
// Reflect a 'me' message back at them? (e.g. if server forces their camera NSFW)
var reflect bool
if msg.VideoStatus&messages.VideoFlagActive == messages.VideoFlagActive {
log.Debug("User %s turns on their video feed", sub.Username)
// Moderation rules?
if rule := config.Current.GetModerationRule(sub.Username); rule != nil {
// Are they barred from sharing their camera on chat?
if rule.DisableCamera {
sub.SendCut()
sub.ChatServer(
"A chat server moderation rule is currently in place which restricts your ability to share your webcam. Please " +
"contact a chat operator for more information.",
)
msg.VideoStatus = 0
}
// Is their camera forced to always be explicit?
if rule.CameraAlwaysNSFW && !(msg.VideoStatus&messages.VideoFlagNSFW == messages.VideoFlagNSFW) {
msg.VideoStatus |= messages.VideoFlagNSFW
reflect = true // send them a 'me' echo afterward to inform the front-end page properly of this
sub.ChatServer(
"A chat server moderation rule is currently in place which forces your camera to stay marked as Explicit. Please " +
"contact a chat moderator if you have any questions about this.",
)
}
}
}
// Hidden status: for operators only, + fake a join/exit chat message.
@ -418,6 +446,11 @@ func (s *Server) OnMe(sub *Subscriber, msg messages.Message) {
// Sync the WhoList to everybody.
s.SendWhoList()
// Reflect a 'me' message back?
if reflect {
sub.SendMe()
}
}
// OnOpen is a client wanting to start WebRTC with another, e.g. to see their camera.
@ -425,7 +458,6 @@ func (s *Server) OnOpen(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error(err.Error())
return
}
@ -490,11 +522,11 @@ func (s *Server) IsVideoNotAllowed(sub *Subscriber, other *Subscriber) (bool, st
Error: fmt.Sprintf("%s has requested that you should share your own camera too before opening theirs.", other.Username),
},
{
If: theirVIPRequired && !sub.IsVIP(),
If: theirVIPRequired && !sub.IsVIP() && !sub.IsAdmin(),
Error: "You do not have permission to view that camera.",
},
{
If: other.Mutes(sub.Username) || other.Blocks(sub),
If: (other.Mutes(sub.Username) || other.Blocks(sub)) && !sub.IsAdmin(),
Error: "You do not have permission to view that camera.",
},
}
@ -633,7 +665,6 @@ func (s *Server) OnCandidate(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error(err.Error())
return
}
@ -649,7 +680,6 @@ func (s *Server) OnSDP(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error(err.Error())
return
}
@ -665,7 +695,6 @@ func (s *Server) OnWatch(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error(err.Error())
return
}
@ -680,7 +709,6 @@ func (s *Server) OnUnwatch(sub *Subscriber, msg messages.Message) {
// Look up the other subscriber.
other, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error(err.Error())
return
}

View File

@ -94,6 +94,7 @@ const (
ActionPing = "ping"
ActionWhoList = "who" // server pushes the Who List
ActionPresence = "presence" // a user joined or left the room
ActionCut = "cut" // tell the client to turn off their webcam
ActionError = "error" // ChatServer errors
ActionKick = "disconnect" // client should disconnect (e.g. have been kicked).

View File

@ -149,7 +149,7 @@ func (s *Server) OnClientMessage(sub *Subscriber, msg messages.Message) {
s.OnReport(sub, msg)
case messages.ActionPing:
default:
sub.ChatServer("Unsupported message type.")
sub.ChatServer("Unsupported message type: %s", msg.Action)
}
}
@ -234,6 +234,13 @@ func (sub *Subscriber) SendMe() {
})
}
// SendCut sends the client a 'cut' message to deactivate their camera.
func (sub *Subscriber) SendCut() {
sub.SendJSON(messages.Message{
Action: messages.ActionCut,
})
}
// ChatServer is a convenience function to deliver a ChatServer error to the client.
func (sub *Subscriber) ChatServer(message string, v ...interface{}) {
sub.SendJSON(messages.Message{

View File

@ -1232,6 +1232,12 @@ export default {
this.config.CachedBlocklist.push(msg.username);
},
// Server side "cut" event: tells the user to turn off their camera.
onCut(msg) {
this.DebugChannel(`Received cut command from server: ${JSON.stringify(msg)}`);
this.stopVideo();
},
// Mute or unmute a user.
muteUser(username) {
username = this.normalizeUsername(username);
@ -1298,6 +1304,16 @@ export default {
isMutedUser(username) {
return this.muted[this.normalizeUsername(username)] != undefined;
},
isBlockedUser(username) {
if (this.config.CachedBlocklist.length > 0) {
for (let user of this.config.CachedBlocklist) {
if (user === username) {
return true;
}
}
}
return false;
},
bulkMuteUsers() {
// On page load, if the website sent you a CachedBlocklist, mute all
// of these users in bulk when the server connects.
@ -1487,6 +1503,7 @@ export default {
onWatch: this.onWatch,
onUnwatch: this.onUnwatch,
onBlock: this.onBlock,
onCut: this.onCut,
bulkMuteUsers: this.bulkMuteUsers,
focusMessageBox: () => {
@ -4154,7 +4171,7 @@ export default {
:class="{
'is-outlined is-dark': !webcam.nsfw,
'is-danger': webcam.nsfw
}" @click.prevent="webcam.nsfw = !webcam.nsfw; sendMe()"
}" @click.prevent="webcam.nsfw = !webcam.nsfw"
title="Toggle the NSFW setting for your camera broadcast">
<i class="fa fa-fire mr-1" :class="{ 'has-text-danger': !webcam.nsfw }"></i> Explicit
</button>
@ -4618,6 +4635,7 @@ export default {
:website-url="config.website"
:is-dnd="isUsernameDND(u.username)"
:is-muted="isMutedUser(u.username)"
:is-blocked="isBlockedUser(u.username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"
@ -4642,6 +4660,7 @@ export default {
:website-url="config.website"
:is-dnd="isUsernameDND(username)"
:is-muted="isMutedUser(username)"
:is-blocked="isBlockedUser(u.username)"
:is-booted="isBooted(u.username)"
:is-op="isOp"
:is-video-not-allowed="isVideoNotAllowed(u)"

View File

@ -63,6 +63,10 @@ export default {
}
return false;
},
isOnCamera() {
// User's camera is enabled.
return (this.user.video & VideoFlag.Active);
},
},
methods: {
refresh() {
@ -134,6 +138,11 @@ export default {
// and we can't follow the current value.
this.cancel();
},
cutCamera() {
if (!window.confirm("Make this user stop broadcasting their camera?")) return;
this.$emit('send-command', `/cut ${this.user.username}`);
this.cancel();
},
kickUser() {
if (!window.confirm("Really kick this user from the chat room?")) return;
this.$emit('send-command', `/kick ${this.user.username}`);
@ -262,13 +271,19 @@ export default {
type="button"
class="button is-small is-outlined is-danger has-text-dark px-2 mr-1 mb-1"
@click="markNsfw()" title="Mark their camera as Explicit (red).">
<i class="fa fa-video mr-1" :class="{
'has-text-success': isMuted,
'has-text-danger': !isMuted
}"></i>
<i class="fa fa-video mr-1 has-text-danger"></i>
Mark camera as Explicit
</button>
<!-- Cut camera -->
<button v-if="isOnCamera"
type="button"
class="button is-small is-outlined is-danger has-text-dark px-2 mr-1 mb-1"
@click="cutCamera()" title="Turn their camera off.">
<i class="fa fa-stop mr-1 has-text-danger"></i>
Cut camera
</button>
<!-- Kick user -->
<button type="button"
class="button is-small is-outlined is-danger has-text-dark px-2 mr-1 mb-1"
@ -282,7 +297,7 @@ export default {
class="button is-small is-outlined is-danger has-text-dark px-2 mb-1"
@click="banUser()" title="Ban this user from the chat room for 24 hours.">
<i class="fa fa-clock mr-1 has-text-danger"></i>
Ban user (temporary)
Ban from chat
</button>
</div>
</div>

View File

@ -8,6 +8,7 @@ export default {
websiteUrl: String, // Base URL to website (for profile/avatar URLs)
isDnd: Boolean, // user is not accepting DMs
isMuted: Boolean, // user is muted by current user
isBlocked: Boolean, // user is blocked on your main website (can't be unmuted)
isBooted: Boolean, // user is booted by current user
vipConfig: Object, // VIP config settings for BareRTC
isOp: Boolean, // current user is operator (can always DM)
@ -201,16 +202,16 @@ export default {
</button>
<!-- Unmute User button (if muted) -->
<button type="button" v-if="isMuted" class="button is-small px-2 py-1"
<button type="button" v-if="isMuted && !isBlocked" class="button is-small px-2 py-1"
@click="muteUser()" title="This user is muted. Click to unmute them.">
<i class="fa fa-comment-slash has-text-danger"></i>
</button>
<!-- DM button (if not muted) -->
<button type="button" v-else class="button is-small px-2 py-1" @click="openDMs(u)"
:disabled="user.username === username || (user.dnd && !isOp)"
:title="user.dnd ? 'This person is not accepting new DMs' : 'Send a Direct Message'">
<i class="fa" :class="{ 'fa-comment': !user.dnd, 'fa-comment-slash': user.dnd }"></i>
:disabled="user.username === username || (user.dnd && !isOp) || (isBlocked && !isOp)"
:title="(user.dnd || isBlocked) ? 'This person is not accepting new DMs' : 'Send a Direct Message'">
<i class="fa" :class="{ 'fa-comment': !(user.dnd || isBlocked), 'fa-comment-slash': user.dnd || isBlocked }"></i>
</button>
<!-- Video button -->

View File

@ -29,6 +29,7 @@ class ChatClient {
onWatch,
onUnwatch,
onBlock,
onCut,
// Misc function registrations for callback.
onNewJWT, // new JWT token from ping response
@ -59,6 +60,7 @@ class ChatClient {
this.onWatch = onWatch;
this.onUnwatch = onUnwatch;
this.onBlock = onBlock;
this.onCut = onCut;
this.onNewJWT = onNewJWT;
this.bulkMuteUsers = bulkMuteUsers;
@ -191,6 +193,9 @@ class ChatClient {
case "block":
this.onBlock(msg);
break;
case "cut":
this.onCut(msg);
break;
case "error":
this.pushHistory({
channel: msg.channel,
@ -269,7 +274,7 @@ class ChatClient {
}
});
conn.addEventListener("open", ev => {
conn.addEventListener("open", () => {
this.ws.connected = true;
this.ChatClient("Websocket connected!");