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.
193 lines
5.1 KiB
193 lines
5.1 KiB
package telemost
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const defaultAPIBaseURL = "https://cloud-api.yandex.ru/telemost_front/v2/telemost"
|
|
|
|
var (
|
|
apiBaseURL = defaultAPIBaseURL
|
|
apiHTTPClient = &http.Client{Timeout: 15 * time.Second}
|
|
)
|
|
|
|
type ConnectionInfo struct {
|
|
RoomID string `json:"room_id"`
|
|
PeerID string `json:"peer_id"`
|
|
Credentials string `json:"credentials"`
|
|
ClientConfig struct {
|
|
MediaServerURL string `json:"media_server_url"`
|
|
} `json:"client_configuration"`
|
|
}
|
|
|
|
type RoomTarget struct {
|
|
RoomID string
|
|
PreferredHost string
|
|
}
|
|
|
|
func ParseRoomTarget(input string) (RoomTarget, error) {
|
|
input = strings.TrimSpace(input)
|
|
if input == "" {
|
|
return RoomTarget{}, fmt.Errorf("telemost invite link is required")
|
|
}
|
|
|
|
target := RoomTarget{}
|
|
if strings.HasPrefix(input, "https://") || strings.HasPrefix(input, "http://") {
|
|
parsed, err := url.Parse(input)
|
|
if err != nil {
|
|
return RoomTarget{}, fmt.Errorf("invalid telemost invite link: %w", err)
|
|
}
|
|
parts := strings.Split(strings.Trim(parsed.Path, "/"), "/")
|
|
if len(parts) >= 2 && parts[0] == "j" && parts[1] != "" {
|
|
target.RoomID = parts[1]
|
|
target.PreferredHost = strings.ToLower(parsed.Hostname())
|
|
}
|
|
}
|
|
|
|
if target.RoomID == "" {
|
|
input = strings.TrimPrefix(input, "https://telemost.yandex.ru/j/")
|
|
input = strings.TrimPrefix(input, "http://telemost.yandex.ru/j/")
|
|
input = strings.TrimPrefix(input, "https://telemost.yandex.com/j/")
|
|
input = strings.TrimPrefix(input, "http://telemost.yandex.com/j/")
|
|
input = strings.TrimPrefix(input, "j/")
|
|
if idx := strings.IndexAny(input, "/?#"); idx != -1 {
|
|
input = input[:idx]
|
|
}
|
|
target.RoomID = strings.TrimSpace(input)
|
|
}
|
|
|
|
if target.RoomID == "" {
|
|
return RoomTarget{}, fmt.Errorf("invalid telemost invite link")
|
|
}
|
|
|
|
return target, nil
|
|
}
|
|
|
|
func (t RoomTarget) CandidateRoomURLs() []string {
|
|
hosts := make([]string, 0, 3)
|
|
addHost := func(host string) {
|
|
host = strings.ToLower(strings.TrimSpace(host))
|
|
if host == "" {
|
|
return
|
|
}
|
|
for _, existing := range hosts {
|
|
if existing == host {
|
|
return
|
|
}
|
|
}
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
addHost(t.PreferredHost)
|
|
addHost("telemost.yandex.ru")
|
|
addHost("telemost.yandex.com")
|
|
|
|
roomURLs := make([]string, 0, len(hosts))
|
|
for _, host := range hosts {
|
|
roomURLs = append(roomURLs, fmt.Sprintf("https://%s/j/%s", host, t.RoomID))
|
|
}
|
|
|
|
return roomURLs
|
|
}
|
|
|
|
func WebOriginFromRoomURL(roomURL string) string {
|
|
parsed, err := url.Parse(roomURL)
|
|
if err != nil || parsed.Host == "" {
|
|
return "https://telemost.yandex.ru"
|
|
}
|
|
|
|
scheme := parsed.Scheme
|
|
if scheme == "" {
|
|
scheme = "https"
|
|
}
|
|
|
|
return scheme + "://" + parsed.Host
|
|
}
|
|
|
|
func getConnectionInfoForRoomURL(ctx context.Context, roomURL, displayName string) (*ConnectionInfo, error) {
|
|
u := fmt.Sprintf("%s/conferences/%s/connection", apiBaseURL, url.QueryEscape(roomURL))
|
|
origin := WebOriginFromRoomURL(roomURL)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
q := req.URL.Query()
|
|
q.Add("next_gen_media_platform_allowed", "true")
|
|
q.Add("display_name", displayName)
|
|
q.Add("waiting_room_supported", "true")
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:149.0) Gecko/20100101 Firefox/149.0")
|
|
req.Header.Set("Accept", "*/*")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Client-Instance-Id", uuid.New().String())
|
|
req.Header.Set("X-Telemost-Client-Version", "187.1.0")
|
|
req.Header.Set("Idempotency-Key", uuid.New().String())
|
|
req.Header.Set("Origin", origin)
|
|
req.Header.Set("Referer", origin+"/")
|
|
|
|
resp, err := apiHTTPClient.Do(req)
|
|
if err != nil {
|
|
if resp != nil && resp.Body != nil {
|
|
if closeErr := resp.Body.Close(); closeErr != nil {
|
|
log.Println(closeErr)
|
|
}
|
|
}
|
|
return nil, err
|
|
}
|
|
defer func(Body io.ReadCloser) {
|
|
err := Body.Close()
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}(resp.Body)
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, readErr := io.ReadAll(resp.Body)
|
|
if readErr != nil {
|
|
return nil, fmt.Errorf("telemost connection API returned %s and response body read failed: %w", resp.Status, readErr)
|
|
}
|
|
return nil, fmt.Errorf("telemost connection API returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
|
}
|
|
|
|
var info ConnectionInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &info, nil
|
|
}
|
|
|
|
func GetConnectionInfo(ctx context.Context, roomInput, displayName string) (*ConnectionInfo, string, error) {
|
|
target, err := ParseRoomTarget(roomInput)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
var errs []string
|
|
for _, roomURL := range target.CandidateRoomURLs() {
|
|
info, err := getConnectionInfoForRoomURL(ctx, roomURL, displayName)
|
|
if err == nil {
|
|
return info, roomURL, nil
|
|
}
|
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
return nil, "", err
|
|
}
|
|
errs = append(errs, fmt.Sprintf("%s: %v", roomURL, err))
|
|
}
|
|
|
|
return nil, "", fmt.Errorf("telemost connection failed for room %s: %s", target.RoomID, strings.Join(errs, "; "))
|
|
}
|
|
|