2023-12-12 18:30:51 +00:00
|
|
|
package rumblelivestreamlib
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
2024-02-09 16:11:18 +00:00
|
|
|
"net/http/cookiejar"
|
2023-12-12 18:30:51 +00:00
|
|
|
"net/url"
|
|
|
|
"strings"
|
2024-01-19 17:54:34 +00:00
|
|
|
"sync"
|
2023-12-12 18:30:51 +00:00
|
|
|
|
|
|
|
"github.com/robertkrimen/otto"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-12-12 20:32:45 +00:00
|
|
|
domain = "rumble.com"
|
|
|
|
urlWeb = "https://" + domain
|
2024-02-08 17:25:30 +00:00
|
|
|
urlAccount = urlWeb + "/account/"
|
2023-12-12 18:30:51 +00:00
|
|
|
urlGetSalts = urlWeb + "/service.php?name=user.get_salts"
|
|
|
|
urlUserLogin = urlWeb + "/service.php?name=user.login"
|
|
|
|
urlUserLogout = urlWeb + "/service.php?name=user.logout"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
2024-04-15 18:36:05 +00:00
|
|
|
httpClient *http.Client
|
|
|
|
chatInfo *ChatInfo
|
|
|
|
chatStream *ChatStream
|
|
|
|
chatStreamMu sync.Mutex
|
|
|
|
ApiKey string
|
|
|
|
LiveStreamUrl string
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2023-12-12 20:32:45 +00:00
|
|
|
func (c *Client) cookies() ([]*http.Cookie, error) {
|
2023-12-12 18:30:51 +00:00
|
|
|
u, err := url.Parse(urlWeb)
|
|
|
|
if err != nil {
|
2023-12-12 20:32:45 +00:00
|
|
|
return nil, fmt.Errorf("error parsing domain: %v", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
2023-12-12 20:32:45 +00:00
|
|
|
return c.httpClient.Jar.Cookies(u), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) PrintCookies() error {
|
|
|
|
cookies, err := c.cookies()
|
|
|
|
if err != nil {
|
|
|
|
return pkgErr("error getting cookies", err)
|
|
|
|
}
|
|
|
|
fmt.Println("Cookies:", len(cookies))
|
|
|
|
for _, cookie := range cookies {
|
|
|
|
fmt.Println(cookie.String())
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
2023-12-12 20:32:45 +00:00
|
|
|
|
|
|
|
return nil
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
type NewClientOptions struct {
|
2024-04-15 18:36:05 +00:00
|
|
|
Cookies []*http.Cookie `json:"cookies"`
|
|
|
|
ApiKey string `json:"stream_key"`
|
|
|
|
LiveStreamUrl string `json:"stream_url"`
|
2024-02-08 17:25:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient(opts NewClientOptions) (*Client, error) {
|
|
|
|
cl, err := newHttpClient(opts.Cookies)
|
2023-12-12 18:30:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, pkgErr("error creating http client", err)
|
|
|
|
}
|
|
|
|
|
2024-04-15 18:36:05 +00:00
|
|
|
return &Client{httpClient: cl, ApiKey: opts.ApiKey, LiveStreamUrl: opts.LiveStreamUrl}, nil
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
func newHttpClient(cookies []*http.Cookie) (*http.Client, error) {
|
2023-12-12 18:30:51 +00:00
|
|
|
jar, err := cookiejar.New(nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error creating cookiejar: %v", err)
|
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
url, err := url.Parse(urlWeb)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error parsing domain: %v", err)
|
|
|
|
}
|
|
|
|
jar.SetCookies(url, cookies)
|
|
|
|
|
2023-12-12 18:30:51 +00:00
|
|
|
return &http.Client{Jar: jar}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type GetSaltsData struct {
|
|
|
|
Salts []string `json:"salts"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type GetSaltsResponse struct {
|
|
|
|
Data GetSaltsData `json:"data"`
|
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
func (c *Client) Login(username string, password string) ([]*http.Cookie, error) {
|
2023-12-12 18:30:51 +00:00
|
|
|
if c.httpClient == nil {
|
2024-02-08 17:25:30 +00:00
|
|
|
return nil, pkgErr("", fmt.Errorf("http client is nil"))
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
salts, err := c.getSalts(username)
|
|
|
|
if err != nil {
|
2024-02-08 17:25:30 +00:00
|
|
|
return nil, pkgErr("error getting salts", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2024-02-09 16:09:18 +00:00
|
|
|
cookies, err := c.userLogin(username, password, salts)
|
2023-12-12 18:30:51 +00:00
|
|
|
if err != nil {
|
2024-02-08 17:25:30 +00:00
|
|
|
return nil, pkgErr("error logging in", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
return cookies, nil
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2023-12-12 20:32:45 +00:00
|
|
|
func (c *Client) getWebpage(url string) (*http.Response, error) {
|
|
|
|
resp, err := c.httpClient.Get(url)
|
2023-12-12 18:30:51 +00:00
|
|
|
if err != nil {
|
2023-12-12 20:32:45 +00:00
|
|
|
return nil, fmt.Errorf("http Get request returned error: %v", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
2023-12-12 20:32:45 +00:00
|
|
|
resp.Body.Close()
|
|
|
|
return nil, fmt.Errorf("http Get response status not %s: %s", http.StatusText(http.StatusOK), resp.Status)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2023-12-12 20:32:45 +00:00
|
|
|
return resp, nil
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) getSalts(username string) ([]string, error) {
|
|
|
|
u := url.URL{}
|
|
|
|
q := u.Query()
|
|
|
|
q.Add("username", username)
|
|
|
|
body := q.Encode()
|
|
|
|
resp, err := c.httpClient.Post(urlGetSalts, "application/x-www-form-urlencoded", strings.NewReader(body))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("http Post request returned error: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return nil, fmt.Errorf("http Post response status not %s: %s", http.StatusText(http.StatusOK), resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyB, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error reading body bytes: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var gsr GetSaltsResponse
|
|
|
|
err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&gsr)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error decoding response body from server: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return gsr.Data.Salts, nil
|
|
|
|
}
|
|
|
|
|
2024-01-05 17:00:50 +00:00
|
|
|
type DataBool struct {
|
|
|
|
Session bool `json:"session"`
|
2024-01-05 16:21:44 +00:00
|
|
|
}
|
|
|
|
|
2024-01-05 17:00:50 +00:00
|
|
|
type LoginResponseBool struct {
|
|
|
|
Data DataBool `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type DataString struct {
|
|
|
|
Session string `json:"session"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type LoginResponseString struct {
|
|
|
|
Data DataString `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func loginResponseSession(body []byte) (string, error) {
|
|
|
|
bodyS := string(body)
|
|
|
|
|
|
|
|
var lrs LoginResponseString
|
|
|
|
err := json.NewDecoder(strings.NewReader(bodyS)).Decode(&lrs)
|
|
|
|
if err == nil {
|
|
|
|
return lrs.Data.Session, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var lrb LoginResponseBool
|
|
|
|
err = json.NewDecoder(strings.NewReader(bodyS)).Decode(&lrb)
|
|
|
|
if err == nil {
|
|
|
|
return "false", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", fmt.Errorf("error decoding login response")
|
2024-01-05 16:21:44 +00:00
|
|
|
}
|
|
|
|
|
2024-02-09 16:09:18 +00:00
|
|
|
func (c *Client) userLogin(username string, password string, salts []string) ([]*http.Cookie, error) {
|
2023-12-12 18:30:51 +00:00
|
|
|
hashes, err := generateHashes(password, salts)
|
|
|
|
if err != nil {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("error generating password hashes: %v", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
u := url.URL{}
|
|
|
|
q := u.Query()
|
|
|
|
q.Add("username", username)
|
|
|
|
q.Add("password_hashes", hashes)
|
|
|
|
body := q.Encode()
|
|
|
|
resp, err := c.httpClient.Post(urlUserLogin, "application/x-www-form-urlencoded", strings.NewReader(body))
|
|
|
|
if err != nil {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("http Post request returned error: %v", err)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
2024-02-09 16:09:18 +00:00
|
|
|
|
2023-12-12 18:30:51 +00:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("http Post response status not %s: %s", http.StatusText(http.StatusOK), resp.Status)
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
2024-01-05 16:21:44 +00:00
|
|
|
bodyB, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("error reading body bytes: %v", err)
|
2024-01-05 16:21:44 +00:00
|
|
|
}
|
|
|
|
|
2024-01-05 17:00:50 +00:00
|
|
|
session, err := loginResponseSession(bodyB)
|
2024-01-05 16:21:44 +00:00
|
|
|
if err != nil {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("error getting login response session: %v", err)
|
2024-01-05 16:21:44 +00:00
|
|
|
}
|
|
|
|
|
2024-01-05 17:00:50 +00:00
|
|
|
if session == "false" {
|
2024-02-09 16:09:18 +00:00
|
|
|
return nil, fmt.Errorf("failed to log in")
|
2024-01-05 16:21:44 +00:00
|
|
|
}
|
|
|
|
|
2024-02-09 16:09:18 +00:00
|
|
|
return resp.Cookies(), nil
|
2023-12-12 18:30:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func generateHashes(password string, salts []string) (string, error) {
|
|
|
|
vm := otto.New()
|
|
|
|
|
|
|
|
vm.Set("password", password)
|
|
|
|
vm.Set("salt0", salts[0])
|
|
|
|
vm.Set("salt1", salts[1])
|
|
|
|
vm.Set("salt2", salts[2])
|
|
|
|
|
|
|
|
_, err := vm.Run(md5)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error running md5 javascript: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
value, err := vm.Get("hashes")
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error getting hashes value: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
hashes, err := value.ToString()
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("error converting hashes value to string: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return hashes, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Logout() error {
|
|
|
|
if c.httpClient == nil {
|
|
|
|
return pkgErr("", fmt.Errorf("http client is nil"))
|
|
|
|
}
|
|
|
|
|
|
|
|
err := c.userLogout()
|
|
|
|
if err != nil {
|
|
|
|
return pkgErr("error logging out", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) userLogout() error {
|
|
|
|
resp, err := c.httpClient.Get(urlUserLogout)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("http Get request returned error: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
return fmt.Errorf("http Get response status not %s: %s", http.StatusText(http.StatusOK), resp.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2024-02-08 17:25:30 +00:00
|
|
|
|
2024-02-08 21:05:23 +00:00
|
|
|
type LoggedInResponseUser struct {
|
|
|
|
LoggedIn bool `json:"logged_in"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type LoggedInResponse struct {
|
|
|
|
User LoggedInResponseUser `json:"user"`
|
|
|
|
}
|
|
|
|
|
2024-02-08 17:25:30 +00:00
|
|
|
func (c *Client) LoggedIn() (bool, error) {
|
2024-02-08 21:05:23 +00:00
|
|
|
resp, err := c.httpClient.Get(urlUserLogin)
|
2024-02-08 17:25:30 +00:00
|
|
|
if err != nil {
|
2024-02-09 16:09:18 +00:00
|
|
|
return false, pkgErr("error getting login service", err)
|
2024-02-08 17:25:30 +00:00
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
2024-02-08 21:05:23 +00:00
|
|
|
bodyB, err := io.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return false, pkgErr("error reading body bytes", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
var lir LoggedInResponse
|
|
|
|
err = json.NewDecoder(strings.NewReader(string(bodyB))).Decode(&lir)
|
|
|
|
if err != nil {
|
|
|
|
return false, pkgErr("error un-marshaling response body", err)
|
2024-02-08 17:25:30 +00:00
|
|
|
}
|
|
|
|
|
2024-02-08 21:05:23 +00:00
|
|
|
return lir.User.LoggedIn, nil
|
2024-02-08 17:25:30 +00:00
|
|
|
}
|