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"
|
Branding = "BareRTC"
|
||||||
WebsiteURL = "https://www.example.com"
|
WebsiteURL = "https://www.example.com"
|
||||||
UseXForwardedFor = true
|
UseXForwardedFor = true
|
||||||
|
CORSHosts = ["https://www.example.com"]
|
||||||
|
PermitNSFW = true
|
||||||
|
|
||||||
[JWT]
|
[JWT]
|
||||||
Enabled = false
|
Enabled = false
|
||||||
|
@ -59,6 +61,8 @@ A description of the config directives includes:
|
||||||
* The About page will link to your website.
|
* 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!
|
* 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.
|
* **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).
|
* **JWT**: settings for JWT [Authentication](#authentication).
|
||||||
* Enabled (bool): activate the JWT token authentication feature.
|
* 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.
|
* 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.
|
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
|
# 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:
|
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
|
// Version of the config format - when new fields are added, it will attempt
|
||||||
// to write the settings.toml to disk so new defaults populate.
|
// to write the settings.toml to disk so new defaults populate.
|
||||||
var currentVersion = 2
|
var currentVersion = 3
|
||||||
|
|
||||||
// Config for your BareRTC app.
|
// Config for your BareRTC app.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -27,7 +27,9 @@ type Config struct {
|
||||||
Title string
|
Title string
|
||||||
Branding string
|
Branding string
|
||||||
WebsiteURL string
|
WebsiteURL string
|
||||||
|
|
||||||
CORSHosts []string
|
CORSHosts []string
|
||||||
|
PermitNSFW bool
|
||||||
|
|
||||||
UseXForwardedFor bool
|
UseXForwardedFor bool
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process commands.
|
||||||
|
if handled := s.ProcessCommand(sub, msg); handled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Translate their message as Markdown syntax.
|
// Translate their message as Markdown syntax.
|
||||||
markdown := RenderMarkdown(msg.Message)
|
markdown := RenderMarkdown(msg.Message)
|
||||||
if markdown == "" {
|
if markdown == "" {
|
||||||
|
@ -119,7 +124,6 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
|
||||||
Channel: msg.Channel,
|
Channel: msg.Channel,
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
Message: markdown,
|
Message: markdown,
|
||||||
Timestamp: time.Now(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this a DM?
|
// Is this a DM?
|
||||||
|
@ -144,6 +148,7 @@ func (s *Server) OnMe(sub *Subscriber, msg Message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sub.VideoActive = msg.VideoActive
|
sub.VideoActive = msg.VideoActive
|
||||||
|
sub.VideoNSFW = msg.NSFW
|
||||||
|
|
||||||
// Sync the WhoList to everybody.
|
// Sync the WhoList to everybody.
|
||||||
s.SendWhoList()
|
s.SendWhoList()
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
package barertc
|
package barertc
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Timestamp time.Time `json:"at,omitempty"`
|
|
||||||
|
|
||||||
// JWT token for `login` actions.
|
// JWT token for `login` actions.
|
||||||
JWTToken string `json:"jwt,omitempty"`
|
JWTToken string `json:"jwt,omitempty"`
|
||||||
|
@ -17,6 +14,7 @@ type Message struct {
|
||||||
|
|
||||||
// Sent on `me` actions along with Username
|
// Sent on `me` actions along with Username
|
||||||
VideoActive bool `json:"videoActive,omitempty"` // user tells us their cam status
|
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.
|
// Sent on `open` actions along with the (other) Username.
|
||||||
OpenSecret string `json:"openSecret,omitempty"`
|
OpenSecret string `json:"openSecret,omitempty"`
|
||||||
|
@ -43,6 +41,7 @@ const (
|
||||||
ActionWhoList = "who" // server pushes the Who List
|
ActionWhoList = "who" // server pushes the Who List
|
||||||
ActionPresence = "presence" // a user joined or left the room
|
ActionPresence = "presence" // a user joined or left the room
|
||||||
ActionError = "error" // ChatServer errors
|
ActionError = "error" // ChatServer errors
|
||||||
|
ActionKick = "disconnect" // client should disconnect (e.g. have been kicked).
|
||||||
|
|
||||||
// WebRTC signaling messages.
|
// WebRTC signaling messages.
|
||||||
ActionCandidate = "candidate"
|
ActionCandidate = "candidate"
|
||||||
|
@ -52,10 +51,11 @@ const (
|
||||||
// WhoList is a member entry in the chat room.
|
// WhoList is a member entry in the chat room.
|
||||||
type WhoList struct {
|
type WhoList struct {
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
VideoActive bool `json:"videoActive"`
|
VideoActive bool `json:"videoActive,omitempty"`
|
||||||
|
NSFW bool `json:"nsfw,omitempty"`
|
||||||
|
|
||||||
// JWT auth extra settings.
|
// JWT auth extra settings.
|
||||||
Operator bool `json:"op"`
|
Operator bool `json:"op"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar,omitempty"`
|
||||||
ProfileURL string `json:"profileURL"`
|
ProfileURL string `json:"profileURL,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,9 @@ func IndexPage() http.HandlerFunc {
|
||||||
"AsHTML": func(v string) template.HTML {
|
"AsHTML": func(v string) template.HTML {
|
||||||
return template.HTML(v)
|
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")
|
tmpl, err := tmpl.ParseFiles("web/templates/chat.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Subscriber struct {
|
||||||
ID int // ID assigned by server
|
ID int // ID assigned by server
|
||||||
Username string
|
Username string
|
||||||
VideoActive bool
|
VideoActive bool
|
||||||
|
VideoNSFW bool
|
||||||
JWTClaims *jwt.Claims
|
JWTClaims *jwt.Claims
|
||||||
authenticated bool // has passed the login step
|
authenticated bool // has passed the login step
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
|
@ -160,10 +161,9 @@ func (s *Server) WebSocket() http.HandlerFunc {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case timestamp := <-pinger.C:
|
case <-pinger.C:
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
Action: ActionPing,
|
Action: ActionPing,
|
||||||
Message: timestamp.Format(time.RFC3339),
|
|
||||||
})
|
})
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
pinger.Stop()
|
pinger.Stop()
|
||||||
|
@ -254,11 +254,6 @@ func (s *Server) SendTo(username string, msg Message) error {
|
||||||
s.subscribersMu.RLock()
|
s.subscribersMu.RLock()
|
||||||
defer s.subscribersMu.RUnlock()
|
defer s.subscribersMu.RUnlock()
|
||||||
|
|
||||||
// If no timestamp, add it.
|
|
||||||
if msg.Timestamp.IsZero() {
|
|
||||||
msg.Timestamp = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
var found bool
|
var found bool
|
||||||
for _, sub := range s.IterSubscribers(true) {
|
for _, sub := range s.IterSubscribers(true) {
|
||||||
if sub.Username == username {
|
if sub.Username == username {
|
||||||
|
@ -293,6 +288,7 @@ func (s *Server) SendWhoList() {
|
||||||
who := WhoList{
|
who := WhoList{
|
||||||
Username: sub.Username,
|
Username: sub.Username,
|
||||||
VideoActive: sub.VideoActive,
|
VideoActive: sub.VideoActive,
|
||||||
|
NSFW: sub.VideoNSFW,
|
||||||
}
|
}
|
||||||
if sub.JWTClaims != nil {
|
if sub.JWTClaims != nil {
|
||||||
who.Operator = sub.JWTClaims.IsAdmin
|
who.Operator = sub.JWTClaims.IsAdmin
|
||||||
|
@ -306,7 +302,6 @@ func (s *Server) SendWhoList() {
|
||||||
sub.SendJSON(Message{
|
sub.SendJSON(Message{
|
||||||
Action: ActionWhoList,
|
Action: ActionWhoList,
|
||||||
WhoList: users,
|
WhoList: users,
|
||||||
Timestamp: time.Now(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@ body {
|
||||||
|
|
||||||
.video-feeds > .feed {
|
.video-feeds > .feed {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 144px;
|
flex: 0 0 168px;
|
||||||
width: 168px;
|
width: 168px;
|
||||||
height: 112px;
|
height: 112px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
@ -156,21 +156,25 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-feeds.x1 > .feed {
|
.video-feeds.x1 > .feed {
|
||||||
|
flex: 0 0 252px;
|
||||||
width: 252px;
|
width: 252px;
|
||||||
height: 168px;
|
height: 168px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-feeds.x2 > .feed {
|
.video-feeds.x2 > .feed {
|
||||||
|
flex: 0 0 336px;
|
||||||
width: 336px;
|
width: 336px;
|
||||||
height: 224px;
|
height: 224px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-feeds.x3 > .feed {
|
.video-feeds.x3 > .feed {
|
||||||
|
flex: 0 0 504px;
|
||||||
width: 504px;
|
width: 504px;
|
||||||
height: 336px;
|
height: 336px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-feeds.x4 > .feed {
|
.video-feeds.x4 > .feed {
|
||||||
|
flex: 0 0 672px;
|
||||||
width: 672px;
|
width: 672px;
|
||||||
height: 448px;
|
height: 448px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ const app = Vue.createApp({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// busy: false, // TODO: not used
|
// busy: false, // TODO: not used
|
||||||
|
disconnect: false, // don't try to reconnect (e.g. kicked)
|
||||||
windowFocused: true, // browser tab is active
|
windowFocused: true, // browser tab is active
|
||||||
windowFocusedAt: new Date(),
|
windowFocusedAt: new Date(),
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ const app = Vue.createApp({
|
||||||
config: {
|
config: {
|
||||||
channels: PublicChannels,
|
channels: PublicChannels,
|
||||||
website: WebsiteURL,
|
website: WebsiteURL,
|
||||||
|
permitNSFW: PermitNSFW,
|
||||||
sounds: {
|
sounds: {
|
||||||
available: SoundEffects,
|
available: SoundEffects,
|
||||||
settings: DefaultSounds,
|
settings: DefaultSounds,
|
||||||
|
@ -58,6 +60,7 @@ const app = Vue.createApp({
|
||||||
elem: null, // <video id="localVideo"> element
|
elem: null, // <video id="localVideo"> element
|
||||||
stream: null, // MediaStream object
|
stream: null, // MediaStream object
|
||||||
muted: false, // our outgoing mic is muted, not by default
|
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.
|
// Who all is watching me? map of users.
|
||||||
watching: {},
|
watching: {},
|
||||||
|
@ -121,6 +124,16 @@ const app = Vue.createApp({
|
||||||
settingsModal: {
|
settingsModal: {
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
nsfwModalCast: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
nsfwModalView: {
|
||||||
|
visible: false,
|
||||||
|
dontShowAgain: false,
|
||||||
|
user: null, // staged User we wanted to open
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -258,6 +271,7 @@ const app = Vue.createApp({
|
||||||
this.ws.conn.send(JSON.stringify({
|
this.ws.conn.send(JSON.stringify({
|
||||||
action: "me",
|
action: "me",
|
||||||
videoActive: this.webcam.active,
|
videoActive: this.webcam.active,
|
||||||
|
nsfw: this.webcam.nsfw,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
onMe(msg) {
|
onMe(msg) {
|
||||||
|
@ -268,6 +282,11 @@ const app = Vue.createApp({
|
||||||
this.username = msg.username;
|
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)}`);
|
// this.ChatClient(`User sync from backend: ${JSON.stringify(msg)}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -386,10 +405,12 @@ const app = Vue.createApp({
|
||||||
this.ws.connected = false;
|
this.ws.connected = false;
|
||||||
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
|
||||||
|
|
||||||
|
if (!this.disconnect) {
|
||||||
if (ev.code !== 1001) {
|
if (ev.code !== 1001) {
|
||||||
this.ChatClient("Reconnecting in 5s");
|
this.ChatClient("Reconnecting in 5s");
|
||||||
setTimeout(this.dial, 5000);
|
setTimeout(this.dial, 5000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
conn.addEventListener("open", ev => {
|
conn.addEventListener("open", ev => {
|
||||||
|
@ -457,6 +478,9 @@ const app = Vue.createApp({
|
||||||
isChatServer: true,
|
isChatServer: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "disconnect":
|
||||||
|
this.disconnect = true;
|
||||||
|
break;
|
||||||
case "ping":
|
case "ping":
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -755,10 +779,16 @@ const app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Start broadcasting my webcam.
|
// Start broadcasting my webcam.
|
||||||
startVideo() {
|
startVideo(force) {
|
||||||
if (this.webcam.busy) return;
|
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({
|
navigator.mediaDevices.getUserMedia({
|
||||||
audio: true,
|
audio: true,
|
||||||
video: true,
|
video: true,
|
||||||
|
@ -777,12 +807,24 @@ const app = Vue.createApp({
|
||||||
},
|
},
|
||||||
|
|
||||||
// Begin connecting to someone else's webcam.
|
// Begin connecting to someone else's webcam.
|
||||||
openVideo(user) {
|
openVideo(user, force) {
|
||||||
if (user.username === this.username) {
|
if (user.username === this.username) {
|
||||||
this.ChatClient("You can already see your own webcam.");
|
this.ChatClient("You can already see your own webcam.");
|
||||||
return;
|
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.
|
// Camera is already open? Then disconnect the connection.
|
||||||
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) {
|
if (this.WebRTC.pc[user.username] != undefined && this.WebRTC.pc[user.username].offerer != undefined) {
|
||||||
this.closeVideo(user.username, "offerer");
|
this.closeVideo(user.username, "offerer");
|
||||||
|
|
|
@ -176,6 +176,89 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="chat-container">
|
||||||
|
|
||||||
<!-- Top header panel -->
|
<!-- Top header panel -->
|
||||||
|
@ -188,18 +271,18 @@
|
||||||
<!-- Stop/Start video buttons -->
|
<!-- Stop/Start video buttons -->
|
||||||
<button type="button"
|
<button type="button"
|
||||||
v-if="webcam.active"
|
v-if="webcam.active"
|
||||||
class="button is-small is-danger"
|
class="button is-small is-danger px-1"
|
||||||
@click="stopVideo()">
|
@click="stopVideo()">
|
||||||
<i class="fa fa-camera mr-2"></i>
|
<i class="fa fa-stop mr-2"></i>
|
||||||
Stop
|
Stop
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
v-else
|
v-else
|
||||||
class="button is-small is-success"
|
class="button is-small is-success px-1"
|
||||||
@click="startVideo()"
|
@click="startVideo()"
|
||||||
:disabled="webcam.busy">
|
:disabled="webcam.busy">
|
||||||
<i class="fa fa-camera mr-2"></i>
|
<i class="fa fa-video mr-2"></i>
|
||||||
Start
|
Share webcam
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Mute/Unmute my mic buttons (if streaming)-->
|
<!-- Mute/Unmute my mic buttons (if streaming)-->
|
||||||
|
@ -226,6 +309,17 @@
|
||||||
<i class="fa fa-eye mr-2"></i>
|
<i class="fa fa-eye mr-2"></i>
|
||||||
[[Object.keys(webcam.watching).length]]
|
[[Object.keys(webcam.watching).length]]
|
||||||
</button>
|
</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>
|
||||||
<div class="column is-narrow pl-1">
|
<div class="column is-narrow pl-1">
|
||||||
<a href="/about" target="_blank" class="button is-small is-link px-2">
|
<a href="/about" target="_blank" class="button is-small is-link px-2">
|
||||||
|
@ -354,8 +448,8 @@
|
||||||
|
|
||||||
<!-- My video -->
|
<!-- My video -->
|
||||||
<video class="feed"
|
<video class="feed"
|
||||||
v-show="webcam.active"
|
|
||||||
id="localVideo"
|
id="localVideo"
|
||||||
|
v-show="webcam.active"
|
||||||
autoplay muted>
|
autoplay muted>
|
||||||
</video>
|
</video>
|
||||||
|
|
||||||
|
@ -536,6 +630,10 @@
|
||||||
<!-- Video button -->
|
<!-- Video button -->
|
||||||
<button type="button" class="button is-small px-2 py-1"
|
<button type="button" class="button is-small px-2 py-1"
|
||||||
:disabled="!u.videoActive"
|
:disabled="!u.videoActive"
|
||||||
|
:class="{
|
||||||
|
'is-danger is-outlined': u.videoActive && u.nsfw,
|
||||||
|
'is-info is-outlined': u.videoActive && !u.nsfw,
|
||||||
|
}"
|
||||||
title="Open video stream"
|
title="Open video stream"
|
||||||
@click="openVideo(u)">
|
@click="openVideo(u)">
|
||||||
<i class="fa fa-video"></i>
|
<i class="fa fa-video"></i>
|
||||||
|
@ -555,6 +653,7 @@
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
const PublicChannels = {{.Config.GetChannels}};
|
const PublicChannels = {{.Config.GetChannels}};
|
||||||
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
const WebsiteURL = "{{.Config.WebsiteURL}}";
|
||||||
|
const PermitNSFW = {{AsJS .Config.PermitNSFW}};
|
||||||
const UserJWTToken = {{.JWTTokenString}};
|
const UserJWTToken = {{.JWTTokenString}};
|
||||||
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
const UserJWTValid = {{if .JWTAuthOK}}true{{else}}false{{end}};
|
||||||
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
const UserJWTClaims = {{.JWTClaims.ToJSON}};
|
||||||
|
|
Loading…
Reference in New Issue
Block a user