BareRTC/client/client.go
Noah Petherbridge 9c77bdb62e New Op commands and fixes with blocking admin users
Add moderation rules:

* You can apply rules in the settings.toml to enforce moderator restrictions on
  certain users, e.g. to force their camera to always be NSFW or bar them from
  sharing their webcam at all anymore.

Chat UI improvements around users blocking admin accounts:

* When a main website block is in place, the DMs button in the Who List shows
  as greyed out with a cross through, as if that user had closed their DMs.
* Admin users are always able to watch the camera of people who have blocked
  them. The broadcaster is not notified about the watch.

New operator commands:

* /cut username: to tell a user to turn off their webcam.
* /unmute-all: to lift all mutes on your side, e.g. so your moderator chatbot
  can still see public messages from users who have blocked it.
* /help-advanced: moved the more dangerous admin command documentation here.

Miscellaneous fixes:

* The admin commands now tolerate an @ prefix in front of usernames.
* The /nsfw command won't fire unless the user's camera is actually active and
  not marked as explicit.
2024-05-17 17:15:48 -07:00

240 lines
6.0 KiB
Go

// Package client provides Go WebSocket client support for BareRTC.
package client
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"git.kirsle.net/apps/barertc/client/config"
"git.kirsle.net/apps/barertc/pkg/jwt"
"git.kirsle.net/apps/barertc/pkg/log"
"git.kirsle.net/apps/barertc/pkg/messages"
"nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
)
// HandlerFunc for WebSocket chat protocol events.
type HandlerFunc func(messages.Message)
// Client represents a WebSocket client connection to BareRTC.
type Client struct {
// Event handlers for your app to respond to.
OnWho HandlerFunc // Who's Online
OnMe HandlerFunc // Status updates for current user sent by server
OnMessage HandlerFunc
OnTakeback HandlerFunc
OnReact HandlerFunc
OnPresence HandlerFunc
OnRing HandlerFunc
OnOpen HandlerFunc
OnWatch HandlerFunc
OnUnwatch HandlerFunc
OnCut HandlerFunc
OnError HandlerFunc
OnDisconnect HandlerFunc
OnPing HandlerFunc
OnCandidate HandlerFunc
OnSDP HandlerFunc
// Private state variables.
url string
jwt string // JWT token
claims jwt.Claims
ctx context.Context
conn *websocket.Conn
}
// NewClient initializes the WebSocket connection (JWT claims required).
//
// URL is like ws://localhost:9000/ws
func NewClient(url string, claims jwt.Claims) (*Client, error) {
// Sanity check the claims.
if claims.Subject == "" {
return nil, errors.New("missing Subject field of JWT claims")
}
return &Client{
url: url,
claims: claims,
}, nil
}
// Run the client, connecting to the WebSocket and returning only on error or disconnect.
func (c *Client) Run() error {
// Authenticate.
if token, err := c.Authenticate(); err != nil {
return fmt.Errorf("didn't get JWT token from BareRTC: %s", err)
} else {
c.jwt = token
}
// Get the WebSocket URL.
wss, err := WebSocketURL(c.url)
if err != nil {
return fmt.Errorf("couldn't get WebSocket URL from %s: %s", c.url, err)
}
ctx := context.Background()
c.ctx = ctx
conn, _, err := websocket.Dial(ctx, wss, nil)
if err != nil {
return fmt.Errorf("dialing websocket URL (%s): %s", c.url, err)
}
c.conn = conn
defer conn.Close(websocket.StatusInternalError, "the sky is falling")
conn.SetReadLimit(config.Current.WebSocketReadLimit)
// Authenticate via JWT token.
if err := c.Send(messages.Message{
Action: messages.ActionLogin,
Username: "testbot",
JWTToken: c.jwt,
}); err != nil {
return fmt.Errorf("sending login message: %s", err)
}
// Enter the Read Loop
for {
var msg messages.Message
err := wsjson.Read(c.ctx, c.conn, &msg)
if err != nil {
log.Error("wsjson.Read: %s", err)
break
}
// Handle the various protocol messages.
switch msg.Action {
case messages.ActionWhoList:
c.Handle(msg, c.OnWho)
case messages.ActionMe:
c.Handle(msg, c.OnMe)
case messages.ActionMessage:
c.Handle(msg, c.OnMessage)
case messages.ActionReact:
c.Handle(msg, c.OnReact)
case messages.ActionPresence:
c.Handle(msg, c.OnPresence)
case messages.ActionRing:
c.Handle(msg, c.OnRing)
case messages.ActionOpen:
c.Handle(msg, c.OnOpen)
case messages.ActionWatch:
c.Handle(msg, c.OnWatch)
case messages.ActionUnwatch:
c.Handle(msg, c.OnUnwatch)
case messages.ActionCut:
c.Handle(msg, c.OnCut)
case messages.ActionError:
c.Handle(msg, c.OnError)
case messages.ActionKick:
c.Handle(msg, c.OnDisconnect)
case messages.ActionPing:
c.Handle(msg, c.OnPing)
case messages.ActionCandidate:
c.Handle(msg, c.OnCandidate)
case messages.ActionSDP:
c.Handle(msg, c.OnSDP)
default:
log.Error("Unsupported chat protocol message type: %s", msg.Action)
}
}
conn.Close(websocket.StatusNormalClosure, "")
return errors.New("disconnected")
}
// Send a WebSocket message.
func (c *Client) Send(msg messages.Message) error {
return wsjson.Write(c.ctx, c.conn, msg)
}
// Username returns the bot's username.
func (c *Client) Username() string {
return c.claims.Subject
}
// Handle a WebSocket message. This is called internally on the read loop.
// It basically passes the message into the HandlerFunc, or returns an
// error if the HandlerFunc is nil (not defined).
//
// Note: handler funcs are run on a background goroutine, so they can be
// free to use time.Sleep and delay message sending if needed.
func (c *Client) Handle(msg messages.Message, fn HandlerFunc) error {
if fn == nil {
return fmt.Errorf("no handler set for '%s' messages", msg.Action)
}
go fn(msg)
return nil
}
// Authenticate with the BareRTC server, returning a signed JWT token.
//
// This posts to the /api/authenticate endpoint on the BareRTC Web API. It
// is called automatically as part of the logon process in Run().
func (c *Client) Authenticate() (string, error) {
// API request struct for BareRTC /api/blocklist endpoint.
var request = struct {
APIKey string
Claims jwt.Claims
}{
APIKey: config.Current.BareRTC.AdminAPIKey,
Claims: c.claims,
}
// Response struct
type response struct {
OK bool
Error string
JWT string
}
// JSON request body.
jsonStr, err := json.Marshal(request)
if err != nil {
return "", err
}
// Make the API request to BareRTC.
var url = strings.TrimSuffix(config.Current.BareRTC.URL, "/") + "/api/authenticate"
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("SendBlocklist: error syncing blocklist to BareRTC: status %d body %s", resp.StatusCode, body)
}
// Return the signed JWT token.
var result response
if err := json.Unmarshal(body, &result); err != nil {
return "", err
}
if result.JWT == "" {
return "", errors.New("did not get JWT token from BareRTC")
}
return result.JWT, err
}