mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-08-24 13:04:56 -04:00
241 lines
7.4 KiB
Go
241 lines
7.4 KiB
Go
|
|
package voip
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/url"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
const FocusWellKnownKey = "org.matrix.msc4143.rtc_foci"
|
||
|
|
|
||
|
|
var ErrNoLiveKitFocus = errors.New("voip: no LiveKit MatrixRTC focus found")
|
||
|
|
|
||
|
|
type Focus struct {
|
||
|
|
Type string `json:"type"`
|
||
|
|
LiveKitServiceURL string `json:"livekit_service_url"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type WellKnownClient struct {
|
||
|
|
RTCFoci []Focus `json:"org.matrix.msc4143.rtc_foci"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func DiscoverLiveKitFocus(ctx context.Context, httpClient *http.Client, serverName, overrideURL string) (*Focus, error) {
|
||
|
|
if overrideURL != "" {
|
||
|
|
if err := validateHTTPSURL(overrideURL); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid configured livekit service URL: %w", err)
|
||
|
|
}
|
||
|
|
return &Focus{Type: "livekit", LiveKitServiceURL: overrideURL}, nil
|
||
|
|
}
|
||
|
|
if serverName == "" {
|
||
|
|
return nil, fmt.Errorf("matrix server name is required")
|
||
|
|
}
|
||
|
|
if strings.Contains(serverName, "://") {
|
||
|
|
return nil, fmt.Errorf("matrix server name must not include a scheme")
|
||
|
|
}
|
||
|
|
if httpClient == nil {
|
||
|
|
httpClient = http.DefaultClient
|
||
|
|
}
|
||
|
|
wellKnownURL := "https://" + serverName + "/.well-known/matrix/client"
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnownURL, nil)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
resp, err := httpClient.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to fetch Matrix client well-known: %w", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
|
|
return nil, fmt.Errorf("Matrix client well-known returned HTTP %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
var wellKnown WellKnownClient
|
||
|
|
if err = json.NewDecoder(resp.Body).Decode(&wellKnown); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to decode Matrix client well-known: %w", err)
|
||
|
|
}
|
||
|
|
for _, focus := range wellKnown.RTCFoci {
|
||
|
|
if focus.Type != "livekit" || focus.LiveKitServiceURL == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if err = validateHTTPSURL(focus.LiveKitServiceURL); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid livekit focus URL in well-known: %w", err)
|
||
|
|
}
|
||
|
|
return &focus, nil
|
||
|
|
}
|
||
|
|
return nil, ErrNoLiveKitFocus
|
||
|
|
}
|
||
|
|
|
||
|
|
type MatrixOpenIDToken struct {
|
||
|
|
AccessToken string `json:"access_token"`
|
||
|
|
TokenType string `json:"token_type"`
|
||
|
|
MatrixServerName string `json:"matrix_server_name"`
|
||
|
|
ExpiresIn int `json:"expires_in"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type LiveKitAuthRequest struct {
|
||
|
|
RoomID string `json:"room_id,omitempty"`
|
||
|
|
SlotID string `json:"slot_id,omitempty"`
|
||
|
|
OpenIDToken MatrixOpenIDToken `json:"openid_token"`
|
||
|
|
Member *LiveKitAuthMember `json:"member,omitempty"`
|
||
|
|
DeviceID string `json:"device_id,omitempty"`
|
||
|
|
SessionID string `json:"session_id,omitempty"`
|
||
|
|
ParticipantID string `json:"participant_id,omitempty"`
|
||
|
|
FocusType string `json:"focus_type,omitempty"`
|
||
|
|
Extra map[string]any `json:"extra,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type LegacyLiveKitAuthRequest struct {
|
||
|
|
Room string `json:"room"`
|
||
|
|
OpenIDToken MatrixOpenIDToken `json:"openid_token"`
|
||
|
|
DeviceID string `json:"device_id"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type LiveKitAuthMember struct {
|
||
|
|
ID string `json:"id,omitempty"`
|
||
|
|
ClaimedDeviceID string `json:"claimed_device_id,omitempty"`
|
||
|
|
ClaimedUserID string `json:"claimed_user_id,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type LiveKitAuthResponse struct {
|
||
|
|
URL string `json:"url,omitempty"`
|
||
|
|
Token string `json:"token,omitempty"`
|
||
|
|
JWTToken string `json:"jwt,omitempty"`
|
||
|
|
RoomName string `json:"room,omitempty"`
|
||
|
|
|
||
|
|
ServerURL string `json:"server_url,omitempty"`
|
||
|
|
LiveKitURL string `json:"livekit_url,omitempty"`
|
||
|
|
AccessToken string `json:"access_token,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (resp LiveKitAuthResponse) ConnectionURL() string {
|
||
|
|
for _, candidate := range []string{resp.URL, resp.ServerURL, resp.LiveKitURL} {
|
||
|
|
if candidate != "" {
|
||
|
|
return candidate
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func (resp LiveKitAuthResponse) JWT() string {
|
||
|
|
for _, candidate := range []string{resp.Token, resp.JWTToken, resp.AccessToken} {
|
||
|
|
if candidate != "" {
|
||
|
|
return candidate
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func RequestLiveKitAuth(ctx context.Context, httpClient *http.Client, liveKitServiceURL string, authReq LiveKitAuthRequest) (*LiveKitAuthResponse, error) {
|
||
|
|
if err := validateHTTPSURL(liveKitServiceURL); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid livekit service URL: %w", err)
|
||
|
|
}
|
||
|
|
if httpClient == nil {
|
||
|
|
httpClient = http.DefaultClient
|
||
|
|
}
|
||
|
|
body, err := json.Marshal(authReq)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
var lastErr error
|
||
|
|
for _, endpoint := range liveKitAuthEndpoints(liveKitServiceURL) {
|
||
|
|
resp, err := postLiveKitAuth(ctx, httpClient, endpoint, body)
|
||
|
|
if err != nil {
|
||
|
|
lastErr = err
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if resp.ConnectionURL() == "" || resp.JWT() == "" {
|
||
|
|
return nil, fmt.Errorf("livekit auth response did not include both URL and token")
|
||
|
|
}
|
||
|
|
return resp, nil
|
||
|
|
}
|
||
|
|
if lastErr != nil {
|
||
|
|
return nil, lastErr
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("livekit auth did not try any endpoints")
|
||
|
|
}
|
||
|
|
|
||
|
|
func RequestLegacyLiveKitAuth(ctx context.Context, httpClient *http.Client, liveKitServiceURL string, authReq LegacyLiveKitAuthRequest) (*LiveKitAuthResponse, error) {
|
||
|
|
if err := validateHTTPSURL(liveKitServiceURL); err != nil {
|
||
|
|
return nil, fmt.Errorf("invalid livekit service URL: %w", err)
|
||
|
|
}
|
||
|
|
if httpClient == nil {
|
||
|
|
httpClient = http.DefaultClient
|
||
|
|
}
|
||
|
|
body, err := json.Marshal(authReq)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
resp, err := postLiveKitAuth(ctx, httpClient, legacyLiveKitAuthEndpoint(liveKitServiceURL), body)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if resp.ConnectionURL() == "" || resp.JWT() == "" {
|
||
|
|
return nil, fmt.Errorf("livekit auth response did not include both URL and token")
|
||
|
|
}
|
||
|
|
return resp, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func liveKitAuthEndpoints(rawURL string) []string {
|
||
|
|
trimmed := strings.TrimRight(rawURL, "/")
|
||
|
|
if strings.HasSuffix(trimmed, "/get_token") || strings.HasSuffix(trimmed, "/sfu/get") {
|
||
|
|
return []string{trimmed}
|
||
|
|
}
|
||
|
|
return []string{
|
||
|
|
trimmed + "/get_token",
|
||
|
|
trimmed + "/sfu/get",
|
||
|
|
trimmed,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func legacyLiveKitAuthEndpoint(rawURL string) string {
|
||
|
|
trimmed := strings.TrimRight(rawURL, "/")
|
||
|
|
if strings.HasSuffix(trimmed, "/sfu/get") {
|
||
|
|
return trimmed
|
||
|
|
}
|
||
|
|
if strings.HasSuffix(trimmed, "/get_token") {
|
||
|
|
return strings.TrimSuffix(trimmed, "/get_token") + "/sfu/get"
|
||
|
|
}
|
||
|
|
return trimmed + "/sfu/get"
|
||
|
|
}
|
||
|
|
|
||
|
|
func postLiveKitAuth(ctx context.Context, httpClient *http.Client, liveKitServiceURL string, body []byte) (*LiveKitAuthResponse, error) {
|
||
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, liveKitServiceURL, bytes.NewReader(body))
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
req.Header.Set("Content-Type", "application/json")
|
||
|
|
req.Header.Set("Accept", "application/json")
|
||
|
|
resp, err := httpClient.Do(req)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to request livekit token: %w", err)
|
||
|
|
}
|
||
|
|
defer resp.Body.Close()
|
||
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
|
|
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||
|
|
return nil, fmt.Errorf("livekit auth returned HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||
|
|
}
|
||
|
|
var authResp LiveKitAuthResponse
|
||
|
|
if err = json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
|
||
|
|
return nil, fmt.Errorf("failed to decode livekit auth response: %w", err)
|
||
|
|
}
|
||
|
|
return &authResp, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func validateHTTPSURL(rawURL string) error {
|
||
|
|
parsed, err := url.Parse(rawURL)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if parsed.Scheme != "https" && parsed.Scheme != "wss" {
|
||
|
|
return fmt.Errorf("URL must use https or wss")
|
||
|
|
}
|
||
|
|
if parsed.Host == "" {
|
||
|
|
return fmt.Errorf("URL must include a host")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|