diff --git a/Protocol.md b/Protocol.md index 2b68b83..d51940d 100644 --- a/Protocol.md +++ b/Protocol.md @@ -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. diff --git a/README.md b/README.md index 93d6e8e..b6505da 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/client/client.go b/client/client.go index 52b4b0b..dc0127e 100644 --- a/client/client.go +++ b/client/client.go @@ -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: diff --git a/client/handlers.go b/client/handlers.go index acce505..15f2511 100644 --- a/client/handlers.go +++ b/client/handlers.go @@ -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", + }) } diff --git a/docs/Configuration.md b/docs/Configuration.md index 69e1b17..d1ecf87 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -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 = "VIP Members" @@ -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. \ No newline at end of file diff --git a/pkg/commands.go b/pkg/commands.go index 5fd2689..d5ccd3c 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -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 ` to kick from chat\n" + "* `/ban ` to ban from chat (default duration is 24 (hours))\n" + "* `/unban ` to list the ban on a user\n" + "* `/bans` to list current banned users and their expiration date\n" + "* `/nsfw ` to mark their camera NSFW\n" + + "* `/cut ` 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 ` to grant operator rights to a user\n" + "* `/deop ` 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.

" // 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 { diff --git a/pkg/config/config.go b/pkg/config/config.go index 7f2758d..44359ab 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 +} diff --git a/pkg/handlers.go b/pkg/handlers.go index e8223d8..a8f6fb5 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -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 } diff --git a/pkg/messages/messages.go b/pkg/messages/messages.go index 3f813c8..9a131d7 100644 --- a/pkg/messages/messages.go +++ b/pkg/messages/messages.go @@ -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). diff --git a/pkg/websocket.go b/pkg/websocket.go index 42875a7..9c97d3b 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -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{ diff --git a/src/App.vue b/src/App.vue index 19db7ea..5f01464 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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"> Explicit @@ -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)" diff --git a/src/components/ProfileModal.vue b/src/components/ProfileModal.vue index f3d166f..17cc654 100644 --- a/src/components/ProfileModal.vue +++ b/src/components/ProfileModal.vue @@ -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)."> - + Mark camera as Explicit + + + diff --git a/src/components/WhoListRow.vue b/src/components/WhoListRow.vue index bd1019c..31f6991 100644 --- a/src/components/WhoListRow.vue +++ b/src/components/WhoListRow.vue @@ -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 { - diff --git a/src/lib/ChatClient.js b/src/lib/ChatClient.js index 08385a3..455e41d 100644 --- a/src/lib/ChatClient.js +++ b/src/lib/ChatClient.js @@ -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!");