More thorough blocking behavior

This commit is contained in:
Noah 2023-09-04 13:36:12 -07:00
parent 940f14e2d6
commit 7ffa6b4dbd
5 changed files with 159 additions and 34 deletions

View File

@ -343,11 +343,32 @@ The `unmute` action does the opposite and removes the mute status:
} }
``` ```
## Block
Sent by: Client.
The block command places a hard block between the current user and the target.
When either user blocks the other:
* They do not see each other in the Who's Online list at all.
* They can not see each other's messages, including presence messages.
**Note:** the chat page currently does not have a front-end button to block a user. This feature is currently used by the Blocklist feature to apply a block to a set of users at once upon join.
```javascript
// Client Block
{
"action": "block",
"username": "target"
}
```
## Blocklist ## Blocklist
Sent by: Client. Sent by: Client.
The blocklist command is basically a bulk mute for (potentially) many usernames at once. The blocklist command is basically a bulk block for (potentially) many usernames at once.
```javascript ```javascript
// Client blocklist // Client blocklist
@ -359,7 +380,7 @@ The blocklist command is basically a bulk mute for (potentially) many usernames
How this works: if you have an existing website and use JWT authentication to sign users into chat, your site can pre-emptively sync the user's block list **before** the user enters the room, using the `/api/blocklist` endpoint (see the README.md for BareRTC). How this works: if you have an existing website and use JWT authentication to sign users into chat, your site can pre-emptively sync the user's block list **before** the user enters the room, using the `/api/blocklist` endpoint (see the README.md for BareRTC).
The chat server holds onto blocklists temporarily in memory: when that user loads the chat room (with a JWT token!), the front-end page receives the cached blocklist. As part of the "on connected" handler, the chat page sends the `blocklist` command over WebSocket to perform a mass mute on these users in one go. The chat server holds onto blocklists temporarily in memory: when that user loads the chat room (with a JWT token!), the front-end page receives the cached blocklist. As part of the "on connected" handler, the chat page sends the `blocklist` command over WebSocket to perform a mass block on these users in one go.
The reason for this workflow is in case the chat server is rebooted _while_ the user is in the room. The cached blocklist pushed by your website is forgotten by the chat server back-end, but the client's page was still open with the cached blocklist already, and it will send the `blocklist` command to the server when it reconnects, eliminating any gaps. The reason for this workflow is in case the chat server is rebooted _while_ the user is in the room. The cached blocklist pushed by your website is forgotten by the chat server back-end, but the client's page was still open with the cached blocklist already, and it will send the `blocklist` command to the server when it reconnects, eliminating any gaps.

View File

@ -3,21 +3,28 @@
On first run it will create the default settings.toml file for you which you may then customize to your liking: On first run it will create the default settings.toml file for you which you may then customize to your liking:
```toml ```toml
Version = 2 Version = 7
Title = "BareRTC" Title = "BareRTC"
Branding = "BareRTC" Branding = "BareRTC"
WebsiteURL = "https://www.example.com" WebsiteURL = "http://www.example.com"
CORSHosts = ["https://www.example.com"] CORSHosts = ["http://www.example.com"]
AdminAPIKey = "e635e463-7987-4788-94f3-671a5c2a589f"
PermitNSFW = true PermitNSFW = true
UseXForwardedFor = true UseXForwardedFor = false
WebSocketReadLimit = 41943040 WebSocketReadLimit = 40971520
MaxImageWidth = 1280 MaxImageWidth = 1280
PreviewImageWidth = 360 PreviewImageWidth = 360
[JWT] [JWT]
Enabled = false Enabled = true
Strict = true Strict = true
SecretKey = "" SecretKey = "05c45344-1c52-430b-beb9-c3f64ff7ed12"
LandingPageURL = "https://www.example.com/enter-chat"
[TURN]
URLs = ["stun:stun.l.google.com:19302"]
Username = ""
Credential = ""
[[PublicChannels]] [[PublicChannels]]
ID = "lobby" ID = "lobby"
@ -29,28 +36,54 @@ PreviewImageWidth = 360
ID = "offtopic" ID = "offtopic"
Name = "Off Topic" Name = "Off Topic"
WelcomeMessages = ["Welcome to the Off Topic channel!"] WelcomeMessages = ["Welcome to the Off Topic channel!"]
[VIP]
Name = "VIP"
Branding = "<em>VIP Members</em>"
Icon = "fa fa-circle"
MutuallySecret = false
``` ```
A description of the config directives includes: A description of the config directives includes:
* Website settings: ## Website Settings
* **Title** goes in the title bar of the chat page.
* **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `<img>` tag to embed an image or use custom markup to color and prettify your logo. * **Version** number for the settings file itself. When new features are added, the Version will increment and your settings.toml will be written to disk with sensible defaults filled in for the new options.
* **WebsiteURL** is the base URL of your actual website which is used in a couple of places: * **Title** goes in the title bar of the chat page.
* The About page will link to your website. * **Branding** is the title shown in the corner of the page. HTML is permitted here! You may write an `<img>` tag to embed an image or use custom markup to color and prettify your logo.
* 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! * **WebsiteURL** is the base URL of your actual website which is used in a couple of places:
* **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. * The About page will link to your website.
* **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`. * 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!
* **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. * **CORSHosts** names HTTP hosts for Cross Origin Resource Sharing. Usually, this will be the same as your WebsiteURL. This feature is used with the [Web API](API.md) if your front-end page needs to call e.g. the /api/statistics endpoint on BareRTC.
* **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload). * **AdminAPIKey** is a shared secret authentication key for the admin API endpoints.
* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view. * **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.
* **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal. * **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.
* **JWT**: settings for JWT [Authentication](#authentication). * **WebSocketReadLimit**: sets a size limit for WebSocket messages - it essentially also caps the max upload size for shared images (add a buffer as images will be base64 encoded on upload).
* Enabled (bool): activate the JWT token authentication feature. * **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view.
* 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. * **PreviewImageWidth**: to not flood the chat, the image in chat is this wide and users can click it to see the MaxImageWidth in a lightbox modal.
* SecretKey (string): the JWT signing secret shared with your back-end app.
* **PublicChannels**: list the public channels and their configuration. The default channel will be the first one listed. ## JWT Authentication
* ID (string): an arbitrary 'username' for the chat channel, like "lobby".
* Name (string): the user friendly name for the channel, like "Off Topic" Settings for JWT [Authentication](#authentication):
* Icon (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message"
* WelcomeMessages ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc. * **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.
* **SecretKey** (string): the JWT signing secret shared with your back-end app.
## Public Channels
Settings for the default public text channels of your room.
* **ID** (string): an arbitrary 'username' for the chat channel, like "lobby".
* **Name** (string): the user friendly name for the channel, like "Off Topic"
* **Icon** (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message"
* **WelcomeMessages** ([]string, optional): messages that are delivered by ChatServer to the user when they connect to the server. Useful to give an introduction to each channel, list its rules, etc.
## VIP Status
If using JWT authentication, your website can mark some users as VIPs when sending them over to the chat. The `[VIP]` section of settings.toml lets you customize the branding and behavior in BareRTC:
* **Name** (string): what you call your VIP users, used in mouse-over tooltips.
* **Branding** (string): HTML supported, this will appear in webcam sharing modals to "make my cam only visible to fellow VIP users"
* **Icon** (string): icon CSS name from Font Awesome.
* **MutuallySecret** (bool): if true, the VIP features are hidden and only visible to people who are, themselves, VIP. For example, the icon on the Who List will only show to VIP users but non-VIP will not see the icon.

View File

@ -179,6 +179,11 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
return return
} }
// If there is blocking happening, do not send.
if sub.Blocks(rcpt) {
return
}
if err := s.SendTo(msg.Channel, message); err != nil { if err := s.SendTo(msg.Channel, message); err != nil {
sub.ChatServer("Your message could not be delivered: %s", err) sub.ChatServer("Your message could not be delivered: %s", err)
} }
@ -289,6 +294,11 @@ func (s *Server) OnFile(sub *Subscriber, msg messages.Message) {
return return
} }
// If there is blocking happening, do not send.
if sub.Blocks(rcpt) {
return
}
if err := s.SendTo(msg.Channel, message); err != nil { if err := s.SendTo(msg.Channel, message); err != nil {
sub.ChatServer("Your message could not be delivered: %s", err) sub.ChatServer("Your message could not be delivered: %s", err)
} }
@ -417,13 +427,33 @@ func (s *Server) OnMute(sub *Subscriber, msg messages.Message, mute bool) {
s.SendWhoList() s.SendWhoList()
} }
// OnBlock is a user placing a hard block (hide from) another user.
func (s *Server) OnBlock(sub *Subscriber, msg messages.Message) {
log.Info("%s blocks %s: %v", sub.Username, msg.Username)
// If the subject of the block is an admin, return an error.
if other, err := s.GetSubscriber(msg.Username); err == nil && other.IsAdmin() {
sub.ChatServer(
"You are not allowed to block a chat operator.",
)
return
}
sub.muteMu.Lock()
sub.blocked[msg.Username] = struct{}{}
sub.muteMu.Unlock()
// Send the Who List so the blocker/blockee can disappear from each other's list.
s.SendWhoList()
}
// OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website. // OnBlocklist is a bulk user mute from the CachedBlocklist sent by the website.
func (s *Server) OnBlocklist(sub *Subscriber, msg messages.Message) { func (s *Server) OnBlocklist(sub *Subscriber, msg messages.Message) {
log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames) log.Info("%s syncs their blocklist: %s", sub.Username, msg.Usernames)
sub.muteMu.Lock() sub.muteMu.Lock()
for _, username := range msg.Usernames { for _, username := range msg.Usernames {
sub.muted[username] = struct{}{} sub.blocked[username] = struct{}{}
} }
sub.muteMu.Unlock() sub.muteMu.Unlock()

View File

@ -70,6 +70,7 @@ const (
ActionBoot = "boot" // boot a user off your video feed ActionBoot = "boot" // boot a user off your video feed
ActionMute = "mute" // mute a user's chat messages ActionMute = "mute" // mute a user's chat messages
ActionUnmute = "unmute" ActionUnmute = "unmute"
ActionBlock = "block" // hard block another user
ActionBlocklist = "blocklist" // mute in bulk for usernames ActionBlocklist = "blocklist" // mute in bulk for usernames
ActionReport = "report" // user reports a message ActionReport = "report" // user reports a message

View File

@ -36,9 +36,10 @@ type Subscriber struct {
messages chan []byte messages chan []byte
closeSlow func() closeSlow func()
muteMu sync.RWMutex muteMu sync.RWMutex
booted map[string]struct{} // usernames booted off your camera booted map[string]struct{} // usernames booted off your camera
muted map[string]struct{} // usernames you muted blocked map[string]struct{} // usernames you have blocked
muted map[string]struct{} // usernames you muted
// Record which message IDs belong to this user. // Record which message IDs belong to this user.
midMu sync.Mutex midMu sync.Mutex
@ -98,6 +99,8 @@ func (sub *Subscriber) ReadLoop(s *Server) {
s.OnBoot(sub, msg) s.OnBoot(sub, msg)
case messages.ActionMute, messages.ActionUnmute: case messages.ActionMute, messages.ActionUnmute:
s.OnMute(sub, msg, msg.Action == messages.ActionMute) s.OnMute(sub, msg, msg.Action == messages.ActionMute)
case messages.ActionBlock:
s.OnBlock(sub, msg)
case messages.ActionBlocklist: case messages.ActionBlocklist:
s.OnBlocklist(sub, msg) s.OnBlocklist(sub, msg)
case messages.ActionCandidate: case messages.ActionCandidate:
@ -193,6 +196,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
}, },
booted: make(map[string]struct{}), booted: make(map[string]struct{}),
muted: make(map[string]struct{}), muted: make(map[string]struct{}),
blocked: make(map[string]struct{}),
messageIDs: make(map[int]struct{}), messageIDs: make(map[int]struct{}),
ChatStatus: "online", ChatStatus: "online",
} }
@ -321,6 +325,13 @@ func (s *Server) Broadcast(msg messages.Message) {
log.Debug("Broadcast: %+v", msg) log.Debug("Broadcast: %+v", msg)
} }
// Get the sender of this message.
sender, err := s.GetSubscriber(msg.Username)
if err != nil {
log.Error("Broadcast: sender name %s not found as a current subscriber!", msg.Username)
sender = nil
}
// Get the list of users who are online NOW, so we don't hold the mutex lock too long. // Get the list of users who are online NOW, so we don't hold the mutex lock too long.
// Example: sending a fat GIF to a large audience could hang up the server for a long // Example: sending a fat GIF to a large audience could hang up the server for a long
// time until every copy of the GIF has been sent. // time until every copy of the GIF has been sent.
@ -336,6 +347,12 @@ func (s *Server) Broadcast(msg messages.Message) {
continue continue
} }
// Don't deliver it if there is any blocking between sender and receiver.
if sender != nil && sender.Blocks(sub) {
log.Debug("Do not broadcast message to %s: blocking between them and %s", msg.Username, sub.Username)
continue
}
// VIP channels: only deliver to subscribed VIP users. // VIP channels: only deliver to subscribed VIP users.
if ch, ok := config.Current.GetChannel(msg.Channel); ok && ch.VIP && !sub.IsVIP() { if ch, ok := config.Current.GetChannel(msg.Channel); ok && ch.VIP && !sub.IsVIP() {
log.Debug("Do not broadcast message to %s: VIP channel and they are not VIP", sub.Username) log.Debug("Do not broadcast message to %s: VIP channel and they are not VIP", sub.Username)
@ -403,6 +420,12 @@ func (s *Server) SendWhoList() {
continue continue
} }
// Blocking: hide the presence of both people from the Who List.
if user.Blocks(sub) {
log.Debug("WhoList: hide %s from %s (blocking)", user.Username, sub.Username)
continue
}
who := messages.WhoList{ who := messages.WhoList{
Username: user.Username, Username: user.Username,
Status: user.ChatStatus, Status: user.ChatStatus,
@ -470,6 +493,23 @@ func (s *Subscriber) Mutes(username string) bool {
return ok return ok
} }
// Blocks checks whether the subscriber blocks the username, or vice versa (blocking goes both directions).
func (s *Subscriber) Blocks(other *Subscriber) bool {
s.muteMu.RLock()
defer s.muteMu.RUnlock()
// Forward block?
if _, ok := s.blocked[other.Username]; ok {
return true
}
// Reverse block?
other.muteMu.RLock()
defer other.muteMu.RUnlock()
_, ok := other.blocked[s.Username]
return ok
}
func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error {
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() defer cancel()