mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-10 16:15:43 +00:00
666 lines
19 KiB
Go
666 lines
19 KiB
Go
package bitrix
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/sagernet/sing-box/transport/call/common"
|
|
"github.com/sagernet/sing/common/logger"
|
|
N "github.com/sagernet/sing/common/network"
|
|
|
|
"github.com/google/uuid"
|
|
headless "github.com/kulikov0/headless-client"
|
|
)
|
|
|
|
const (
|
|
slbBase = "https://slb.bitrix24.tech/v2/join"
|
|
signalSDK = "js"
|
|
signalVersion = "1.6.7"
|
|
signalProtocol = "8"
|
|
clientVersion = "1.0.0"
|
|
clientPlatform = "web"
|
|
callTypeInstant = 2
|
|
roomTypeSmall = 1
|
|
)
|
|
|
|
var (
|
|
reConferenceID = regexp.MustCompile(`conferenceId:\s*'([^']+)'`)
|
|
reChatID = regexp.MustCompile(`chatId:\s*'([^']+)'`)
|
|
reAlias = regexp.MustCompile(`alias:\s*'([^']+)'`)
|
|
reSessid = regexp.MustCompile(`bitrix_sessid["']?\s*[:=]\s*["']([a-f0-9]{8,})`)
|
|
reUserID = regexp.MustCompile(`"USER_ID"\s*:\s*"?(\d+)`)
|
|
)
|
|
|
|
type Client struct {
|
|
HTTP *http.Client
|
|
Logger logger.ContextLogger
|
|
|
|
portal string
|
|
sessid string
|
|
userAgent string
|
|
instanceID string
|
|
email string
|
|
password string
|
|
chatID string
|
|
conferenceID string
|
|
selfUserID string
|
|
}
|
|
|
|
type JoinResult struct {
|
|
RoomData string `json:"roomData"`
|
|
RoomID string `json:"roomId"`
|
|
MediaServerURL string `json:"mediaServerUrl"`
|
|
}
|
|
|
|
type conferenceParams struct {
|
|
ConferenceID string
|
|
ChatID string
|
|
Alias string
|
|
}
|
|
|
|
type callInfo struct {
|
|
UUID string
|
|
ChatID string
|
|
CallToken string
|
|
UserToken string
|
|
Alias string
|
|
GuestLink string
|
|
}
|
|
|
|
type PullConfigResult struct {
|
|
WebSocket string
|
|
ChannelID string
|
|
Hostname string
|
|
Revision int
|
|
}
|
|
|
|
type extraHeader struct {
|
|
Name string
|
|
Value string
|
|
}
|
|
|
|
type slbRequest struct {
|
|
UserToken string `json:"userToken"`
|
|
IsOneToOne bool `json:"isOneToOne"`
|
|
ClientVersion string `json:"clientVersion"`
|
|
ClientPlatform string `json:"clientPlatform"`
|
|
CallType int `json:"callType"`
|
|
RoomType int `json:"roomType"`
|
|
InstanceID string `json:"instanceId"`
|
|
CallToken string `json:"callToken"`
|
|
Provider string `json:"provider"`
|
|
IsVideo bool `json:"isVideo"`
|
|
CallUUID string `json:"callUuid"`
|
|
}
|
|
|
|
func NewClient(portal, userAgent string, dialer N.Dialer, log logger.ContextLogger) (*Client, error) {
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if log == nil {
|
|
log = logger.NOP()
|
|
}
|
|
httpClient := common.HttpClient(dialer)
|
|
httpClient.Jar = jar
|
|
return &Client{
|
|
HTTP: httpClient,
|
|
Logger: log,
|
|
portal: strings.TrimRight(portal, "/"),
|
|
userAgent: userAgent,
|
|
instanceID: uuid.New().String(),
|
|
}, nil
|
|
}
|
|
|
|
func SignalURL(res JoinResult) string {
|
|
q := url.Values{}
|
|
q.Set("auto_subscribe", "1")
|
|
q.Set("sdk", signalSDK)
|
|
q.Set("version", signalVersion)
|
|
q.Set("protocol", signalProtocol)
|
|
q.Set("roomData", res.RoomData)
|
|
q.Set("clientVersion", clientVersion)
|
|
q.Set("clientPlatform", clientPlatform)
|
|
return res.MediaServerURL + "?" + q.Encode()
|
|
}
|
|
|
|
func (c *Client) JoinAsGuest(alias, displayName string) (JoinResult, error) {
|
|
var res JoinResult
|
|
conf, err := c.fetchConference(alias)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
c.chatID = conf.ChatID
|
|
c.conferenceID = conf.ConferenceID
|
|
userToken, err := c.registerGuest(conf, displayName)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
info, err := c.tryJoinCall(conf.ChatID)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
info.UserToken = userToken
|
|
return c.slbJoin(info, false)
|
|
}
|
|
|
|
func (c *Client) JoinAsHost(alias string) (JoinResult, error) {
|
|
var res JoinResult
|
|
conf, err := c.fetchConference(alias)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[host] conference chatId=%s conferenceId=%s", conf.ChatID, conf.ConferenceID))
|
|
c.chatID = conf.ChatID
|
|
callToken, userToken, err := c.getCallToken(conf.ChatID)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
info := callInfo{
|
|
UUID: uuid.New().String(),
|
|
ChatID: conf.ChatID,
|
|
CallToken: callToken,
|
|
UserToken: userToken,
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[host] creating media room callUUID=%s callToken=%dB userToken=%dB", info.UUID, len(info.CallToken), len(info.UserToken)))
|
|
return c.slbJoin(info, true)
|
|
}
|
|
|
|
func (c *Client) CreateAndJoin() (JoinResult, string, error) {
|
|
var res JoinResult
|
|
info, err := c.createRoom()
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
c.chatID = info.ChatID
|
|
guestLink := info.GuestLink
|
|
if guestLink == "" {
|
|
guestLink, err = c.getGuestLink(info.ChatID)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
}
|
|
if info.CallToken == "" {
|
|
callToken, userToken, err := c.getCallToken(info.ChatID)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
info.CallToken = callToken
|
|
if info.UserToken == "" {
|
|
info.UserToken = userToken
|
|
}
|
|
}
|
|
res, err = c.slbJoin(info, true)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
return res, guestLink, nil
|
|
}
|
|
|
|
func (c *Client) resetJar() error {
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.HTTP.Jar = jar
|
|
c.sessid = ""
|
|
return nil
|
|
}
|
|
|
|
func secFetchSiteFor(endpoint, origin string) string {
|
|
endpointURL, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return "cross-site"
|
|
}
|
|
originURL, err := url.Parse(origin)
|
|
if err != nil {
|
|
return "cross-site"
|
|
}
|
|
if endpointURL.Host == originURL.Host {
|
|
return "same-origin"
|
|
}
|
|
return "cross-site"
|
|
}
|
|
|
|
func (c *Client) do(method, endpoint, contentType string, body io.Reader, extraHeaders ...extraHeader) ([]byte, int, error) {
|
|
req, err := http.NewRequest(method, endpoint, body)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
req.Header = headless.ChromeWindows.Headers(headless.DestEmpty)
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
req.Header.Set("Accept", "application/json, text/plain, */*")
|
|
if c.portal != "" {
|
|
req.Header.Set("Origin", c.portal)
|
|
req.Header.Set("Referer", c.portal+"/")
|
|
req.Header.Set("Sec-Fetch-Site", secFetchSiteFor(endpoint, c.portal))
|
|
}
|
|
if contentType != "" {
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
for _, h := range extraHeaders {
|
|
req.Header.Set(h.Name, h.Value)
|
|
}
|
|
resp, err := c.HTTP.Do(req)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
data, err := io.ReadAll(resp.Body)
|
|
return data, resp.StatusCode, err
|
|
}
|
|
|
|
func (c *Client) restForm(action string, form url.Values) ([]byte, int, error) {
|
|
endpoint := c.portal + "/rest/" + action + ".json"
|
|
return c.do("POST", endpoint, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()))
|
|
}
|
|
|
|
func (c *Client) restFormAuthed(action string, form url.Values) ([]byte, int, error) {
|
|
endpoint := c.portal + "/rest/" + action + ".json"
|
|
var extraHeaders []extraHeader
|
|
if c.sessid != "" {
|
|
form.Set("sessid", c.sessid)
|
|
endpoint += "?sessid=" + url.QueryEscape(c.sessid)
|
|
extraHeaders = append(extraHeaders, extraHeader{Name: "X-Bitrix-Csrf-Token", Value: c.sessid})
|
|
}
|
|
return c.do("POST", endpoint, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()), extraHeaders...)
|
|
}
|
|
|
|
func (c *Client) ajaxAction(action string, form url.Values) ([]byte, int, error) {
|
|
endpoint := c.portal + "/bitrix/services/main/ajax.php?action=" + action
|
|
var extraHeaders []extraHeader
|
|
if c.sessid != "" {
|
|
endpoint += "&sessid=" + url.QueryEscape(c.sessid)
|
|
form.Set("sessid", c.sessid)
|
|
extraHeaders = append(extraHeaders, extraHeader{Name: "X-Bitrix-Csrf-Token", Value: c.sessid})
|
|
}
|
|
return c.do("POST", endpoint, "application/x-www-form-urlencoded", strings.NewReader(form.Encode()), extraHeaders...)
|
|
}
|
|
|
|
func (c *Client) fetchConference(alias string) (conferenceParams, error) {
|
|
var p conferenceParams
|
|
body, status, err := c.do("GET", c.portal+"/video/"+alias, "", nil)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
if status != 200 {
|
|
return p, fmt.Errorf("fetch conference page: status %d", status)
|
|
}
|
|
page := string(body)
|
|
if m := reConferenceID.FindStringSubmatch(page); m != nil {
|
|
p.ConferenceID = m[1]
|
|
}
|
|
if m := reChatID.FindStringSubmatch(page); m != nil {
|
|
p.ChatID = m[1]
|
|
}
|
|
if m := reAlias.FindStringSubmatch(page); m != nil {
|
|
p.Alias = m[1]
|
|
} else {
|
|
p.Alias = alias
|
|
}
|
|
if m := reSessid.FindStringSubmatch(page); m != nil {
|
|
c.sessid = m[1]
|
|
}
|
|
if p.ConferenceID == "" || p.ChatID == "" {
|
|
return p, fmt.Errorf("conference params not found in page (conferenceId=%q chatId=%q)", p.ConferenceID, p.ChatID)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (c *Client) registerGuest(p conferenceParams, displayName string) (userToken string, err error) {
|
|
form := url.Values{}
|
|
form.Set("call_auth_id", "guest")
|
|
form.Set("videoconf_id", p.ConferenceID)
|
|
form.Set("call_chat_id", p.ChatID)
|
|
form.Set("alias", p.Alias)
|
|
form.Set("user_hash", "")
|
|
if displayName != "" {
|
|
form.Set("name", displayName)
|
|
}
|
|
body, status, err := c.restForm("call.user.register", form)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var out struct {
|
|
Result struct {
|
|
UserToken string `json:"userToken"`
|
|
} `json:"result"`
|
|
Error string `json:"error"`
|
|
ErrorDescription string `json:"error_description"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return "", fmt.Errorf("register guest: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Result.UserToken == "" {
|
|
return "", fmt.Errorf("register guest: no userToken (status %d, err %s %s)", status, out.Error, out.ErrorDescription)
|
|
}
|
|
return out.Result.UserToken, nil
|
|
}
|
|
|
|
func (c *Client) tryJoinCall(chatID string) (callInfo, error) {
|
|
var info callInfo
|
|
form := url.Values{}
|
|
form.Set("entityType", "chat")
|
|
form.Set("entityId", "chat"+chatID)
|
|
form.Set("provider", "Bitrix")
|
|
form.Set("callType", fmt.Sprintf("%d", callTypeInstant))
|
|
body, status, err := c.withRelogin(func() ([]byte, int, error) {
|
|
return c.ajaxAction("call.Call.tryJoinCall", form)
|
|
})
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
Errors []struct {
|
|
Message string `json:"message"`
|
|
Code any `json:"code"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return info, fmt.Errorf("tryJoinCall: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
var data struct {
|
|
Success bool `json:"success"`
|
|
Call struct {
|
|
UUID string `json:"UUID"`
|
|
ChatID json.Number `json:"CHAT_ID"`
|
|
} `json:"call"`
|
|
CallToken string `json:"callToken"`
|
|
}
|
|
if err := json.Unmarshal(out.Data, &data); err != nil && out.Status == "success" {
|
|
return info, fmt.Errorf("tryJoinCall: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Status == "success" && !data.Success {
|
|
return info, fmt.Errorf("tryJoinCall: no active call at chat%s, organizer not in the room", chatID)
|
|
}
|
|
if out.Status != "success" || data.Call.UUID == "" {
|
|
return info, fmt.Errorf("tryJoinCall failed (status %d, %s, errors %+v, body %s)", status, out.Status, out.Errors, common.BodySnippet(body))
|
|
}
|
|
info.UUID = data.Call.UUID
|
|
info.ChatID = data.Call.ChatID.String()
|
|
info.CallToken = data.CallToken
|
|
return info, nil
|
|
}
|
|
|
|
func (c *Client) slbJoin(info callInfo, mustCreate bool) (JoinResult, error) {
|
|
var res JoinResult
|
|
reqBody := slbRequest{
|
|
UserToken: info.UserToken,
|
|
IsOneToOne: false,
|
|
ClientVersion: clientVersion,
|
|
ClientPlatform: clientPlatform,
|
|
CallType: callTypeInstant,
|
|
RoomType: roomTypeSmall,
|
|
InstanceID: c.instanceID,
|
|
CallToken: info.CallToken,
|
|
Provider: "Bitrix",
|
|
IsVideo: true,
|
|
CallUUID: info.UUID,
|
|
}
|
|
payload, _ := json.Marshal(reqBody)
|
|
endpoint := slbBase + "?mustCreate=" + strconv.FormatBool(mustCreate)
|
|
body, status, err := c.do("POST", endpoint, "text/plain;charset=UTF-8", strings.NewReader(string(payload)))
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
var out struct {
|
|
Result JoinResult `json:"result"`
|
|
Error json.RawMessage `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return res, fmt.Errorf("slb join: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Result.MediaServerURL == "" || out.Result.RoomData == "" {
|
|
return res, fmt.Errorf("slb join: missing roomData/mediaServerUrl (status %d, err %s)", status, string(out.Error))
|
|
}
|
|
return out.Result, nil
|
|
}
|
|
|
|
func (c *Client) currentUserID() (string, error) {
|
|
body, status, err := c.do("GET", c.portal+"/online/", "", nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if m := reUserID.FindSubmatch(body); m != nil && string(m[1]) != "0" {
|
|
return string(m[1]), nil
|
|
}
|
|
return "", fmt.Errorf("current user id not found (status %d)", status)
|
|
}
|
|
|
|
func (c *Client) createRoom() (callInfo, error) {
|
|
var info callInfo
|
|
uid, err := c.currentUserID()
|
|
if err != nil {
|
|
return info, fmt.Errorf("createRoom: %w", err)
|
|
}
|
|
form := url.Values{}
|
|
form.Set("fields[entityType]", "VIDEOCONF")
|
|
form.Set("fields[title]", "")
|
|
form.Set("fields[memberEntities][0][0]", "user")
|
|
form.Set("fields[memberEntities][0][1]", uid)
|
|
form.Set("fields[ownerId]", uid)
|
|
form.Set("fields[description]", "")
|
|
form.Set("fields[manageUsersAdd]", "member")
|
|
form.Set("fields[manageUsersDelete]", "manager")
|
|
form.Set("fields[manageUi]", "member")
|
|
form.Set("fields[manageMessages]", "member")
|
|
form.Set("fields[conferencePassword]", "")
|
|
body, status, err := c.withRelogin(func() ([]byte, int, error) {
|
|
return c.ajaxAction("im.v2.Chat.add", form)
|
|
})
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
Errors []struct {
|
|
Code any `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return info, fmt.Errorf("createRoom: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
var data struct {
|
|
ChatID json.Number `json:"chatId"`
|
|
Alias string `json:"alias"`
|
|
Link string `json:"link"`
|
|
}
|
|
if err := json.Unmarshal(out.Data, &data); err == nil {
|
|
info.ChatID = data.ChatID.String()
|
|
info.Alias = data.Alias
|
|
info.GuestLink = data.Link
|
|
}
|
|
if info.ChatID == "" {
|
|
errMsg := ""
|
|
if len(out.Errors) > 0 {
|
|
errMsg = fmt.Sprintf("%v %s", out.Errors[0].Code, out.Errors[0].Message)
|
|
}
|
|
return info, fmt.Errorf("createRoom: no chatId (status %d, status=%q err=%q, body %s)", status, out.Status, errMsg, common.BodySnippet(body))
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[create] videoconf chatId=%s alias=%s", info.ChatID, info.Alias))
|
|
return info, nil
|
|
}
|
|
|
|
func (c *Client) KickUser(userID string) error {
|
|
if c.chatID == "" {
|
|
return fmt.Errorf("kick: no active chatId")
|
|
}
|
|
if userID == "" {
|
|
return fmt.Errorf("kick: empty userId")
|
|
}
|
|
form := url.Values{}
|
|
form.Set("chatId", c.chatID)
|
|
form.Set("userId", userID)
|
|
body, status, err := c.withRelogin(func() ([]byte, int, error) {
|
|
return c.ajaxAction("im.v2.Chat.deleteUser", form)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Errors []struct {
|
|
Code any `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"errors"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return fmt.Errorf("kick: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if len(out.Errors) > 0 {
|
|
return fmt.Errorf("kick: chatId=%s userId=%s rejected (status %d, %v %s)", c.chatID, userID, status, out.Errors[0].Code, out.Errors[0].Message)
|
|
}
|
|
c.Logger.Debug(fmt.Sprintf("[kick] removed userId=%s from chatId=%s", userID, c.chatID))
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) SelfUserID() (string, error) {
|
|
if c.selfUserID != "" {
|
|
return c.selfUserID, nil
|
|
}
|
|
id, err := c.currentUserID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
c.selfUserID = id
|
|
return id, nil
|
|
}
|
|
|
|
func (c *Client) callHash() string {
|
|
if c.HTTP == nil || c.HTTP.Jar == nil {
|
|
return ""
|
|
}
|
|
u, err := url.Parse(c.portal)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
for _, ck := range c.HTTP.Jar.Cookies(u) {
|
|
if ck.Name == "BITRIX_CALL_HASH" {
|
|
return ck.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *Client) PullConfig() (PullConfigResult, error) {
|
|
var pc PullConfigResult
|
|
form := url.Values{}
|
|
form.Set("CACHE", "N")
|
|
var body []byte
|
|
var status int
|
|
var err error
|
|
if hash := c.callHash(); hash != "" {
|
|
form.Set("call_auth_id", hash)
|
|
form.Set("videoconf_id", c.conferenceID)
|
|
form.Set("call_chat_id", c.chatID)
|
|
body, status, err = c.restForm("pull.config.get", form)
|
|
} else {
|
|
body, status, err = c.withRelogin(func() ([]byte, int, error) {
|
|
return c.restFormAuthed("pull.config.get", form)
|
|
})
|
|
}
|
|
if err != nil {
|
|
return pc, err
|
|
}
|
|
var out struct {
|
|
Result struct {
|
|
Server struct {
|
|
WebSocket string `json:"websocket"`
|
|
} `json:"server"`
|
|
API struct {
|
|
RevisionWeb int `json:"revision_web"`
|
|
} `json:"api"`
|
|
Channels struct {
|
|
Shared struct {
|
|
ID string `json:"id"`
|
|
} `json:"shared"`
|
|
Private struct {
|
|
ID string `json:"id"`
|
|
} `json:"private"`
|
|
} `json:"channels"`
|
|
} `json:"result"`
|
|
Error string `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return pc, fmt.Errorf("pull config: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
if out.Result.Server.WebSocket == "" || out.Result.Channels.Private.ID == "" {
|
|
return pc, fmt.Errorf("pull config: missing websocket/channel (status %d, body %s)", status, common.BodySnippet(body))
|
|
}
|
|
pc.WebSocket = out.Result.Server.WebSocket
|
|
pc.ChannelID = out.Result.Channels.Private.ID
|
|
if out.Result.Channels.Shared.ID != "" {
|
|
pc.ChannelID += "/" + out.Result.Channels.Shared.ID
|
|
}
|
|
pc.Revision = out.Result.API.RevisionWeb
|
|
pc.Hostname = c.portal
|
|
if u, perr := url.Parse(c.portal); perr == nil && u.Host != "" {
|
|
pc.Hostname = u.Host
|
|
}
|
|
return pc, nil
|
|
}
|
|
|
|
func (c *Client) getGuestLink(chatID string) (string, error) {
|
|
form := url.Values{}
|
|
form.Set("chatId", chatID)
|
|
body, status, err := c.withRelogin(func() ([]byte, int, error) {
|
|
return c.ajaxAction("call.Call.getGuestLink", form)
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return "", fmt.Errorf("getGuestLink: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
var data struct {
|
|
GuestLink string `json:"guestLink"`
|
|
}
|
|
if err := json.Unmarshal(out.Data, &data); err != nil || data.GuestLink == "" {
|
|
return "", fmt.Errorf("getGuestLink: empty (status %d, body %s)", status, common.BodySnippet(body))
|
|
}
|
|
return data.GuestLink, nil
|
|
}
|
|
|
|
func (c *Client) getCallToken(chatID string) (callToken, userToken string, err error) {
|
|
form := url.Values{}
|
|
form.Set("chatId", chatID)
|
|
body, status, err := c.withRelogin(func() ([]byte, int, error) {
|
|
return c.ajaxAction("call.Call.getCallToken", form)
|
|
})
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
var out struct {
|
|
Status string `json:"status"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &out); err != nil {
|
|
return "", "", fmt.Errorf("getCallToken: %w (status %d, body %s)", err, status, common.BodySnippet(body))
|
|
}
|
|
var data struct {
|
|
CallToken string `json:"callToken"`
|
|
UserToken string `json:"userToken"`
|
|
}
|
|
if err := json.Unmarshal(out.Data, &data); err != nil || data.CallToken == "" {
|
|
return "", "", fmt.Errorf("getCallToken: empty (status %d, body %s)", status, common.BodySnippet(body))
|
|
}
|
|
return data.CallToken, data.UserToken, nil
|
|
}
|