NSFW Cameras and Moderator Commands
This commit is contained in:
parent
b966f85ecc
commit
15ebc42bd3
28
README.md
28
README.md
|
@ -32,6 +32,8 @@ Title = "BareRTC"
|
|||
Branding = "BareRTC"
|
||||
WebsiteURL = "https://www.example.com"
|
||||
UseXForwardedFor = true
|
||||
CORSHosts = ["https://www.example.com"]
|
||||
PermitNSFW = true
|
||||
|
||||
[JWT]
|
||||
Enabled = false
|
||||
|
@ -59,6 +61,8 @@ A description of the config directives includes:
|
|||
* The About page will link to your website.
|
||||
* If using [JWT authentication](#authentication), avatar and profile URLs may be relative (beginning with a "/") and will append to your website URL to safe space on the JWT token size!
|
||||
* **UseXForwardedFor**: set it to true and (for logging) the user's remote IP will use the X-Real-IP header or the first address in X-Forwarded-For. Set this if you run the app behind a proxy like nginx if you want IPs not to be all localhost.
|
||||
* **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`.
|
||||
* **PermitNSFW**: for user webcam streams, expressly permit "NSFW" content if the user opts in to mark their feed as such. Setting this will enable pop-up modals regarding NSFW video and give broadcasters an opt-in button, which will warn other users before they click in to watch.
|
||||
* **JWT**: settings for JWT [Authentication](#authentication).
|
||||
* Enabled (bool): activate the JWT token authentication feature.
|
||||
* Strict (bool): if true, **only** valid signed JWT tokens may log in. If false, users with no/invalid token can enter their own username without authentication.
|
||||
|
@ -175,6 +179,30 @@ This app is not designed to run without JWT authentication for users enabled. In
|
|||
|
||||
Note that they would not get past history of those DMs as this server only pushes _new_ messages to users after they connect.
|
||||
|
||||
# Moderator Commands
|
||||
|
||||
If you authenticate an Op user via JWT they can enter IRC-style chat commands to moderate the server. Current commands include:
|
||||
|
||||
* `/kick <username>` to disconnect a user's chat session.
|
||||
* `/nsfw <username>` to tag a user's video feed as NSFW (if your settings.toml has PermitNSFW enabled).
|
||||
|
||||
# JSON APIs
|
||||
|
||||
For better integration with your website, the chat server exposes some data via JSON APIs ready for cross-origin ajax requests. In your settings.toml set the `CORSHosts` to your list of website domains, such as "https://www.example.com", "http://localhost:8080" or so on.
|
||||
|
||||
Current API endpoints include:
|
||||
|
||||
* `GET /api/statistics`
|
||||
|
||||
Returns basic info about the count and usernames of connected chatters:
|
||||
|
||||
```json
|
||||
{
|
||||
"UserCount": 1,
|
||||
"Usernames": ["admin"]
|
||||
}
|
||||
```
|
||||
|
||||
# Deploying This App
|
||||
|
||||
It is recommended to use a reverse proxy such as nginx in front of this app. You will need to configure nginx to forward WebSocket related headers:
|
||||
|
|
65
pkg/commands.go
Normal file
65
pkg/commands.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
package barertc
|
||||
|
||||
import "strings"
|
||||
|
||||
// ProcessCommand parses a chat message for "/commands"
|
||||
func (s *Server) ProcessCommand(sub *Subscriber, msg Message) bool {
|
||||
if len(msg.Message) == 0 || msg.Message[0] != '/' {
|
||||
return false
|
||||
}
|
||||
|
||||
// Line begins with a slash, parse it apart.
|
||||
words := strings.Fields(msg.Message)
|
||||
if len(words) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Moderator commands.
|
||||
if sub.JWTClaims != nil && sub.JWTClaims.IsAdmin {
|
||||
switch words[0] {
|
||||
case "/kick":
|
||||
if len(words) == 1 {
|
||||
sub.ChatServer("Usage: `/kick username` to remove the user from the chat room.")
|
||||
}
|
||||
username := words[1]
|
||||
other, err := s.GetSubscriber(username)
|
||||
if err != nil {
|
||||
sub.ChatServer("/kick: username not found: %s", username)
|
||||
} else {
|
||||
other.ChatServer("You have been kicked from the chat room by %s", sub.Username)
|
||||
other.SendJSON(Message{
|
||||
Action: ActionKick,
|
||||
})
|
||||
s.DeleteSubscriber(other)
|
||||
sub.ChatServer("%s has been kicked from the room", username)
|
||||
}
|
||||
return true
|
||||
case "/nsfw":
|
||||
if len(words) == 1 {
|
||||
sub.ChatServer("Usage: `/nsfw username` to add the NSFW flag to their camera.")
|
||||
}
|
||||
username := words[1]
|
||||
other, err := s.GetSubscriber(username)
|
||||
if err != nil {
|
||||
sub.ChatServer("/nsfw: username not found: %s", username)
|
||||
} else {
|
||||
other.ChatServer("Your camera has been marked as NSFW by %s", sub.Username)
|
||||
other.VideoNSFW = true
|
||||
other.SendMe()
|
||||
s.SendWhoList()
|
||||
sub.ChatServer("%s has their camera marked as NSFW", username)
|
||||
}
|
||||
return true
|
||||
case "/help":
|
||||
sub.ChatServer(RenderMarkdown("Moderator commands are:\n\n" +
|
||||
"* `/kick <username>` to kick from chat\n" +
|
||||
"* `/nsfw <username>` to mark their camera NSFW\n" +
|
||||
"* `/help` to show this message",
|
||||
))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Not handled.
|
||||
return false
|
||||
}
|
|
@ -12,7 +12,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 = 2
|
||||
var currentVersion = 3
|
||||
|
||||
// Config for your BareRTC app.
|
||||
type Config struct {
|
||||
|
@ -27,7 +27,9 @@ type Config struct {
|
|||
Title string
|
||||
Branding string
|
||||
WebsiteURL string
|
||||
|
||||
CORSHosts []string
|
||||
PermitNSFW bool
|
||||
|
||||
UseXForwardedFor bool
|
||||
|
||||
|
|
|
@ -107,6 +107,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
return
|
||||
}
|
||||
|
||||
// Process commands.
|
||||
if handled := s.ProcessCommand(sub, msg); handled {
|
||||
return
|
||||
}
|
||||
|
||||
// Translate their message as Markdown syntax.
|
||||
markdown := RenderMarkdown(msg.Message)
|
||||
if markdown == "" {
|
||||
|
@ -115,11 +120,10 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
|||
|
||||
// Message to be echoed to the channel.
|
||||
var message = Message{
|
||||
Action: ActionMessage,
|
||||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
Message: markdown,
|
||||
Timestamp: time.Now(),
|
||||
Action: ActionMessage,
|
||||
Channel: msg.Channel,
|
||||
Username: sub.Username,
|
||||
Message: markdown,
|
||||
}
|
||||
|
||||
// Is this a DM?
|
||||
|
@ -144,6 +148,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
|||
}
|
||||
|
||||
sub.VideoActive = msg.VideoActive
|
||||
sub.VideoNSFW = msg.NSFW
|
||||
|
||||
// Sync the WhoList to everybody.
|
||||
s.SendWhoList()
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package barertc
|
||||
|
||||
import "time"
|
||||
|
||||
type Message struct {
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp time.Time `json:"at,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// JWT token for `login` actions.
|
||||
JWTToken string `json:"jwt,omitempty"`
|
||||
|
@ -17,6 +14,7 @@ type Message struct {
|
|||
|
||||
// Sent on `me` actions along with Username
|
||||
VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status
|
||||
NSFW bool `json:"nsfw,omitempty"` // user tags their video NSFW
|
||||
|
||||
// Sent on `open` actions along with the (other) Username.
|
||||
OpenSecret string `json:"openSecret,omitempty"`
|
||||
|
@ -40,9 +38,10 @@ const (
|
|||
|
||||
// Actions sent by server only
|
||||
ActionPing = "ping"
|
||||
ActionWhoList = "who" // server pushes the Who List
|
||||
ActionPresence = "presence" // a user joined or left the room
|
||||
ActionError = "error" // ChatServer errors
|
||||
ActionWhoList = "who" // server pushes the Who List
|
||||
ActionPresence = "presence" // a user joined or left the room
|
||||
ActionError = "error" // ChatServer errors
|
||||
ActionKick = "disconnect" // client should disconnect (e.g. have been kicked).
|
||||
|
||||
// WebRTC signaling messages.
|
||||
ActionCandidate = "candidate"
|
||||
|
@ -52,10 +51,11 @@ const (
|
|||
// WhoList is a member entry in the chat room.
|
||||
type WhoList struct {
|
||||
Username string `json:"username"`
|
||||
VideoActive bool `json:"videoActive"`
|
||||
VideoActive bool `json:"videoActive,omitempty"`
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
|
||||
// JWT auth extra settings.
|
||||
Operator bool `json:"op"`
|
||||
Avatar string `json:"avatar"`
|
||||
ProfileURL string `json:"profileURL"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
ProfileURL string `json:"profileURL,omitempty"`
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ func IndexPage() http.HandlerFunc {
|
|||
"AsHTML": func(v string) template.HTML {
|
||||
return template.HTML(v)
|
||||
},
|
||||
"AsJS": func(v interface{}) template.JS {
|
||||
return template.JS(fmt.Sprintf("%v", v))
|
||||
},
|
||||
})
|
||||
tmpl, err := tmpl.ParseFiles("web/templates/chat.html")
|
||||
if err != nil {
|
||||
|
|
|
@ -21,6 +21,7 @@ type Subscriber struct {
|
|||
ID int // ID assigned by server
|
||||
Username string
|
||||
VideoActive bool
|
||||
VideoNSFW bool
|
||||
JWTClaims *jwt.Claims
|
||||
authenticated bool // has passed the login step
|
||||
conn *websocket.Conn
|
||||
|
@ -160,10 +161,9 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
case timestamp := <-pinger.C:
|
||||
case <-pinger.C:
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionPing,
|
||||
Message: timestamp.Format(time.RFC3339),
|
||||
Action: ActionPing,
|
||||
})
|
||||
case <-ctx.Done():
|
||||
pinger.Stop()
|
||||
|
@ -254,11 +254,6 @@ func (s *Server) SendTo(username string, msg Message) error {
|
|||
s.subscribersMu.RLock()
|
||||
defer s.subscribersMu.RUnlock()
|
||||
|
||||
// If no timestamp, add it.
|
||||
if msg.Timestamp.IsZero() {
|
||||
msg.Timestamp = time.Now()
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, sub := range s.IterSubscribers(true) {
|
||||
if sub.Username == username {
|
||||
|
@ -293,6 +288,7 @@ func (s *Server) SendWhoList() {
|
|||
who := WhoList{
|
||||
Username: sub.Username,
|
||||
VideoActive: sub.VideoActive,
|
||||
NSFW: sub.VideoNSFW,
|
||||
}
|
||||
if sub.JWTClaims != nil {
|
||||
who.Operator = sub.JWTClaims.IsAdmin
|
||||
|
@ -304,9 +300,8 @@ func (s *Server) SendWhoList() {
|
|||
|
||||
for _, sub := range subscribers {
|
||||
sub.SendJSON(Message{
|
||||
Action: ActionWhoList,
|
||||
WhoList: users,
|
||||
Timestamp: time.Now(),
|
||||
Action: ActionWhoList,
|
||||
WhoList: users,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ body {
|
|||
|
||||
.video-feeds > .feed {
|
||||
position: relative;
|
||||
flex: 0 0 144px;
|
||||
flex: 0 0 168px;
|
||||
width: 168px;
|
||||
height: 112px;
|
||||
background-color: black;
|
||||
|
@ -156,21 +156,25 @@ body {
|
|||
}
|
||||
|
||||
.video-feeds.x1 > .feed {
|
||||
flex: 0 0 252px;
|
||||
width: 252px;
|
||||
height: 168px;
|
||||
}
|
||||
|
||||
.video-feeds.x2 > .feed {
|
||||
flex: 0 0 336px;
|
||||
width: 336px;
|
||||
height: 224px;
|
||||
}
|
||||
|
||||
.video-feeds.x3 > .feed {
|
||||
flex: 0 0 504px;
|
||||
width: 504px;
|
||||
height: 336px;
|
||||
}
|
||||
|
||||
.video-feeds.x4 > .feed {
|
||||
flex: 0 0 672px;
|
||||
width: 672px;
|
||||
height: 448px;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ const app = Vue.createApp({
|
|||
data() {
|
||||
return {
|
||||
// busy: false, // TODO: not used
|
||||
disconnect: false, // don't try to reconnect (e.g. kicked)
|
||||
windowFocused: true, // browser tab is active
|
||||
windowFocusedAt: new Date(),
|
||||
|
||||
|
@ -20,6 +21,7 @@ const app = Vue.createApp({
|
|||
config: {
|
||||
channels: PublicChannels,
|
||||
website: WebsiteURL,
|
||||
permitNSFW: PermitNSFW,
|
||||
sounds: {
|
||||
available: SoundEffects,
|
||||
settings: DefaultSounds,
|
||||
|
@ -58,6 +60,7 @@ const app = Vue.createApp({
|
|||
elem: null, // <video id="localVideo"> element
|
||||
stream: null, // MediaStream object
|
||||
muted: false, // our outgoing mic is muted, not by default
|
||||
nsfw: false, // user has flagged their camera to be NSFW
|
||||
|
||||
// Who all is watching me? map of users.
|
||||
watching: {},
|
||||
|
@ -121,6 +124,16 @@ const app = Vue.createApp({
|
|||
settingsModal: {
|
||||
visible: false,
|
||||
},
|
||||
|
||||
nsfwModalCast: {
|
||||
visible: false,
|
||||
},
|
||||
|
||||
nsfwModalView: {
|
||||
visible: false,
|
||||
dontShowAgain: false,
|
||||
user: null, // staged User we wanted to open
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -258,6 +271,7 @@ const app = Vue.createApp({
|
|||
this.ws.conn.send(JSON.stringify({
|
||||
action: "me",
|
||||
videoActive: this.webcam.active,
|
||||
nsfw: this.webcam.nsfw,
|
||||
}));
|
||||
},
|
||||
onMe(msg) {
|
||||
|
@ -268,6 +282,11 @@ const app = Vue.createApp({
|
|||
this.username = msg.username;
|
||||
}
|
||||
|
||||
// The server can set our webcam NSFW flag.
|
||||
if (this.webcam.nsfw != msg.nsfw) {
|
||||
this.webcam.nsfw = msg.nsfw;
|
||||
}
|
||||
|
||||
// this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
||||
},
|
||||
|
||||
|
@ -386,9 +405,11 @@ const app = Vue.createApp({
|
|||
this.ws.connected = false;
|
||||
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||
|
||||
if (ev.code !== 1001) {
|
||||
this.ChatClient("Reconnecting in 5s");
|
||||
setTimeout(this.dial, 5000);
|
||||
if (!this.disconnect) {
|
||||
if (ev.code !== 1001) {
|
||||
this.ChatClient("Reconnecting in 5s");
|
||||
setTimeout(this.dial, 5000);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -457,6 +478,9 @@ const app = Vue.createApp({
|
|||
isChatServer: true,
|
||||
});
|
||||
break;
|
||||
case "disconnect":
|
||||
this.disconnect = true;
|
||||
break;
|
||||
case "ping":
|
||||
break;
|
||||
default:
|
||||
|
@ -755,10 +779,16 @@ const app = Vue.createApp({
|
|||
},
|
||||
|
||||
// Start broadcasting my webcam.
|
||||
startVideo() {
|
||||
startVideo(force) {
|
||||
if (this.webcam.busy) return;
|
||||
this.webcam.busy = true;
|
||||
|
||||
// If we are running in PermitNSFW mode, show the user the modal.
|
||||
if (this.config.permitNSFW && !force) {
|
||||
this.nsfwModalCast.visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.webcam.busy = true;
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
video: true,
|
||||
|
@ -777,12 +807,24 @@ const app = Vue.createApp({
|
|||
},
|
||||
|
||||
// Begin connecting to someone else's webcam.
|
||||
openVideo(user) {
|
||||
openVideo(user, force) {
|
||||
if (user.username === this.username) {
|
||||
this.ChatClient("You can already see your own webcam.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the target user NSFW? Go thru the modal.
|
||||
let dontShowAgain = localStorage["skip-nsfw-modal"] == "true";
|
||||
if (user.nsfw && !dontShowAgain && !force) {
|
||||
this.nsfwModalView.user = user;
|
||||
this.nsfwModalView.visible = true;
|
||||
return;
|
||||
}
|
||||
if (this.nsfwModalView.dontShowAgain) {
|
||||
// user doesn't want to see the modal again.
|
||||
localStorage["skip-nsfw-modal"] = "true";
|
||||
}
|
||||
|
||||
// Camera is already open? Then disconnect the connection.
|
||||
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) {
|
||||
this.closeVideo(user.username, "offerer");
|
||||
|
|
|
@ -176,6 +176,89 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NSFW Modal: before user activates their webcam -->
|
||||
<div class="modal" :class="{'is-active': nsfwModalCast.visible}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-info">
|
||||
<p class="card-header-title has-text-light">Broadcast my webcam</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<p class="block">
|
||||
You can turn on your webcam and enable others in the room to connect to yours.
|
||||
The controls to stop, <i class="fa fa-microphone-slash"></i> mute audio, and
|
||||
<i class="fa fa-eye"></i> see who is watching will be at the top of the page.
|
||||
</p>
|
||||
|
||||
<p class="block">
|
||||
If your camera will be featuring "<abbr title="Not Safe For Work">NSFW</abbr>" or sexual content, please mark it as such by
|
||||
clicking on the <i class="fa fa-fire has-text-danger"></i> button or checking the box below to start with it enabled.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
v-model="webcam.nsfw">
|
||||
Check this box if your webcam will <em>definitely</em> be NSFW. 😈
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control has-text-centered">
|
||||
<button type="button"
|
||||
class="button is-link mr-4"
|
||||
@click="startVideo(true); nsfwModalCast.visible=false">Start webcam</button>
|
||||
<button type="button"
|
||||
class="button"
|
||||
@click="nsfwModalCast.visible=false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NSFW Modal: before user views a NSFW camera the first time -->
|
||||
<div class="modal" :class="{'is-active': nsfwModalView.visible}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-content">
|
||||
<div class="card">
|
||||
<header class="card-header has-background-info">
|
||||
<p class="card-header-title has-text-light">This camera may contain NSFW content</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<p class="block">
|
||||
This camera has been marked as "<abbr title="Not Safe For Work">NSFW</abbr>" and may
|
||||
contain displays of sexuality. If you do not want to see this, look for cameras with
|
||||
a <span class="button is-small is-info is-outlined px-1"><i class="fa fa-video"></i></span>
|
||||
blue icon rather than the <span class="button is-small is-danger is-outlined px-1"><i class="fa fa-video"></i></span>
|
||||
red ones.
|
||||
</p>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox"
|
||||
v-model="nsfwModalView.dontShowAgain">
|
||||
Don't show this message again
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="control has-text-centered">
|
||||
<button type="button"
|
||||
class="button is-link mr-4"
|
||||
@click="openVideo(nsfwModalView.user, true); nsfwModalView.visible=false">Open webcam</button>
|
||||
<button type="button"
|
||||
class="button"
|
||||
@click="nsfwModalView.visible=false">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
|
||||
<!-- Top header panel -->
|
||||
|
@ -188,18 +271,18 @@
|
|||
<!-- Stop/Start video buttons -->
|
||||
<button type="button"
|
||||
v-if="webcam.active"
|
||||
class="button is-small is-danger"
|
||||
class="button is-small is-danger px-1"
|
||||
@click="stopVideo()">
|
||||
<i class="fa fa-camera mr-2"></i>
|
||||
<i class="fa fa-stop mr-2"></i>
|
||||
Stop
|
||||
</button>
|
||||
<button type="button"
|
||||
v-else
|
||||
class="button is-small is-success"
|
||||
class="button is-small is-success px-1"
|
||||
@click="startVideo()"
|
||||
:disabled="webcam.busy">
|
||||
<i class="fa fa-camera mr-2"></i>
|
||||
Start
|
||||
<i class="fa fa-video mr-2"></i>
|
||||
Share webcam
|
||||
</button>
|
||||
|
||||
<!-- Mute/Unmute my mic buttons (if streaming)-->
|
||||
|
@ -226,6 +309,17 @@
|
|||
<i class="fa fa-eye mr-2"></i>
|
||||
[[Object.keys(webcam.watching).length]]
|
||||
</button>
|
||||
|
||||
<!-- NSFW toggle button -->
|
||||
<button type="button"
|
||||
v-if="webcam.active && config.permitNSFW"
|
||||
class="button is-small px-1 ml-1"
|
||||
:class="{'is-outlined is-dark': !webcam.nsfw,
|
||||
'is-danger': webcam.nsfw}"
|
||||
@click.prevent="webcam.nsfw = !webcam.nsfw; sendMe()"
|
||||
title="Toggle the NSFW setting for your camera broadcast">
|
||||
<i class="fa fa-fire"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="column is-narrow pl-1">
|
||||
<a href="/about" target="_blank" class="button is-small is-link px-2">
|
||||
|
@ -354,8 +448,8 @@
|
|||
|
||||
<!-- My video -->
|
||||
<video class="feed"
|
||||
v-show="webcam.active"
|
||||
id="localVideo"
|
||||
v-show="webcam.active"
|
||||
autoplay muted>
|
||||
</video>
|
||||
|
||||
|
@ -536,6 +630,10 @@
|
|||
<!-- Video button -->
|
||||
<button type="button" class="button is-small px-2 py-1"
|
||||
:disabled="!u.videoActive"
|
||||
:class="{
|
||||
'is-danger is-outlined': u.videoActive && u.nsfw,
|
||||
'is-info is-outlined': u.videoActive && !u.nsfw,
|
||||
}"
|
||||
title="Open video stream"
|
||||
@click="openVideo(u)">
|
||||
<i class="fa fa-video"></i>
|
||||
|
@ -555,6 +653,7 @@
|
|||
<script type="text/javascript">
|
||||
const PublicChannels = {{.Config.GetChannels}};
|
||||
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||
const UserJWTToken = {{.JWTTokenString}};
|
||||
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
||||
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||
|
|
Loading…
Reference in New Issue
Block a user