mirror of
https://github.com/Team254/cheesy-arena-lite.git
synced 2026-03-09 13:46:44 -04:00
Refactor display registry to not broadcast all config changes to all displays.
This commit is contained in:
@@ -40,7 +40,7 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -50,7 +50,7 @@ func (web *Web) allianceStationDisplayWebsocketHandler(w http.ResponseWriter, r
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.AllianceStationDisplayModeNotifier,
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.AllianceStationDisplayModeNotifier,
|
||||
web.arena.ArenaStatusNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
|
||||
web.arena.RealtimeScoreNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
web.arena.RealtimeScoreNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -36,13 +36,13 @@ func TestAllianceStationDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "allianceStationDisplayMode")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
|
||||
// Change to a different screen.
|
||||
web.arena.AllianceStationDisplayMode = "logo"
|
||||
|
||||
@@ -40,7 +40,7 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -50,7 +50,7 @@ func (web *Web) announcerDisplayWebsocketHandler(w http.ResponseWriter, r *http.
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
|
||||
web.arena.RealtimeScoreNotifier, web.arena.ScorePostedNotifier, web.arena.AudienceDisplayModeNotifier,
|
||||
web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier,
|
||||
web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier, web.arena.ScorePostedNotifier,
|
||||
web.arena.AudienceDisplayModeNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -29,13 +29,13 @@ func TestAnnouncerDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "realtimeScore")
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
|
||||
@@ -43,7 +43,7 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -53,8 +53,8 @@ func (web *Web) audienceDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.AudienceDisplayModeNotifier,
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.AudienceDisplayModeNotifier,
|
||||
web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier, web.arena.RealtimeScoreNotifier,
|
||||
web.arena.PlaySoundNotifier, web.arena.ScorePostedNotifier, web.arena.AllianceSelectionNotifier,
|
||||
web.arena.LowerThirdNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
web.arena.LowerThirdNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "audienceDisplayMode")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
@@ -44,7 +45,6 @@ func TestAudienceDisplayWebsocket(t *testing.T) {
|
||||
readWebsocketType(t, ws, "scorePosted")
|
||||
readWebsocketType(t, ws, "allianceSelection")
|
||||
readWebsocketType(t, ws, "lowerThird")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
|
||||
// Run through a match cycle.
|
||||
web.arena.MatchLoadNotifier.Notify()
|
||||
|
||||
@@ -54,18 +54,16 @@ func (web *Web) enforceDisplayConfiguration(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// Constructs, registers, and returns the display object for the given incoming websocket request.
|
||||
func (web *Web) registerDisplay(r *http.Request) (*field.Display, error) {
|
||||
display, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
|
||||
displayConfig, err := field.DisplayFromUrl(r.URL.Path, r.URL.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the source IP address of the request and store it in the display object.
|
||||
if ipAddress := r.Header.Get("X-Real-IP"); ipAddress != "" {
|
||||
display.IpAddress = ipAddress
|
||||
} else {
|
||||
display.IpAddress = regexp.MustCompile("(.*):\\d+$").FindStringSubmatch(r.RemoteAddr)[1]
|
||||
var ipAddress string
|
||||
if ipAddress = r.Header.Get("X-Real-IP"); ipAddress == "" {
|
||||
ipAddress = regexp.MustCompile("(.*):\\d+$").FindStringSubmatch(r.RemoteAddr)[1]
|
||||
}
|
||||
|
||||
web.arena.RegisterDisplay(display)
|
||||
return display, nil
|
||||
return web.arena.RegisterDisplay(displayConfig, ipAddress), nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *ht
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -49,6 +49,5 @@ func (web *Web) fieldMonitorDisplayWebsocketHandler(w http.ResponseWriter, r *ht
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.ArenaStatusNotifier, web.arena.DisplayConfigurationNotifier,
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.ArenaStatusNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,6 @@ func TestFieldMonitorDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "arenaStatus")
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -49,6 +49,5 @@ func (web *Web) pitDisplayWebsocketHandler(w http.ResponseWriter, r *http.Reques
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.EventStatusNotifier, web.arena.DisplayConfigurationNotifier,
|
||||
web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.EventStatusNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ func TestPitDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "eventStatus")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "eventStatus")
|
||||
|
||||
// Check forced reloading as that is the only purpose the pit websocket serves.
|
||||
web.arena.ReloadDisplaysNotifier.Notify()
|
||||
|
||||
@@ -39,7 +39,7 @@ func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter, r *htt
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -49,5 +49,5 @@ func (web *Web) placeholderDisplayWebsocketHandler(w http.ResponseWriter, r *htt
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -37,15 +37,15 @@ func TestPlaceholderDisplayWebsocket(t *testing.T) {
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
|
||||
if assert.Contains(t, web.arena.Displays, "123") {
|
||||
assert.Equal(t, "blop", web.arena.Displays["123"].Nickname)
|
||||
if assert.Equal(t, 1, len(web.arena.Displays["123"].Configuration)) {
|
||||
assert.Equal(t, "b", web.arena.Displays["123"].Configuration["a"])
|
||||
assert.Equal(t, "blop", web.arena.Displays["123"].DisplayConfiguration.Nickname)
|
||||
if assert.Equal(t, 1, len(web.arena.Displays["123"].DisplayConfiguration.Configuration)) {
|
||||
assert.Equal(t, "b", web.arena.Displays["123"].DisplayConfiguration.Configuration["a"])
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigure the display and verify that the new configuration is received.
|
||||
display := &field.Display{Id: "123", Nickname: "Alliance", Type: field.AllianceStationDisplay,
|
||||
displayConfig := field.DisplayConfiguration{Id: "123", Nickname: "Alliance", Type: field.AllianceStationDisplay,
|
||||
Configuration: map[string]string{"station": "B2"}}
|
||||
web.arena.UpdateDisplay(display)
|
||||
web.arena.UpdateDisplay(displayConfig)
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -79,6 +79,6 @@ func (web *Web) queueingDisplayWebsocketHandler(w http.ResponseWriter, r *http.R
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier, web.arena.MatchTimeNotifier,
|
||||
web.arena.EventStatusNotifier, web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.MatchTimingNotifier, web.arena.MatchLoadNotifier,
|
||||
web.arena.MatchTimeNotifier, web.arena.EventStatusNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@ func TestQueueingDisplayWebsocket(t *testing.T) {
|
||||
ws := websocket.NewTestWebsocket(conn)
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
readWebsocketType(t, ws, "matchTiming")
|
||||
readWebsocketType(t, ws, "matchLoad")
|
||||
readWebsocketType(t, ws, "matchTime")
|
||||
readWebsocketType(t, ws, "eventStatus")
|
||||
readWebsocketType(t, ws, "displayConfiguration")
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ func (web *Web) displaysWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
switch messageType {
|
||||
case "configureDisplay":
|
||||
var display field.Display
|
||||
err = mapstructure.Decode(data, &display)
|
||||
var displayConfig field.DisplayConfiguration
|
||||
err = mapstructure.Decode(data, &displayConfig)
|
||||
if err != nil {
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
if err = web.arena.UpdateDisplay(&display); err != nil {
|
||||
if err = web.arena.UpdateDisplay(displayConfig); err != nil {
|
||||
ws.WriteError(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -32,37 +32,37 @@ func TestSetupDisplaysWebsocket(t *testing.T) {
|
||||
|
||||
// Should get a few status updates right after connection.
|
||||
message := readDisplayConfiguration(t, ws)
|
||||
assert.Empty(t, message.Displays)
|
||||
assert.Empty(t, message.DisplayUrls)
|
||||
assert.Empty(t, message)
|
||||
|
||||
// Connect a couple of displays and verify the resulting configuration messages.
|
||||
displayConn1, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
|
||||
defer displayConn1.Close()
|
||||
displayWs1 := websocket.NewTestWebsocket(displayConn1)
|
||||
assert.Equal(t, "/display?displayId=1", readWebsocketType(t, displayWs1, "displayConfiguration"))
|
||||
readDisplayConfiguration(t, ws)
|
||||
displayConn2, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+
|
||||
"/displays/alliance_station/websocket?displayId=2&station=R2", nil)
|
||||
defer displayConn2.Close()
|
||||
expectedDisplay1 := &field.Display{Id: "1", Type: field.PlaceholderDisplay, Configuration: map[string]string{},
|
||||
ConnectionCount: 1, IpAddress: "127.0.0.1"}
|
||||
expectedDisplay2 := &field.Display{Id: "2", Type: field.AllianceStationDisplay,
|
||||
Configuration: map[string]string{"station": "R2"}, ConnectionCount: 1, IpAddress: "127.0.0.1"}
|
||||
message = readDisplayConfiguration(t, ws)
|
||||
if assert.Equal(t, 2, len(message.Displays)) {
|
||||
assert.Equal(t, expectedDisplay1, message.Displays["1"])
|
||||
assert.Equal(t, expectedDisplay2, message.Displays["2"])
|
||||
assert.Equal(t, expectedDisplay1.ToUrl(), message.DisplayUrls["1"])
|
||||
assert.Equal(t, expectedDisplay2.ToUrl(), message.DisplayUrls["2"])
|
||||
if assert.Equal(t, 2, len(message)) {
|
||||
assert.Equal(t, field.DisplayConfiguration{"1", "", field.PlaceholderDisplay, map[string]string{}},
|
||||
message["1"].DisplayConfiguration)
|
||||
assert.Equal(t, 1, message["1"].ConnectionCount)
|
||||
assert.Equal(t, "127.0.0.1", message["1"].IpAddress)
|
||||
assert.Equal(t, field.DisplayConfiguration{"2", "", field.AllianceStationDisplay,
|
||||
map[string]string{"station": "R2"}}, message["2"].DisplayConfiguration)
|
||||
assert.Equal(t, 1, message["2"].ConnectionCount)
|
||||
assert.Equal(t, "127.0.0.1", message["2"].IpAddress)
|
||||
}
|
||||
|
||||
// Reconfigure a display and verify the result.
|
||||
expectedDisplay1.Nickname = "Audience Display"
|
||||
expectedDisplay1.Type = field.AudienceDisplay
|
||||
expectedDisplay1.Configuration["background"] = "#00f"
|
||||
expectedDisplay1.Configuration["reversed"] = "true"
|
||||
ws.Write("configureDisplay", expectedDisplay1)
|
||||
displayConfig := field.DisplayConfiguration{Id: "1", Nickname: "Audience Display", Type: field.AudienceDisplay,
|
||||
Configuration: map[string]string{"background": "#00f", "reversed": "true"}}
|
||||
ws.Write("configureDisplay", displayConfig)
|
||||
message = readDisplayConfiguration(t, ws)
|
||||
assert.Equal(t, expectedDisplay1, message.Displays["1"])
|
||||
assert.Equal(t, expectedDisplay1.ToUrl(), message.DisplayUrls["1"])
|
||||
assert.Equal(t, displayConfig, message["1"].DisplayConfiguration)
|
||||
assert.Equal(t, "/displays/audience?displayId=1&nickname=Audience+Display&background=%2300f&reversed=true",
|
||||
readWebsocketType(t, displayWs1, "displayConfiguration"))
|
||||
}
|
||||
|
||||
func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
|
||||
@@ -82,7 +82,7 @@ func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
|
||||
displayConn, _, _ := gorillawebsocket.DefaultDialer.Dial(wsUrl+"/display/websocket?displayId=1", nil)
|
||||
defer displayConn.Close()
|
||||
displayWs := websocket.NewTestWebsocket(displayConn)
|
||||
readDisplayConfiguration(t, displayWs)
|
||||
assert.Equal(t, "/display?displayId=1", readWebsocketType(t, displayWs, "displayConfiguration"))
|
||||
readDisplayConfiguration(t, ws)
|
||||
|
||||
// Reset a display selectively and verify the resulting message.
|
||||
@@ -92,10 +92,10 @@ func TestSetupDisplaysWebsocketReloadDisplays(t *testing.T) {
|
||||
assert.Equal(t, nil, readWebsocketType(t, displayWs, "reload"))
|
||||
}
|
||||
|
||||
func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) *field.DisplayConfigurationMessage {
|
||||
func readDisplayConfiguration(t *testing.T, ws *websocket.Websocket) map[string]field.Display {
|
||||
message := readWebsocketType(t, ws, "displayConfiguration")
|
||||
var displayConfigurationMessage field.DisplayConfigurationMessage
|
||||
var displayConfigurationMessage map[string]field.Display
|
||||
err := mapstructure.Decode(message, &displayConfigurationMessage)
|
||||
assert.Nil(t, err)
|
||||
return &displayConfigurationMessage
|
||||
return displayConfigurationMessage
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (web *Web) twitchDisplayWebsocketHandler(w http.ResponseWriter, r *http.Req
|
||||
handleWebErr(w, err)
|
||||
return
|
||||
}
|
||||
defer web.arena.MarkDisplayDisconnected(display)
|
||||
defer web.arena.MarkDisplayDisconnected(display.DisplayConfiguration.Id)
|
||||
|
||||
ws, err := websocket.NewWebsocket(w, r)
|
||||
if err != nil {
|
||||
@@ -49,5 +49,5 @@ func (web *Web) twitchDisplayWebsocketHandler(w http.ResponseWriter, r *http.Req
|
||||
defer ws.Close()
|
||||
|
||||
// Subscribe the websocket to the notifiers whose messages will be passed on to the client.
|
||||
ws.HandleNotifiers(web.arena.DisplayConfigurationNotifier, web.arena.ReloadDisplaysNotifier)
|
||||
ws.HandleNotifiers(display.Notifier, web.arena.ReloadDisplaysNotifier)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user