You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
205 lines
5.7 KiB
205 lines
5.7 KiB
package jazz
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cacggghp/vk-turn-proxy/internal/namegen"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const defaultAPIBaseURL = "https://bk.salutejazz.ru"
|
|
|
|
var (
|
|
apiBaseURL = defaultAPIBaseURL
|
|
apiHTTPClient = &http.Client{Timeout: 15 * time.Second}
|
|
)
|
|
|
|
var (
|
|
errCreateRoomFailed = errors.New("create room failed")
|
|
errPreconnectFailed = errors.New("preconnect failed")
|
|
)
|
|
|
|
type RoomInfo struct {
|
|
RoomID string `json:"roomId"`
|
|
Password string `json:"password"`
|
|
ConnectorURL string `json:"connectorUrl"`
|
|
}
|
|
|
|
func CreateRoom(ctx context.Context) (*RoomInfo, error) {
|
|
headers := jazzHeaders()
|
|
|
|
createResp, err := createMeeting(ctx, headers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create meeting: %w", err)
|
|
}
|
|
|
|
connectorURL, err := preconnect(ctx, createResp.RoomID, createResp.Password, headers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("preconnect: %w", err)
|
|
}
|
|
|
|
return &RoomInfo{
|
|
RoomID: createResp.RoomID,
|
|
Password: createResp.Password,
|
|
ConnectorURL: connectorURL,
|
|
}, nil
|
|
}
|
|
|
|
func JoinRoom(ctx context.Context, roomInput string) (*RoomInfo, error) {
|
|
roomID, password := parseRoomInput(roomInput)
|
|
if roomID == "" {
|
|
return nil, fmt.Errorf("jazz room is required")
|
|
}
|
|
|
|
connectorURL, err := preconnect(ctx, roomID, password, jazzHeaders())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &RoomInfo{
|
|
RoomID: roomID,
|
|
Password: password,
|
|
ConnectorURL: connectorURL,
|
|
}, nil
|
|
}
|
|
|
|
func parseRoomInput(roomInput string) (string, string) {
|
|
roomInput = strings.TrimSpace(roomInput)
|
|
roomInput = strings.TrimPrefix(roomInput, "https://salutejazz.ru/")
|
|
roomInput = strings.TrimPrefix(roomInput, "http://salutejazz.ru/")
|
|
roomInput = strings.TrimPrefix(roomInput, "https://jazz.sber.ru/")
|
|
roomInput = strings.TrimPrefix(roomInput, "http://jazz.sber.ru/")
|
|
if idx := strings.IndexAny(roomInput, "/?#"); idx != -1 {
|
|
roomInput = roomInput[:idx]
|
|
}
|
|
|
|
roomID, password, _ := strings.Cut(roomInput, ":")
|
|
return strings.TrimSpace(roomID), strings.TrimSpace(password)
|
|
}
|
|
|
|
func jazzHeaders() map[string]string {
|
|
return map[string]string{
|
|
"X-Jazz-ClientId": uuid.New().String(),
|
|
"X-Jazz-AuthType": "ANONYMOUS",
|
|
"X-Client-AuthType": "ANONYMOUS",
|
|
"Content-Type": "application/json",
|
|
}
|
|
}
|
|
|
|
type createResponse struct {
|
|
RoomID string `json:"roomId"`
|
|
Password string `json:"password"`
|
|
}
|
|
|
|
func createMeeting(ctx context.Context, headers map[string]string) (*createResponse, error) {
|
|
createPayload := map[string]any{
|
|
"title": namegen.Generate() + " ДР",
|
|
"guestEnabled": true,
|
|
"lobbyEnabled": false,
|
|
"serverVideoRecordAutoStartEnabled": false,
|
|
"sipEnabled": false,
|
|
"moderatorEmails": []string{},
|
|
"summarizationEnabled": false,
|
|
"room3dEnabled": false,
|
|
"room3dScene": "XRLobby",
|
|
}
|
|
|
|
body, err := json.Marshal(createPayload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal create payload: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiBaseURL+"/room/create-meeting", bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
setHeaders(req, headers)
|
|
|
|
resp, err := apiHTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("do create request: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, statusError(errCreateRoomFailed, resp)
|
|
}
|
|
|
|
var res createResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
|
return nil, fmt.Errorf("decode create response: %w", err)
|
|
}
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
func preconnect(ctx context.Context, roomID, password string, headers map[string]string) (string, error) {
|
|
preconnectPayload := map[string]any{
|
|
"password": password,
|
|
"jazzNextMigration": map[string]any{
|
|
"b2bBaseRoomSupport": true,
|
|
"demoRoomBaseSupport": true,
|
|
"demoRoomVersionSupport": 2,
|
|
"mediaWithoutAutoSubscribeSupport": true,
|
|
"webinarSpeakerSupport": true,
|
|
"webinarViewerSupport": true,
|
|
"sdkRoomSupport": true,
|
|
"sberclassRoomSupport": true,
|
|
},
|
|
}
|
|
|
|
body, err := json.Marshal(preconnectPayload)
|
|
if err != nil {
|
|
return "", fmt.Errorf("marshal preconnect payload: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("%s/room/%s/preconnect", apiBaseURL, roomID), bytes.NewReader(body))
|
|
if err != nil {
|
|
return "", fmt.Errorf("create preconnect request: %w", err)
|
|
}
|
|
setHeaders(req, headers)
|
|
|
|
resp, err := apiHTTPClient.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("do preconnect request: %w", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", statusError(errPreconnectFailed, resp)
|
|
}
|
|
|
|
var preconnectResp struct {
|
|
ConnectorURL string `json:"connectorUrl"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&preconnectResp); err != nil {
|
|
return "", fmt.Errorf("decode preconnect response: %w", err)
|
|
}
|
|
if preconnectResp.ConnectorURL == "" {
|
|
return "", fmt.Errorf("preconnect response missing connector URL")
|
|
}
|
|
|
|
return preconnectResp.ConnectorURL, nil
|
|
}
|
|
|
|
func setHeaders(req *http.Request, headers map[string]string) {
|
|
for key, value := range headers {
|
|
req.Header.Set(key, value)
|
|
}
|
|
}
|
|
|
|
func statusError(base error, resp *http.Response) error {
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: status %s and response body read failed: %v", base, resp.Status, err)
|
|
}
|
|
return fmt.Errorf("%w: status %s: %s", base, resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
|