From 96d61614f4254aa64f4c9c3d9a93ccf0929566be Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 14 Mar 2024 23:04:24 -0700 Subject: [PATCH] Add DisconnectNow API endpoint --- docs/API.md | 152 ++++++++++++++++++++++++++++++++++----- pkg/api.go | 128 +++++++++++++++++++++++++++++++++ pkg/commands.go | 4 +- pkg/handlers.go | 6 +- pkg/messages/messages.go | 9 +++ pkg/polling_api.go | 2 +- pkg/server.go | 1 + pkg/websocket.go | 4 +- 8 files changed, 282 insertions(+), 24 deletions(-) diff --git a/docs/API.md b/docs/API.md index 939d909..c7f47c9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -27,16 +27,16 @@ Post your desired JWT claims to the endpoint to customize your user and it will ```json { - "APIKey": "from settings.toml", - "Claims": { - "sub": "username", - "nick": "Display Name", - "op": false, - "img": "/static/photos/avatar.png", - "url": "/users/username", - "emoji": "🤖", - "gender": "m" - } + "APIKey": "from settings.toml", + "Claims": { + "sub": "username", + "nick": "Display Name", + "op": false, + "img": "/static/photos/avatar.png", + "url": "/users/username", + "emoji": "🤖", + "gender": "m" + } } ``` @@ -44,9 +44,9 @@ The return schema looks like: ```json { - "OK": true, - "Error": "error string, omitted if none", - "JWT": "jwt token string" + "OK": true, + "Error": "error string, omitted if none", + "JWT": "jwt token string" } ``` @@ -58,7 +58,7 @@ It requires the AdminAPIKey to post: ```json { - "APIKey": "from settings.toml" + "APIKey": "from settings.toml" } ``` @@ -66,8 +66,8 @@ The return schema looks like: ```json { - "OK": true, - "Error": "error string, omitted if none" + "OK": true, + "Error": "error string, omitted if none" } ``` @@ -110,3 +110,123 @@ The JSON response to this endpoint may look like: "Error": "if error, or this key is omitted if OK" } ``` + +## POST /api/block/now + +Your website can tell BareRTC to put a block between users "now." For +example, if a user on your main website adds a block on another user, +and one or both of them are presently logged into the chat room, BareRTC +can begin enforcing the block immediately so both users will disappear +from each other's view and no longer get one another's messages. + +The request body payload looks like: + +```json +{ + "APIKey": "from your settings.toml", + "Usernames": [ "alice", "bob" ] +} +``` + +The pair of usernames should be the two who are blocking each other, in +any order. This will put in a two-way block between those chatters. + +If you provide more than two usernames, the block is put between every +combination of usernames given. + +The JSON response to this endpoint may look like: + +```json +{ + "OK": true, + "Error": "if error, or this key is omitted if OK" +} +``` + +## POST /api/disconnect/now + +Your website can tell BareRTC to remove a user from the chat room "now" +in case that user is presently online in the chat. + +The request body payload looks like: + +```json +{ + "APIKey": "from your settings.toml", + "Usernames": [ "alice" ], + "Message": "a custom ChatServer message to send them, optional", + "Kick": false, +} +``` + +The `Message` parameter, if provided, will be sent to that user as a +ChatServer error before they are removed from the room. You can use this +to provide them context as to why they are being kicked. For example: +"You have been logged out of chat because you deactivated your profile on +the main website." + +The `Kick` boolean is whether the removal should manifest to other users +in chat as a "kick" (sending a presence message of "has been kicked from +the room!"). By default (false), BareRTC will tell the user to disconnect +and it will manifest as a regular "has left the room" event to other online +chatters. + +The JSON response to this endpoint may look like: + +```json +{ + "OK": true, + "Removed": 1, + "Error": "if error, or this key is omitted if OK" +} +``` + +The "Removed" field is the count of users actually removed from chat; a zero +means the user was not presently online. + +# Ajax Endpoints (User API) + +## POST /api/profile + +Fetch profile information from your main website about a user in the +chat room. + +Note: this API request is done by the BareRTC chat front-end page, as an +ajax request for a current logged-in user. It backs the profile card pop-up +widget in the chat room when a user clicks on another user's profile. + +The request body payload looks like: + +```json +{ + "JWTToken": "the caller's chat jwt token", + "Username": "soandso" +} +``` + +The JWT token is the current chat user's token. This API only works when +your BareRTC config requires the use of JWT tokens for authorization. + +BareRTC will translate the request into the +["Profile Webhook"](Webhooks.md#Profile%20Webhook) to fetch the target +user's profile from your website. + +The response JSON given to the chat page from /api/profile looks like: + +```json +{ + "OK": true, + "Error": "only on error messages", + "ProfileFields": [ + { + "Name": "Age", + "Value": "30yo" + }, + { + "Name": "Gender", + "Value": "Man" + }, + ... + ] +} +``` \ No newline at end of file diff --git a/pkg/api.go b/pkg/api.go index 19c4adc..f123b4b 100644 --- a/pkg/api.go +++ b/pkg/api.go @@ -468,6 +468,134 @@ func (s *Server) BlockNow() http.HandlerFunc { }) } +// DisconnectNow (/api/disconnect/now) allows your website to remove a user from +// the chat room if they are currently online. +// +// For example: a user on your website has deactivated their account, and so +// should not be allowed to remain in the chat room. +// +// It is a POST request with a json body containing the following schema: +// +// { +// "APIKey": "from settings.toml", +// "Usernames": [ "alice", "bob" ], +// "Message": "An optional ChatServer message to send them first.", +// "Kick": false, +// } +// +// The `Message` parameter, if provided, will be sent to that user as a +// ChatServer error before they are removed from the room. You can use this +// to provide them context as to why they are being kicked. For example: +// "You have been logged out of chat because you deactivated your profile on +// the main website." +// +// The `Kick` boolean is whether the removal should manifest to other users +// in chat as a "kick" (sending a presence message of "has been kicked from +// the room!"). By default (false), BareRTC will tell the user to disconnect +// and it will manifest as a regular "has left the room" event to other online +// chatters. +func (s *Server) DisconnectNow() http.HandlerFunc { + type request struct { + APIKey string + Usernames []string + Message string + Kick bool + } + + type result struct { + OK bool + Removed int + 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(¶ms); 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 disconnect them from the chat. + var removed int + for _, username := range params.Usernames { + if sub, err := s.GetSubscriber(username); err == nil { + // Broadcast to everybody that the user left the chat. + message := messages.PresenceExited + if params.Kick { + message = messages.PresenceKicked + } + s.Broadcast(messages.Message{ + Action: messages.ActionPresence, + Username: username, + Message: message, + }) + + // Custom message to send to them? + if params.Message != "" { + sub.ChatServer(params.Message) + } + + // Disconnect them. + sub.SendJSON(messages.Message{ + Action: messages.ActionKick, + }) + sub.authenticated = false + sub.Username = "" + + removed++ + } + } + + // If any changes to blocklists were made: send the Who List. + if removed > 0 { + s.SendWhoList() + } + + enc.Encode(result{ + OK: true, + Removed: removed, + }) + }) +} + // UserProfile (/api/profile) fetches profile information about a user. // // This endpoint will proxy to your WebhookURL for the "profile" endpoint. diff --git a/pkg/commands.go b/pkg/commands.go index 06e4298..5fd2689 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -160,7 +160,7 @@ func (s *Server) KickCommand(words []string, sub *Subscriber) { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: username, - Message: "has been kicked from the room!", + Message: messages.PresenceKicked, }) } } @@ -237,7 +237,7 @@ func (s *Server) BanCommand(words []string, sub *Subscriber) { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: username, - Message: "has been banned!", + Message: messages.PresenceBanned, }) other.ChatServer("You have been banned from the chat room by %s. You may come back after %d hours.", sub.Username, duration/time.Hour) diff --git a/pkg/handlers.go b/pkg/handlers.go index a94be2c..330ad1c 100644 --- a/pkg/handlers.go +++ b/pkg/handlers.go @@ -97,7 +97,7 @@ func (s *Server) OnLogin(sub *Subscriber, msg messages.Message) { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: msg.Username, - Message: "has joined the room!", + Message: messages.PresenceJoined, }) // Send the user back their settings. @@ -379,14 +379,14 @@ func (s *Server) OnMe(sub *Subscriber, msg messages.Message) { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: sub.Username, - Message: "has exited the room!", + Message: messages.PresenceExited, }) } else if sub.ChatStatus == "hidden" && msg.ChatStatus != "hidden" { // Leaving hidden - fake join message s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: sub.Username, - Message: "has joined the room!", + Message: messages.PresenceJoined, }) } } else if msg.ChatStatus == "hidden" { diff --git a/pkg/messages/messages.go b/pkg/messages/messages.go index d5862ac..3f813c8 100644 --- a/pkg/messages/messages.go +++ b/pkg/messages/messages.go @@ -131,3 +131,12 @@ const ( VideoFlagMutualOpen // viewer wants to auto-open viewers' cameras VideoFlagOnlyVIP // can only shows as active to VIP members ) + +// Presence message templates. +const ( + PresenceJoined = "has joined the room!" + PresenceExited = "has exited the room!" + PresenceKicked = "has been kicked from the room!" + PresenceBanned = "has been banned!" + PresenceTimedOut = "has timed out!" +) diff --git a/pkg/polling_api.go b/pkg/polling_api.go index 7c85fde..ca443e8 100644 --- a/pkg/polling_api.go +++ b/pkg/polling_api.go @@ -65,7 +65,7 @@ func (s *Server) KickIdlePollUsers() { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: sub.Username, - Message: "has timed out!", + Message: messages.PresenceTimedOut, }) s.SendWhoList() } diff --git a/pkg/server.go b/pkg/server.go index eab6228..a6e4c35 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -42,6 +42,7 @@ func (s *Server) Setup() error { mux.Handle("/api/statistics", s.Statistics()) mux.Handle("/api/blocklist", s.BlockList()) mux.Handle("/api/block/now", s.BlockNow()) + mux.Handle("/api/disconnect/now", s.DisconnectNow()) mux.Handle("/api/authenticate", s.Authenticate()) mux.Handle("/api/shutdown", s.ShutdownAPI()) mux.Handle("/api/profile", s.UserProfile()) diff --git a/pkg/websocket.go b/pkg/websocket.go index 8661943..b56c256 100644 --- a/pkg/websocket.go +++ b/pkg/websocket.go @@ -99,7 +99,7 @@ func (s *Server) NewPollingSubscriber(ctx context.Context, cancelFunc func()) *S s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: sub.Username, - Message: "has exited the room!", + Message: messages.PresenceExited, }) s.SendWhoList() } @@ -167,7 +167,7 @@ func (sub *Subscriber) ReadLoop(s *Server) { s.Broadcast(messages.Message{ Action: messages.ActionPresence, Username: sub.Username, - Message: "has exited the room!", + Message: messages.PresenceExited, }) s.SendWhoList() }