More thorough blocking behavior
This commit is contained in:
parent
940f14e2d6
commit
7ffa6b4dbd
25
Protocol.md
25
Protocol.md
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
* **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.
|
||||||
|
* **WebsiteURL** is the base URL of your actual website which is used in a couple of places:
|
||||||
* 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.
|
* **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.
|
||||||
* **CORSHosts**: your website's domain names that will be allowed to access [JSON APIs](#JSON APIs), like `/api/statistics`.
|
* **AdminAPIKey** is a shared secret authentication key for the admin API endpoints.
|
||||||
* **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.
|
* **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.
|
||||||
* **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).
|
* **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.
|
||||||
* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view.
|
* **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).
|
||||||
* **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.
|
* **MaxImageWidth**: for pictures shared in chat the server will resize them down to no larger than this width for the full size view.
|
||||||
* **JWT**: settings for JWT [Authentication](#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.
|
||||||
* 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.
|
## JWT Authentication
|
||||||
* 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.
|
Settings for JWT [Authentication](#authentication):
|
||||||
* ID (string): an arbitrary 'username' for the chat channel, like "lobby".
|
|
||||||
* Name (string): the user friendly name for the channel, like "Off Topic"
|
* **Enabled** (bool): activate the JWT token authentication feature.
|
||||||
* Icon (string, optional): CSS class names for FontAwesome icon for the channel, like "fa fa-message"
|
* **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.
|
||||||
* 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.
|
* **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.
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ type Subscriber struct {
|
||||||
|
|
||||||
muteMu sync.RWMutex
|
muteMu sync.RWMutex
|
||||||
booted map[string]struct{} // usernames booted off your camera
|
booted map[string]struct{} // usernames booted off your camera
|
||||||
|
blocked map[string]struct{} // usernames you have blocked
|
||||||
muted map[string]struct{} // usernames you muted
|
muted map[string]struct{} // usernames you muted
|
||||||
|
|
||||||
// Record which message IDs belong to this user.
|
// Record which message IDs belong to this user.
|
||||||
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user