Blocklist improvements + WebSocket timeout tweak

This commit is contained in:
Noah 2023-09-30 12:32:09 -07:00
parent 4b971fcf41
commit a1b0d2e965
7 changed files with 153 additions and 4 deletions

View File

@ -345,7 +345,7 @@ The `unmute` action does the opposite and removes the mute status:
## Block ## Block
Sent by: Client. Sent by: Client, Server.
The block command places a hard block between the current user and the target. The block command places a hard block between the current user and the target.
@ -364,6 +364,10 @@ When either user blocks the other:
} }
``` ```
The server may send a "block" message to the client in response to the BlockNow API endpoint: your main website can communicate that a block was just added, so if either user is currently in chat the block can apply immediately instead of at either user's next re-join of the room.
The server "block" message follows the same format, having the username of the other party.
## Blocklist ## Blocklist
Sent by: Client. Sent by: Client.

View File

@ -354,6 +354,119 @@ func (s *Server) BlockList() http.HandlerFunc {
}) })
} }
// BlockNow (/api/block/now) allows your website to add to a current online chatter's
// blocked list immediately.
//
// For example: the BlockList endpoint does a bulk sync of the blocklist at the time
// a user joins the chat room, but if users are already on chat when the blocking begins,
// it doesn't take effect until one or the other re-joins the room. This API endpoint
// can apply the blocking immediately to the currently online users.
//
// It is a POST request with a json body containing the following schema:
//
// {
// "APIKey": "from settings.toml",
// "Usernames": [ "source", "target" ]
// }
//
// The pair of usernames will be the two users who block one another (in any order).
// If any of the users are currently connected to the chat, they will all mutually
// block one another immediately.
func (s *Server) BlockNow() http.HandlerFunc {
type request struct {
APIKey string
Usernames []string
}
type result struct {
OK bool
Error string `json:",omitempty"`
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// JSON writer for the response.
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
// Parse the request.
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusBadRequest)
enc.Encode(result{
Error: "Only POST methods allowed",
})
return
} else if r.Header.Get("Content-Type") != "application/json" {
w.WriteHeader(http.StatusBadRequest)
enc.Encode(result{
Error: "Only application/json content-types allowed",
})
return
}
defer r.Body.Close()
// Parse the request payload.
var (
params request
dec = json.NewDecoder(r.Body)
)
if err := dec.Decode(&params); err != nil {
w.WriteHeader(http.StatusBadRequest)
enc.Encode(result{
Error: err.Error(),
})
return
}
// Validate the API key.
if params.APIKey != config.Current.AdminAPIKey {
w.WriteHeader(http.StatusUnauthorized)
enc.Encode(result{
Error: "Authentication denied.",
})
return
}
// Check if any of these users are online, and update their blocklist accordingly.
var changed bool
for _, username := range params.Usernames {
if sub, err := s.GetSubscriber(username); err == nil {
for _, otherName := range params.Usernames {
if username == otherName {
continue
}
log.Info("BlockNow API: %s is currently on chat, add block for %+v", username, otherName)
sub.muteMu.Lock()
sub.muted[otherName] = struct{}{}
sub.blocked[otherName] = struct{}{}
sub.muteMu.Unlock()
// Changes have been made to online users.
changed = true
// Send a server-side "block" command to the subscriber, so their front-end page might
// update the cachedBlocklist so there's no leakage in case of chat server rebooting.
sub.SendJSON(messages.Message{
Action: messages.ActionBlock,
Username: otherName,
})
}
}
}
// If any changes to blocklists were made: send the Who List.
if changed {
s.SendWhoList()
}
enc.Encode(result{
OK: true,
})
})
}
// Blocklist cache sent over from your website. // Blocklist cache sent over from your website.
var ( var (
// Map of username to the list of usernames they block. // Map of username to the list of usernames they block.

View File

@ -179,6 +179,7 @@ func (s *Server) OnMessage(sub *Subscriber, msg messages.Message) {
// If the user is OP, just tell them we would. // If the user is OP, just tell them we would.
if sub.IsAdmin() { if sub.IsAdmin() {
sub.ChatServer("Your recent chat context would have been reported to your main website.") sub.ChatServer("Your recent chat context would have been reported to your main website.")
return
} }
// Send the report to the main website. // Send the report to the main website.
@ -483,7 +484,7 @@ func (s *Server) OnBlock(sub *Subscriber, msg messages.Message) {
// 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 {

View File

@ -28,6 +28,13 @@ func (s *Server) filterMessage(sub *Subscriber, rawMsg messages.Message, msg *me
if strings.HasPrefix(msg.Channel, "@") { if strings.HasPrefix(msg.Channel, "@") {
// DM // DM
pushDirectMessageContext(sub, sub.Username, msg.Channel[1:], rawMsg) pushDirectMessageContext(sub, sub.Username, msg.Channel[1:], rawMsg)
// If either party is an admin user, waive filtering this DM chat.
if sub.IsAdmin() {
return nil, false
} else if other, err := s.GetSubscriber(msg.Channel[1:]); err == nil && other.IsAdmin() {
return nil, false
}
} else { } else {
// Public channel // Public channel
pushMessageContext(sub, msg.Channel, rawMsg) pushMessageContext(sub, msg.Channel, rawMsg)

View File

@ -36,6 +36,7 @@ func (s *Server) Setup() error {
mux.Handle("/ws", s.WebSocket()) mux.Handle("/ws", s.WebSocket())
mux.Handle("/api/statistics", s.Statistics()) mux.Handle("/api/statistics", s.Statistics())
mux.Handle("/api/blocklist", s.BlockList()) mux.Handle("/api/blocklist", s.BlockList())
mux.Handle("/api/block/now", s.BlockNow())
mux.Handle("/api/authenticate", s.Authenticate()) mux.Handle("/api/authenticate", s.Authenticate())
mux.Handle("/api/shutdown", s.ShutdownAPI()) mux.Handle("/api/shutdown", s.ShutdownAPI())
mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets")))) mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))

View File

@ -218,7 +218,7 @@ func (s *Server) WebSocket() http.HandlerFunc {
for { for {
select { select {
case msg := <-sub.messages: case msg := <-sub.messages:
err = writeTimeout(ctx, time.Second*5, c, msg) err = writeTimeout(ctx, time.Second*15, c, msg)
if err != nil { if err != nil {
return return
} }

View File

@ -957,6 +957,21 @@ export default {
} }
}, },
// Server side "block" event: for when the main website sends a BlockNow API request.
onBlock(msg) {
// Close any video connections we had with this user.
this.closeVideo(msg.username);
// Add it to our CachedBlocklist so in case the server reboots, we continue to sync it on reconnect.
for (let existing of this.config.CachedBlocklist) {
if (existing === msg.username) {
return;
}
}
this.config.CachedBlocklist.push(msg.username);
},
// Mute or unmute a user. // Mute or unmute a user.
muteUser(username) { muteUser(username) {
username = this.normalizeUsername(username); username = this.normalizeUsername(username);
@ -1184,6 +1199,11 @@ export default {
this.ws.connected = true; this.ws.connected = true;
this.ChatClient("Websocket connected!"); this.ChatClient("Websocket connected!");
// Upload our blocklist to the server before login. This resolves a bug where if a block
// was added recently (other user still online in chat), that user would briefly see your
// "has entered the room" message followed by you immediately not being online.
this.bulkMuteUsers();
// Tell the server our username. // Tell the server our username.
this.ws.conn.send(JSON.stringify({ this.ws.conn.send(JSON.stringify({
action: "login", action: "login",
@ -1249,6 +1269,9 @@ export default {
case "unwatch": case "unwatch":
this.onUnwatch(msg); this.onUnwatch(msg);
break; break;
case "block":
this.onBlock(msg);
break;
case "error": case "error":
this.pushHistory({ this.pushHistory({
channel: msg.channel, channel: msg.channel,
@ -3733,7 +3756,7 @@ export default {
label on the chat history panel --> label on the chat history panel -->
<div class="dropdown-content p-0"> <div class="dropdown-content p-0">
<EmojiPicker <EmojiPicker
:native="false" :native="true"
:display-recent="true" :display-recent="true"
:disable-skin-tones="true" :disable-skin-tones="true"
theme="auto" theme="auto"