diff --git a/pkg/handlers.go b/pkg/handlers.go
index 0345e81..b08306e 100644
--- a/pkg/handlers.go
+++ b/pkg/handlers.go
@@ -133,6 +133,20 @@ func (s *Server) OnMessage(sub *Subscriber, msg Message) {
// Echo the message only to both parties.
s.SendTo(sub.Username, message)
message.Channel = "@" + sub.Username
+
+ // Don't deliver it if the receiver has muted us.
+ rcpt, err := s.GetSubscriber(strings.TrimPrefix(msg.Channel, "@"))
+ if err == nil && rcpt.Mutes(sub.Username) {
+ log.Debug("Do not send message to %s: they have muted or booted %s", rcpt.Username, sub.Username)
+ return
+ }
+
+ // If the sender already mutes the recipient, reply back with the error.
+ if sub.Mutes(rcpt.Username) {
+ sub.ChatServer("You have muted %s and so your message has not been sent.", rcpt.Username)
+ return
+ }
+
if err := s.SendTo(msg.Channel, message); err != nil {
sub.ChatServer("Your message could not be delivered: %s", err)
}
@@ -191,6 +205,20 @@ func (s *Server) OnFile(sub *Subscriber, msg Message) {
// Echo the message only to both parties.
s.SendTo(sub.Username, message)
message.Channel = "@" + sub.Username
+
+ // Don't deliver it if the receiver has muted us.
+ rcpt, err := s.GetSubscriber(strings.TrimPrefix(msg.Channel, "@"))
+ if err == nil && rcpt.Mutes(sub.Username) {
+ log.Debug("Do not send message to %s: they have muted or booted %s", rcpt.Username, sub.Username)
+ return
+ }
+
+ // If the sender already mutes the recipient, reply back with the error.
+ if sub.Mutes(rcpt.Username) {
+ sub.ChatServer("You have muted %s and so your message has not been sent.", rcpt.Username)
+ return
+ }
+
if err := s.SendTo(msg.Channel, message); err != nil {
sub.ChatServer("Your message could not be delivered: %s", err)
}
@@ -242,6 +270,35 @@ func (s *Server) OnOpen(sub *Subscriber, msg Message) {
})
}
+// OnBoot is a user kicking you off their video stream.
+func (s *Server) OnBoot(sub *Subscriber, msg Message) {
+ log.Info("%s boots %s off their camera", sub.Username, msg.Username)
+
+ sub.muteMu.Lock()
+ sub.booted[msg.Username] = struct{}{}
+ sub.muteMu.Unlock()
+
+ s.SendWhoList()
+}
+
+// OnMute is a user kicking setting the mute flag for another user.
+func (s *Server) OnMute(sub *Subscriber, msg Message, mute bool) {
+ log.Info("%s mutes or unmutes %s: %v", sub.Username, msg.Username, mute)
+
+ sub.muteMu.Lock()
+
+ if mute {
+ sub.muted[msg.Username] = struct{}{}
+ } else {
+ delete(sub.muted, msg.Username)
+ }
+
+ sub.muteMu.Unlock()
+
+ // Send the Who List in case our cam will show as disabled to the muted party.
+ s.SendWhoList()
+}
+
// OnCandidate handles WebRTC candidate signaling.
func (s *Server) OnCandidate(sub *Subscriber, msg Message) {
// Look up the other subscriber.
diff --git a/pkg/messages.go b/pkg/messages.go
index 8e2ce2b..612ba56 100644
--- a/pkg/messages.go
+++ b/pkg/messages.go
@@ -37,7 +37,10 @@ type Message struct {
const (
// Actions sent by the client side only
- ActionLogin = "login" // post the username to backend
+ ActionLogin = "login" // post the username to backend
+ ActionBoot = "boot" // boot a user off your video feed
+ ActionMute = "mute" // mute a user's chat messages
+ ActionUnmute = "unmute"
// Actions sent by server or client
ActionMessage = "message" // post a message to the room
diff --git a/pkg/websocket.go b/pkg/websocket.go
index 0a46faa..010db17 100644
--- a/pkg/websocket.go
+++ b/pkg/websocket.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net/http"
"strings"
+ "sync"
"time"
"git.kirsle.net/apps/barertc/pkg/config"
@@ -30,6 +31,10 @@ type Subscriber struct {
cancel context.CancelFunc
messages chan []byte
closeSlow func()
+
+ muteMu sync.RWMutex
+ booted map[string]struct{} // usernames booted off your camera
+ muted map[string]struct{} // usernames you muted
}
// ReadLoop spawns a goroutine that reads from the websocket connection.
@@ -81,6 +86,10 @@ func (sub *Subscriber) ReadLoop(s *Server) {
s.OnMe(sub, msg)
case ActionOpen:
s.OnOpen(sub, msg)
+ case ActionBoot:
+ s.OnBoot(sub, msg)
+ case ActionMute, ActionUnmute:
+ s.OnMute(sub, msg, msg.Action == ActionMute)
case ActionCandidate:
s.OnCandidate(sub, msg)
case ActionSDP:
@@ -102,7 +111,7 @@ func (sub *Subscriber) SendJSON(v interface{}) error {
if err != nil {
return err
}
- // log.Debug("SendJSON(%d=%s): %s", sub.ID, sub.Username, data)
+ log.Debug("SendJSON(%d=%s): %s", sub.ID, sub.Username, data)
return sub.conn.Write(sub.ctx, websocket.MessageText, data)
}
@@ -154,6 +163,8 @@ func (s *Server) WebSocket() http.HandlerFunc {
closeSlow: func() {
c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages")
},
+ booted: make(map[string]struct{}),
+ muted: make(map[string]struct{}),
}
s.AddSubscriber(sub)
@@ -253,6 +264,12 @@ func (s *Server) Broadcast(msg Message) {
continue
}
+ // Don't deliver it if the receiver has muted us.
+ if sub.Mutes(msg.Username) {
+ log.Debug("Do not broadcast message to %s: they have muted or booted %s", sub.Username, msg.Username)
+ continue
+ }
+
sub.SendJSON(msg)
}
}
@@ -286,29 +303,38 @@ func (s *Server) SendTo(username string, msg Message) error {
// SendWhoList broadcasts the connected members to everybody in the room.
func (s *Server) SendWhoList() {
var (
- users = []WhoList{}
subscribers = s.IterSubscribers()
)
+ // Build the WhoList for each subscriber.
+ // TODO: it's the only way to fake videoActive for booted user views.
for _, sub := range subscribers {
if !sub.authenticated {
continue
}
- who := WhoList{
- Username: sub.Username,
- VideoActive: sub.VideoActive,
- NSFW: sub.VideoNSFW,
- }
- if sub.JWTClaims != nil {
- who.Operator = sub.JWTClaims.IsAdmin
- who.Avatar = sub.JWTClaims.Avatar
- who.ProfileURL = sub.JWTClaims.ProfileURL
- }
- users = append(users, who)
- }
+ var users = []WhoList{}
+ for _, user := range subscribers {
+ who := WhoList{
+ Username: user.Username,
+ VideoActive: user.VideoActive,
+ NSFW: user.VideoNSFW,
+ }
+
+ // If this person had booted us, force their camera to "off"
+ if user.Boots(sub.Username) || user.Mutes(sub.Username) {
+ who.VideoActive = false
+ who.NSFW = false
+ }
+
+ if sub.JWTClaims != nil {
+ who.Operator = user.JWTClaims.IsAdmin
+ who.Avatar = user.JWTClaims.Avatar
+ who.ProfileURL = user.JWTClaims.ProfileURL
+ }
+ users = append(users, who)
+ }
- for _, sub := range subscribers {
sub.SendJSON(Message{
Action: ActionWhoList,
WhoList: users,
@@ -316,6 +342,22 @@ func (s *Server) SendWhoList() {
}
}
+// Boots checks whether the subscriber has blocked username from their camera.
+func (s *Subscriber) Boots(username string) bool {
+ s.muteMu.RLock()
+ defer s.muteMu.RUnlock()
+ _, ok := s.booted[username]
+ return ok
+}
+
+// Mutes checks whether the subscriber has muted username.
+func (s *Subscriber) Mutes(username string) bool {
+ s.muteMu.RLock()
+ defer s.muteMu.RUnlock()
+ _, ok := s.muted[username]
+ return ok
+}
+
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
diff --git a/web/static/js/BareRTC.js b/web/static/js/BareRTC.js
index 644cad3..2bb68bd 100644
--- a/web/static/js/BareRTC.js
+++ b/web/static/js/BareRTC.js
@@ -67,7 +67,9 @@ const app = Vue.createApp({
// Who List for the room.
whoList: [],
+ whoTab: 'online',
whoMap: {}, // map username to wholist entry
+ muted: {}, // muted usernames for client side state
// My video feed.
webcam: {
@@ -339,6 +341,36 @@ const app = Vue.createApp({
}
},
+ // Mute or unmute a user.
+ muteUser(username) {
+ let mute = this.muted[username] == undefined;
+ if (mute) {
+ this.muted[username] = true;
+ } else {
+ delete this.muted[username];
+ }
+
+ this.sendMute(username, mute);
+ if (mute) {
+ this.ChatClient(
+ `You have muted ${username} and will no longer see their chat messages, `+
+ `and they will not see whether your webcam is active. You may unmute them via the Who Is Online list.`);
+ } else {
+ this.ChatClient(
+ `You have unmuted ${username} and can see their chat messages from now on.`,
+ );
+ }
+ },
+ sendMute(username, mute) {
+ this.ws.conn.send(JSON.stringify({
+ action: mute ? "mute" : "unmute",
+ username: username,
+ }));
+ },
+ isMutedUser(username) {
+ return this.muted[username] != undefined;
+ },
+
// Send a video request to access a user's camera.
sendOpen(username) {
this.ws.conn.send(JSON.stringify({
@@ -346,6 +378,12 @@ const app = Vue.createApp({
username: username,
}));
},
+ sendBoot(username) {
+ this.ws.conn.send(JSON.stringify({
+ action: "boot",
+ username: username,
+ }));
+ },
onOpen(msg) {
// Response for the opener to begin WebRTC connection.
const secret = msg.openSecret;
@@ -428,6 +466,7 @@ const app = Vue.createApp({
conn.addEventListener("close", ev => {
// Lost connection to server - scrub who list.
this.onWho({whoList: []});
+ this.muted = {};
this.ws.connected = false;
this.ChatClient(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`);
@@ -930,6 +969,39 @@ const app = Vue.createApp({
} else {
this.ChatClient("Your current webcam viewers are:
" + users.join(", "));
}
+
+ // Also focus the Watching list.
+ this.whoTab = 'watching';
+
+ // TODO: if mobile, show the panel - this width matches
+ // the media query in chat.css
+ if (screen.width < 1024) {
+ this.openWhoPanel();
+ }
+ },
+
+ // Boot someone off yourn video.
+ bootUser(username) {
+ if (!window.confirm(
+ `Kick ${username} off your camera? This will also prevent them `+
+ `from seeing that your camera is active for the remainder of your `+
+ `chat session.`)) {
+ return;
+ }
+
+ this.sendBoot(username);
+
+ // Close the WebRTC peer connection.
+ if (this.WebRTC.pc[username] != undefined) {
+ this.closeVideo(username, "answerer");
+ }
+
+ this.ChatClient(
+ `You have booted ${username} off your camera. They will no longer be able `+
+ `to connect to your camera, or even see that your camera is active at all -- `+
+ `to them it appears as though you had turned yours off.
This will be `+
+ `in place for the remainder of your current chat session.`
+ );
},
// Stop broadcasting.
@@ -937,6 +1009,7 @@ const app = Vue.createApp({
this.webcam.elem.srcObject = null;
this.webcam.stream = null;
this.webcam.active = false;
+ this.whoTab = "online";
// Close all WebRTC sessions.
for (username of Object.keys(this.WebRTC.pc)) {
diff --git a/web/templates/chat.html b/web/templates/chat.html
index 5d9a655..1bf806c 100644
--- a/web/templates/chat.html
+++ b/web/templates/chat.html
@@ -574,12 +574,27 @@