package wbstream import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "github.com/openlibrecommunity/olcrtc/internal/protect" ) var apiBase = "https://stream.wb.ru" //nolint:gochecknoglobals // package-level state intentional var ( errGuestRegister = errors.New("guest register failed") errCreateRoom = errors.New("create room failed") errJoinRoom = errors.New("join room failed") errGetToken = errors.New("get token failed") ) type guestRegisterRequest struct { DisplayName string `json:"displayName"` Device device `json:"device"` } type device struct { DeviceName string `json:"deviceName"` DeviceType string `json:"deviceType"` } type guestRegisterResponse struct { AccessToken string `json:"accessToken"` } type createRoomRequest struct { RoomType string `json:"roomType"` RoomPrivacy string `json:"roomPrivacy"` } type createRoomResponse struct { RoomID string `json:"roomId"` } type tokenResponse struct { RoomToken string `json:"roomToken"` ServerURL string `json:"serverUrl"` } func registerGuest(ctx context.Context, displayName string) (string, error) { u := apiBase + "/auth/api/v1/auth/user/guest-register" reqBody := guestRegisterRequest{ DisplayName: displayName, Device: device{ DeviceName: "Linux", DeviceType: "PARTICIPANT_DEVICE_TYPE_WEB_DESKTOP", }, } body, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("marshal request body: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(body)) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (Linux x86_64)") client := protect.NewHTTPClient() resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("do request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("%w: %d %s", errGuestRegister, resp.StatusCode, b) } var res guestRegisterResponse if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return "", fmt.Errorf("decode response: %w", err) } return res.AccessToken, nil } func createRoom(ctx context.Context, accessToken string) (string, error) { u := apiBase + "/api-room/api/v2/room" reqBody := createRoomRequest{ RoomType: "ROOM_TYPE_ALL_ON_SCREEN", RoomPrivacy: "ROOM_PRIVACY_FREE", } body, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("marshal request body: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewBuffer(body)) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("User-Agent", "Mozilla/5.0 (Linux x86_64)") client := protect.NewHTTPClient() resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("do request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { b, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("%w: %d %s", errCreateRoom, resp.StatusCode, b) } var res createRoomResponse if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return "", fmt.Errorf("decode response: %w", err) } return res.RoomID, nil } // CreateRoom registers a temporary guest, creates a WB Stream room, and returns its id. func CreateRoom(ctx context.Context, displayName string) (string, error) { accessToken, err := registerGuest(ctx, displayName) if err != nil { return "", fmt.Errorf("register guest: %w", err) } roomID, err := createRoom(ctx, accessToken) if err != nil { return "", fmt.Errorf("create room: %w", err) } return roomID, nil } func joinRoom(ctx context.Context, accessToken, roomID string) error { u := fmt.Sprintf("%s/api-room/api/v1/room/%s/join", apiBase, roomID) req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader([]byte("{}"))) if err != nil { return fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("User-Agent", "Mozilla/5.0 (Linux x86_64)") client := protect.NewHTTPClient() resp, err := client.Do(req) if err != nil { return fmt.Errorf("do request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return fmt.Errorf("%w: %d %s", errJoinRoom, resp.StatusCode, b) } return nil } func getToken(ctx context.Context, accessToken, roomID, displayName string) (string, error) { u := fmt.Sprintf("%s/api-room-manager/v2/room/%s/connection-details", apiBase, roomID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return "", fmt.Errorf("create request: %w", err) } q := req.URL.Query() q.Add("deviceType", "PARTICIPANT_DEVICE_TYPE_WEB_DESKTOP") q.Add("displayName", displayName) req.URL.RawQuery = q.Encode() req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("User-Agent", "Mozilla/5.0 (Linux x86_64)") client := protect.NewHTTPClient() resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("do request: %w", err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("%w: %d %s", errGetToken, resp.StatusCode, b) } var res tokenResponse if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return "", fmt.Errorf("decode response: %w", err) } return res.RoomToken, nil }