diff --git a/src/main/java/ctbrec/io/CookieJarImpl.java b/src/main/java/ctbrec/io/CookieJarImpl.java index 712ff30c..8baf7a25 100644 --- a/src/main/java/ctbrec/io/CookieJarImpl.java +++ b/src/main/java/ctbrec/io/CookieJarImpl.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; @@ -78,5 +80,16 @@ public class CookieJarImpl implements CookieJar { return host; } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Entry> entry : cookieStore.entrySet()) { + sb.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); + } + return sb.toString(); + } + protected Map> getCookies() { + return cookieStore; + } } diff --git a/src/main/java/ctbrec/io/CookieJsonAdapter.java b/src/main/java/ctbrec/io/CookieJsonAdapter.java new file mode 100644 index 00000000..c94df9b6 --- /dev/null +++ b/src/main/java/ctbrec/io/CookieJsonAdapter.java @@ -0,0 +1,79 @@ +package ctbrec.io; + +import java.io.IOException; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.JsonReader; +import com.squareup.moshi.JsonWriter; + +import okhttp3.Cookie; +import okhttp3.Cookie.Builder; + +public class CookieJsonAdapter extends JsonAdapter { + + @Override + public Cookie fromJson(JsonReader reader) throws IOException { + Builder builder = new Cookie.Builder(); + // domain + reader.nextName(); + String domain = reader.nextString(); + builder.domain(domain); + + // expiresAt + reader.nextName(); + builder.expiresAt(reader.nextLong()); + + // host only + reader.nextName(); + if(reader.nextBoolean()) { + builder.hostOnlyDomain(domain); + } + + // http only + reader.nextName(); + if(reader.nextBoolean()) { + builder.httpOnly(); + } + + // name + reader.nextName(); + builder.name(reader.nextString()); + + // path + reader.nextName(); + builder.path(reader.nextString()); + + // persistent + reader.nextName(); + if(reader.nextBoolean()) { + // noop + } + + // secure + reader.nextName(); + if(reader.nextBoolean()) { + builder.secure(); + } + + // value + reader.nextName(); + builder.value(reader.nextString()); + + return builder.build(); + } + + @Override + public void toJson(JsonWriter writer, Cookie cookie) throws IOException { + writer.beginObject(); + writer.name("domain").value(cookie.domain()); + writer.name("expiresAt").value(cookie.expiresAt()); + writer.name("hostOnly").value(cookie.hostOnly()); + writer.name("httpOnly").value(cookie.httpOnly()); + writer.name("name").value(cookie.name()); + writer.name("path").value(cookie.path()); + writer.name("persistent").value(cookie.persistent()); + writer.name("secure").value(cookie.secure()); + writer.name("value").value(cookie.value()); + writer.endObject(); + } +} diff --git a/src/main/java/ctbrec/io/HttpClient.java b/src/main/java/ctbrec/io/HttpClient.java index 02c8f818..843d65a4 100644 --- a/src/main/java/ctbrec/io/HttpClient.java +++ b/src/main/java/ctbrec/io/HttpClient.java @@ -3,11 +3,20 @@ package ctbrec.io; import java.io.IOException; import java.net.Authenticator; import java.net.PasswordAuthentication; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; + import ctbrec.Config; import ctbrec.Settings.ProxyType; import okhttp3.ConnectionPool; +import okhttp3.Cookie; import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.OkHttpClient.Builder; @@ -16,12 +25,16 @@ import okhttp3.Response; import okhttp3.Route; public abstract class HttpClient { + private static final transient Logger LOG = LoggerFactory.getLogger(HttpClient.class); + protected OkHttpClient client; protected CookieJarImpl cookieJar = new CookieJarImpl(); protected boolean loggedIn = false; protected int loginTries = 0; + private String name; - protected HttpClient() { + protected HttpClient(String name) { + this.name = name; reconfigure(); } @@ -112,10 +125,23 @@ public abstract class HttpClient { } public void shutdown() { + persistCookies(); client.connectionPool().evictAll(); client.dispatcher().executorService().shutdown(); } + private void persistCookies() { + try { + Map> cookies = cookieJar.getCookies(); + Moshi moshi = new Moshi.Builder().add(Cookie.class, new CookieJsonAdapter()).build(); + @SuppressWarnings("rawtypes") + JsonAdapter adapter = moshi.adapter(Map.class).indent(" "); + String json = adapter.toJson(cookies); + } catch (Exception e) { + LOG.error("Couldn't persist cookies for {}", name, e); + } + } + private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) { return new okhttp3.Authenticator() { @Override diff --git a/src/main/java/ctbrec/recorder/server/HttpServer.java b/src/main/java/ctbrec/recorder/server/HttpServer.java index 00537280..5767b72e 100644 --- a/src/main/java/ctbrec/recorder/server/HttpServer.java +++ b/src/main/java/ctbrec/recorder/server/HttpServer.java @@ -20,6 +20,7 @@ import ctbrec.Config; import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.sites.Site; +import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; @@ -66,6 +67,7 @@ public class HttpServer { sites.add(new MyFreeCams()); sites.add(new Camsoda()); sites.add(new Cam4()); + sites.add(new BongaCams()); } private void addShutdownHook() { diff --git a/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java b/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java index 22027ba9..7b3170e7 100644 --- a/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java +++ b/src/main/java/ctbrec/recorder/server/RecorderHttpClient.java @@ -6,6 +6,10 @@ import ctbrec.io.HttpClient; public class RecorderHttpClient extends HttpClient { + public RecorderHttpClient() { + super("recorder"); + } + @Override public boolean login() throws IOException { return false; diff --git a/src/main/java/ctbrec/sites/bonga/BongaCams.java b/src/main/java/ctbrec/sites/bonga/BongaCams.java new file mode 100644 index 00000000..cead29b9 --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCams.java @@ -0,0 +1,150 @@ +package ctbrec.sites.bonga; + +import java.io.IOException; + +import org.json.JSONObject; + +import ctbrec.Config; +import ctbrec.Model; +import ctbrec.io.HttpClient; +import ctbrec.recorder.Recorder; +import ctbrec.sites.AbstractSite; +import ctbrec.sites.ConfigUI; +import ctbrec.ui.TabProvider; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class BongaCams extends AbstractSite { + + public static final String BASE_URL = "https://bongacams.com"; + + private BongaCamsHttpClient httpClient; + + private Recorder recorder; + + @Override + public String getName() { + return "BongaCams"; + } + + @Override + public String getBaseUrl() { + return BASE_URL; + } + + @Override + public String getAffiliateLink() { + return "http://bongacams2.com/track?c=610249"; + } + + @Override + public void setRecorder(Recorder recorder) { + this.recorder = recorder; + } + + @Override + public TabProvider getTabProvider() { + return new BongaCamsTabProvider(recorder, this); + } + + @Override + public Model createModel(String name) { + BongaCamsModel model = new BongaCamsModel(); + model.setName(name); + model.setUrl(BASE_URL + '/' + name); + model.setDescription(""); + model.setSite(this); + return model; + } + + @Override + public Integer getTokenBalance() throws IOException { + int userId = ((BongaCamsHttpClient)getHttpClient()).getUserId(); + String url = BongaCams.BASE_URL + "/tools/amf.php"; + RequestBody body = new FormBody.Builder() + .add("method", "ping") + .add("args[]", Integer.toString(userId)) + .build(); + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = getHttpClient().execute(request, true)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optString("status").equals("online")) { + JSONObject userData = json.getJSONObject("userData"); + return userData.getInt("balance"); + } else { + throw new IOException("Request was not successful: " + json.toString(2)); + } + } else { + throw new IOException(response.code() + " " + response.message()); + } + } + } + + @Override + public String getBuyTokensLink() { + return getAffiliateLink(); + } + + @Override + public void login() throws IOException { + getHttpClient().login(); + } + + @Override + public HttpClient getHttpClient() { + if(httpClient == null) { + httpClient = new BongaCamsHttpClient(); + } + return httpClient; + } + + @Override + public void init() throws IOException { + } + + @Override + public void shutdown() { + if(httpClient != null) { + httpClient.shutdown(); + } + } + + @Override + public boolean supportsTips() { + return true; + } + + @Override + public boolean supportsFollow() { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean isSiteForModel(Model m) { + return m instanceof BongaCamsModel; + } + + @Override + public ConfigUI getConfigurationGui() { + return new BongaCamsConfigUI(this); + } + + @Override + public boolean credentialsAvailable() { + String username = Config.getInstance().getSettings().bongaUsername; + return username != null && !username.trim().isEmpty(); + } + +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsConfigUI.java b/src/main/java/ctbrec/sites/bonga/BongaCamsConfigUI.java new file mode 100644 index 00000000..32ad77a3 --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsConfigUI.java @@ -0,0 +1,54 @@ +package ctbrec.sites.bonga; + +import ctbrec.Config; +import ctbrec.sites.ConfigUI; +import ctbrec.ui.DesktopIntergation; +import ctbrec.ui.SettingsTab; +import javafx.geometry.Insets; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +public class BongaCamsConfigUI implements ConfigUI { + + private BongaCams bongaCams; + + public BongaCamsConfigUI(BongaCams bongaCams) { + this.bongaCams = bongaCams; + } + + @Override + public Parent createConfigPanel() { + GridPane layout = SettingsTab.createGridLayout(); + layout.add(new Label("BongaCams User"), 0, 0); + TextField username = new TextField(Config.getInstance().getSettings().bongaUsername); + username.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaUsername = username.getText()); + GridPane.setFillWidth(username, true); + GridPane.setHgrow(username, Priority.ALWAYS); + GridPane.setColumnSpan(username, 2); + layout.add(username, 1, 0); + + layout.add(new Label("BongaCams Password"), 0, 1); + PasswordField password = new PasswordField(); + password.setText(Config.getInstance().getSettings().bongaPassword); + password.focusedProperty().addListener((e) -> Config.getInstance().getSettings().bongaPassword = password.getText()); + GridPane.setFillWidth(password, true); + GridPane.setHgrow(password, Priority.ALWAYS); + GridPane.setColumnSpan(password, 2); + layout.add(password, 1, 1); + + Button createAccount = new Button("Create new Account"); + createAccount.setOnAction((e) -> DesktopIntergation.open(bongaCams.getAffiliateLink())); + layout.add(createAccount, 1, 2); + GridPane.setColumnSpan(createAccount, 2); + GridPane.setMargin(username, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(password, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + GridPane.setMargin(createAccount, new Insets(0, 0, 0, SettingsTab.CHECKBOX_MARGIN)); + return layout; + } + +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsHttpClient.java b/src/main/java/ctbrec/sites/bonga/BongaCamsHttpClient.java new file mode 100644 index 00000000..26e08381 --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsHttpClient.java @@ -0,0 +1,208 @@ +package ctbrec.sites.bonga; + +import java.io.IOException; +import java.net.HttpCookie; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.io.HttpClient; +import javafx.application.Platform; +import okhttp3.Cookie; +import okhttp3.FormBody; +import okhttp3.HttpUrl; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class BongaCamsHttpClient extends HttpClient { + + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsHttpClient.class); + private int userId = 0; + + public BongaCamsHttpClient() { + super("bongacams"); + } + + @Override + public synchronized boolean login() throws IOException { + if(loggedIn) { + return true; + } + + BlockingQueue queue = new LinkedBlockingQueue<>(); + + Runnable showDialog = () -> { + // login with javafx WebView + BongaCamsLoginDialog loginDialog = new BongaCamsLoginDialog(); + + // transfer cookies from WebView to OkHttp cookie jar + transferCookies(loginDialog); + + try { + queue.put(true); + } catch (InterruptedException e) { + LOG.error("Error while signaling termination", e); + } + }; + + if(Platform.isFxApplicationThread()) { + showDialog.run(); + } else { + Platform.runLater(showDialog); + try { + queue.take(); + } catch (InterruptedException e) { + LOG.error("Error while waiting for login dialog to close", e); + throw new IOException(e); + } + } + + loggedIn = checkLoginSuccess(); + if(loggedIn) { + LOG.info("Logged in. User ID is {}", userId); + } else { + LOG.info("Login failed"); + } + return loggedIn; + } + + /** + * Check, if the login worked by requesting roomdata and looking + * @throws IOException + */ + private boolean checkLoginSuccess() throws IOException { + String modelName = getAnyModelName(); + // we request the roomData of a random model, because it contains + // user data, if the user is logged in, which we can use to verify, that the login worked + String url = BongaCams.BASE_URL + "/tools/amf.php"; + RequestBody body = new FormBody.Builder() + .add("method", "getRoomData") + .add("args[]", modelName) + .add("args[]", "false") + //.add("method", "ping") // TODO alternative request, but + //.add("args[]", ) // where to get the userId + .build(); + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optString("status").equals("success")) { + JSONObject userData = json.getJSONObject("userData"); + userId = userData.optInt("userId"); + return userId > 0; + } else { + throw new IOException("Request was not successful: " + json.toString(2)); + } + } else { + throw new IOException(response.code() + " " + response.message()); + } + } + } + + /** + * Fetches the list of online models and returns the name of the first model + */ + private String getAnyModelName() throws IOException { + Request request = new Request.Builder() + .url(BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=female&online_only=true&is_mobile=true&offset=0") + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + try(Response response = execute(request)) { + if (response.isSuccessful()) { + String content = response.body().string(); + JSONObject json = new JSONObject(content); + if(json.optString("status").equals("success")) { + JSONArray _models = json.getJSONArray("models"); + JSONObject m = _models.getJSONObject(0); + String name = m.getString("username"); + return name; + } else { + throw new IOException("Request was not successful: " + content); + } + } else { + throw new IOException(response.code() + ' ' + response.message()); + } + } + } + + private void transferCookies(BongaCamsLoginDialog loginDialog) { + HttpUrl redirectedUrl = HttpUrl.parse(loginDialog.getUrl()); + List cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(redirectedUrl, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(redirectedUrl, cookies); + + HttpUrl origUrl = HttpUrl.parse(BongaCamsLoginDialog.URL); + cookies = new ArrayList<>(); + for (HttpCookie webViewCookie : loginDialog.getCookies()) { + Cookie cookie = Cookie.parse(origUrl, webViewCookie.toString()); + cookies.add(cookie); + } + cookieJar.saveFromResponse(origUrl, cookies); + } + + // @Override + // public boolean login() throws IOException { + // String url = BongaCams.BASE_URL + "/login"; + // String dateTime = new SimpleDateFormat("d.MM.yyyy', 'HH:mm:ss").format(new Date()); + // RequestBody body = new FormBody.Builder() + // .add("security_log_additional_info","{\"language\":\"en\",\"cookieEnabled\":true,\"javaEnabled\":false,\"flashVersion\":\"31.0.0\",\"dateTime\":\""+dateTime+"\",\"ips\":[\"192.168.0.1\"]}") + // .add("log_in[username]", Config.getInstance().getSettings().bongaUsername) + // .add("log_in[password]", Config.getInstance().getSettings().bongaPassword) + // .add("log_in[remember]", "1") + // .add("log_in[bfpt]", "") + // .add("header_form", "1") + // .build(); + // Request request = new Request.Builder() + // .url(url) + // .post(body) + // .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + // .addHeader("Accept","application/json") + // .addHeader("Accept-Language", "en") + // .addHeader("Referer", BongaCams.BASE_URL) + // .addHeader("X-Requested-With", "XMLHttpRequest") + // .build(); + // try(Response response = execute(request)) { + // if(response.isSuccessful()) { + // JSONObject json = new JSONObject(response.body().string()); + // if(json.optString("status").equals("success")) { + // return true; + // } else { + // LOG.debug("Login response: {}", json.toString(2)); + // Platform.runLater(() -> new BongaCamsLoginDialog()); + // throw new IOException("Login not successful"); + // } + // } else { + // throw new IOException(response.code() + " " + response.message()); + // } + // } + // } + + public int getUserId() throws IOException { + if(userId == 0) { + login(); + } + return userId; + } +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsLoginDialog.java b/src/main/java/ctbrec/sites/bonga/BongaCamsLoginDialog.java new file mode 100644 index 00000000..0d609311 --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsLoginDialog.java @@ -0,0 +1,118 @@ +package ctbrec.sites.bonga; + +import java.io.File; +import java.io.InputStream; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Config; +import ctbrec.OS; +import javafx.concurrent.Worker.State; +import javafx.scene.Scene; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.image.Image; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.stage.Stage; + +public class BongaCamsLoginDialog { + + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsLoginDialog.class); + public static final String URL = BongaCams.BASE_URL + "/login"; + private List cookies = null; + private String url; + private Region veil; + private ProgressIndicator p; + + public BongaCamsLoginDialog() { + Stage stage = new Stage(); + stage.setTitle("BongaCams Login"); + InputStream icon = getClass().getResourceAsStream("/icon.png"); + stage.getIcons().add(new Image(icon)); + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + WebView webView = createWebView(stage); + + veil = new Region(); + veil.setStyle("-fx-background-color: rgba(0, 0, 0, 0.4)"); + p = new ProgressIndicator(); + p.setMaxSize(140, 140); + + StackPane stackPane = new StackPane(); + stackPane.getChildren().addAll(webView, veil, p); + + stage.setScene(new Scene(stackPane, 640, 480)); + stage.showAndWait(); + cookies = cookieManager.getCookieStore().getCookies(); + } + + private WebView createWebView(Stage stage) { + WebView browser = new WebView(); + WebEngine webEngine = browser.getEngine(); + webEngine.setJavaScriptEnabled(true); + webEngine.locationProperty().addListener((obs, oldV, newV) -> { + try { + URL _url = new URL(newV); + if (Objects.equals(_url.getPath(), "/")) { + stage.close(); + } + } catch (MalformedURLException e) { + LOG.error("Couldn't parse new url {}", newV, e); + } + url = newV.toString(); + }); + webEngine.getLoadWorker().stateProperty().addListener((observable, oldState, newState) -> { + if (newState == State.SUCCEEDED) { + veil.setVisible(false); + p.setVisible(false); + //System.out.println("############# " + webEngine.getLocation()); + //System.out.println(webEngine.getDocument().getDocumentElement().getTextContent()); + try { + String username = Config.getInstance().getSettings().bongaUsername; + if (username != null && !username.trim().isEmpty()) { + webEngine.executeScript("$('input[name=\"log_in[username]\"]').attr('value','" + username + "')"); + } + String password = Config.getInstance().getSettings().bongaPassword; + if (password != null && !password.trim().isEmpty()) { + webEngine.executeScript("$('input[name=\"log_in[password]\"]').attr('value','" + password + "')"); + } + webEngine.executeScript("$('div[class~=\"fancybox-overlay\"]').css('display','none')"); + webEngine.executeScript("$('div#header').css('display','none')"); + webEngine.executeScript("$('div.footer').css('display','none')"); + webEngine.executeScript("$('div.footer_copy').css('display','none')"); + webEngine.executeScript("$('div[class~=\"banner_top_index\"]').css('display','none')"); + webEngine.executeScript("$('td.menu_container').css('display','none')"); + } catch(Exception e) { + LOG.warn("Couldn't auto fill username and password for BongaCams", e); + } + } else if (newState == State.CANCELLED || newState == State.FAILED) { + veil.setVisible(false); + p.setVisible(false); + } + }); + webEngine.setUserDataDirectory(new File(OS.getConfigDir(), "webengine")); + webEngine.load(URL); + return browser; + } + + public List getCookies() { + // for (HttpCookie httpCookie : cookies) { + // LOG.debug("Cookie: {}", httpCookie); + // } + return cookies; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java b/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java new file mode 100644 index 00000000..2f29563b --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsModel.java @@ -0,0 +1,226 @@ +package ctbrec.sites.bonga; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iheartradio.m3u8.Encoding; +import com.iheartradio.m3u8.Format; +import com.iheartradio.m3u8.ParseException; +import com.iheartradio.m3u8.PlaylistException; +import com.iheartradio.m3u8.PlaylistParser; +import com.iheartradio.m3u8.data.MasterPlaylist; +import com.iheartradio.m3u8.data.Playlist; +import com.iheartradio.m3u8.data.PlaylistData; +import com.iheartradio.m3u8.data.StreamInfo; + +import ctbrec.AbstractModel; +import ctbrec.recorder.download.StreamSource; +import ctbrec.sites.Site; +import okhttp3.FormBody; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class BongaCamsModel extends AbstractModel { + + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsModel.class); + + private BongaCams site; + private int userId; + private String onlineState = "n/a"; + private boolean online = false; + private List streamSources = new ArrayList<>(); + private int[] resolution; + + @Override + public boolean isOnline(boolean ignoreCache) throws IOException, ExecutionException, InterruptedException { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } + + @Override + public String getOnlineState(boolean failFast) throws IOException, ExecutionException { + return onlineState; + } + + public void setOnlineState(String onlineState) { + this.onlineState = onlineState; + } + + @Override + public List getStreamSources() throws IOException, ExecutionException, ParseException, PlaylistException { + String streamUrl = getStreamUrl(); + if (streamUrl == null) { + return Collections.emptyList(); + } + Request req = new Request.Builder().url(streamUrl).build(); + Response response = site.getHttpClient().execute(req); + try { + InputStream inputStream = response.body().byteStream(); + PlaylistParser parser = new PlaylistParser(inputStream, Format.EXT_M3U, Encoding.UTF_8); + Playlist playlist = parser.parse(); + MasterPlaylist master = playlist.getMasterPlaylist(); + for (PlaylistData playlistData : master.getPlaylists()) { + + StreamSource streamsource = new StreamSource(); + streamsource.mediaPlaylistUrl = streamUrl.replace("playlist.m3u8", playlistData.getUri()); + if (playlistData.hasStreamInfo()) { + StreamInfo info = playlistData.getStreamInfo(); + streamsource.bandwidth = info.getBandwidth(); + streamsource.width = info.hasResolution() ? info.getResolution().width : 0; + streamsource.height = info.hasResolution() ? info.getResolution().height : 0; + } else { + streamsource.bandwidth = 0; + streamsource.width = 0; + streamsource.height = 0; + } + streamSources.add(streamsource); + } + } finally { + response.close(); + } + return streamSources; + } + + private String getStreamUrl() throws IOException { + String url = BongaCams.BASE_URL + "/tools/amf.php"; + RequestBody body = new FormBody.Builder() + .add("method", "getRoomData") + .add("args[]", getName()) + .add("args[]", "false") + .build(); + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = site.getHttpClient().execute(request)) { + if(response.isSuccessful()) { + JSONObject json = new JSONObject(response.body().string()); + if(json.optString("status").equals("success")) { + JSONObject localData = json.getJSONObject("localData"); + String server = localData.getString("videoServerUrl"); + return "https:" + server + "/hls/stream_" + getName() + "/playlist.m3u8"; + } else { + throw new IOException("Request was not successful: " + json.toString(2)); + } + } else { + throw new IOException(response.code() + " " + response.message()); + } + } + } + + @Override + public void invalidateCacheEntries() { + // TODO Auto-generated method stub + + } + + @Override + public void receiveTip(int tokens) throws IOException { + // method=tipModel&args[]=Sweetsexbia&args[]=1&args[]=66050808&args[3]=&_csrf_token=dd304a3876025127cc487e71d44a5843 + String url = BongaCams.BASE_URL + "/chat-ajax-amf-service?" + System.currentTimeMillis(); + int userId = ((BongaCamsHttpClient)site.getHttpClient()).getUserId(); + RequestBody body = new FormBody.Builder() + .add("method", "tipModel") + .add("args[]", getName()) + .add("args[]", Integer.toString(tokens)) + .add("args[]", Integer.toString(userId)) + .add("args[3]", "") + .build(); + Request request = new Request.Builder() + .url(url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", BongaCams.BASE_URL + '/' + getName()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .post(body) + .build(); + try(Response response = site.getHttpClient().execute(request, true)) { + if(response.isSuccessful()) { + // { + // "dataKey": "d40f579faf592324c1b0b97bd711039f", + // "amount": "1", + // "balance": 11, + // "actionKey": "b60f780c472e83b95167efe9bc9512bf", + // "description": "Sie haben erfolgreich Sweetsexbia 1 Token Trinkgeld gegeben!", + // "status": "success" + // } + JSONObject json = new JSONObject(response.body().string()); + System.out.println(json.toString(2)); + } else { + throw new IOException(response.code() + ' ' + response.message()); + } + } + } + + @Override + public int[] getStreamResolution(boolean failFast) throws ExecutionException { + if(resolution == null) { + if(failFast) { + return new int[2]; + } + try { + List streamSources = getStreamSources(); + Collections.sort(streamSources); + StreamSource best = streamSources.get(streamSources.size()-1); + resolution = new int[] {best.width, best.height}; + } catch (ExecutionException | IOException | ParseException | PlaylistException e) { + LOG.error("Couldn't determine stream resolution", e); + } + return resolution; + } else { + return resolution; + } + } + + @Override + public boolean follow() throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public boolean unfollow() throws IOException { + // TODO Auto-generated method stub + return false; + } + + @Override + public void setSite(Site site) { + if(site instanceof BongaCams) { + this.site = (BongaCams) site; + } else { + throw new IllegalArgumentException("Site has to be an instance of BongaCams"); + } + } + + @Override + public Site getSite() { + return site; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsTabProvider.java b/src/main/java/ctbrec/sites/bonga/BongaCamsTabProvider.java new file mode 100644 index 00000000..5a5bf55a --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsTabProvider.java @@ -0,0 +1,66 @@ +package ctbrec.sites.bonga; + +import java.util.ArrayList; +import java.util.List; + +import ctbrec.recorder.Recorder; +import ctbrec.ui.PaginatedScheduledService; +import ctbrec.ui.TabProvider; +import ctbrec.ui.ThumbOverviewTab; +import javafx.scene.Scene; +import javafx.scene.control.Tab; + +public class BongaCamsTabProvider extends TabProvider { + + private BongaCams bongaCams; + private Recorder recorder; + + public BongaCamsTabProvider(Recorder recorder, BongaCams bongaCams) { + this.recorder = recorder; + this.bongaCams = bongaCams; + } + + @Override + public List getTabs(Scene scene) { + List tabs = new ArrayList<>(); + + // female + String url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=female&online_only=true&is_mobile=true&offset="; + BongaCamsUpdateService updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("Female", updateService)); + + // male + url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=male&online_only=true&is_mobile=true&offset="; + updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("Male", updateService)); + + // couples + url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=couples&online_only=true&is_mobile=true&offset="; + updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("Couples", updateService)); + + // trans + url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=transsexual&online_only=true&is_mobile=true&offset="; + updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("Transsexual", updateService)); + + // new + url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=new-models&online_only=true&is_mobile=true&offset="; + updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("New", updateService)); + + // friends + url = BongaCams.BASE_URL + "/tools/listing_v3.php?livetab=friends&online_only=true&offset="; + updateService = new BongaCamsUpdateService(bongaCams, url); + tabs.add(createTab("Friends", updateService)); + + return tabs; + } + + private Tab createTab(String title, PaginatedScheduledService updateService) { + ThumbOverviewTab tab = new ThumbOverviewTab(title, updateService, bongaCams); + tab.setRecorder(recorder); + return tab; + } + +} diff --git a/src/main/java/ctbrec/sites/bonga/BongaCamsUpdateService.java b/src/main/java/ctbrec/sites/bonga/BongaCamsUpdateService.java new file mode 100644 index 00000000..e1f19274 --- /dev/null +++ b/src/main/java/ctbrec/sites/bonga/BongaCamsUpdateService.java @@ -0,0 +1,72 @@ +package ctbrec.sites.bonga; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ctbrec.Model; +import ctbrec.ui.PaginatedScheduledService; +import javafx.concurrent.Task; +import okhttp3.Request; +import okhttp3.Response; + +public class BongaCamsUpdateService extends PaginatedScheduledService { + + private static final transient Logger LOG = LoggerFactory.getLogger(BongaCamsUpdateService.class); + + private BongaCams bongaCams; + private String url; + + public BongaCamsUpdateService(BongaCams bongaCams, String url) { + this.bongaCams = bongaCams; + this.url = url; + } + + @Override + protected Task> createTask() { + return new Task>() { + @Override + public List call() throws IOException { + String _url = url + ((page-1) * 50); + LOG.debug("Fetching page {}", _url); + Request request = new Request.Builder() + .url(_url) + .addHeader("User-Agent", "Mozilla/5.0 (Android 9.0; Mobile; rv:61.0) Gecko/61.0 Firefox/61.0") + .addHeader("Accept", "application/json, text/javascript, */*") + .addHeader("Accept-Language", "en") + .addHeader("Referer", bongaCams.getBaseUrl()) + .addHeader("X-Requested-With", "XMLHttpRequest") + .build(); + Response response = bongaCams.getHttpClient().execute(request); + if (response.isSuccessful()) { + String content = response.body().string(); + List models = new ArrayList<>(); + JSONObject json = new JSONObject(content); + if(json.optString("status").equals("success")) { + JSONArray _models = json.getJSONArray("models"); + for (int i = 0; i < _models.length(); i++) { + JSONObject m = _models.getJSONObject(i); + String name = m.getString("username"); + BongaCamsModel model = (BongaCamsModel) bongaCams.createModel(name); + model.setUserId(m.getInt("user_id")); + model.setOnlineState(m.getString("room")); + model.setOnline(m.optBoolean("online") && !m.optBoolean("is_away")); + model.setPreview("https:" + m.getString("thumb_image")); + models.add(model); + } + } + return models; + } else { + int code = response.code(); + response.close(); + throw new IOException("HTTP status " + code); + } + } + }; + } +} diff --git a/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java b/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java index 3a40c555..f6434401 100644 --- a/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java +++ b/src/main/java/ctbrec/sites/cam4/Cam4HttpClient.java @@ -23,6 +23,10 @@ public class Cam4HttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(Cam4HttpClient.class); + public Cam4HttpClient() { + super("cam4"); + } + @Override public synchronized boolean login() throws IOException { if(loggedIn) { diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java index 9ab57b9e..b6148863 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaHttpClient.java @@ -29,6 +29,10 @@ public class CamsodaHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(CamsodaHttpClient.class); private String csrfToken = null; + public CamsodaHttpClient() { + super("camsoda"); + } + @Override public boolean login() throws IOException { if(loggedIn) { diff --git a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java index 44070fcc..3557a55a 100644 --- a/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java +++ b/src/main/java/ctbrec/sites/camsoda/CamsodaShowsTab.java @@ -195,8 +195,8 @@ public class CamsodaShowsTab extends Tab implements TabSelectionListener { root.setCenter(grid); loadImage(model, thumb); - record.prefWidthProperty().bind(openInBrowser.widthProperty()); - follow.prefWidthProperty().bind(openInBrowser.widthProperty()); + record.minWidthProperty().bind(openInBrowser.widthProperty()); + follow.minWidthProperty().bind(openInBrowser.widthProperty()); } private void follow(Model model) { diff --git a/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java b/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java index 33cdbcd8..06ce6262 100644 --- a/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java +++ b/src/main/java/ctbrec/sites/chaturbate/ChaturbateHttpClient.java @@ -20,6 +20,10 @@ public class ChaturbateHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(ChaturbateHttpClient.class); protected String token; + public ChaturbateHttpClient() { + super("chaturbate"); + } + private void extractCsrfToken(Request request) { try { Cookie csrfToken = cookieJar.getCookie(request.url(), "csrftoken"); diff --git a/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java b/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java index 762d3dd6..7d49237b 100644 --- a/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java +++ b/src/main/java/ctbrec/sites/mfc/MyFreeCamsHttpClient.java @@ -24,6 +24,10 @@ public class MyFreeCamsHttpClient extends HttpClient { private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsHttpClient.class); + public MyFreeCamsHttpClient() { + super("myfreecams"); + } + @Override public boolean login() throws IOException { if(loggedIn) { diff --git a/src/main/java/ctbrec/ui/CamrecApplication.java b/src/main/java/ctbrec/ui/CamrecApplication.java index 90be7e79..5856873e 100644 --- a/src/main/java/ctbrec/ui/CamrecApplication.java +++ b/src/main/java/ctbrec/ui/CamrecApplication.java @@ -27,6 +27,7 @@ import ctbrec.recorder.LocalRecorder; import ctbrec.recorder.Recorder; import ctbrec.recorder.RemoteRecorder; import ctbrec.sites.Site; +import ctbrec.sites.bonga.BongaCams; import ctbrec.sites.cam4.Cam4; import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.chaturbate.Chaturbate; @@ -60,10 +61,11 @@ public class CamrecApplication extends Application { @Override public void start(Stage primaryStage) throws Exception { + sites.add(new BongaCams()); + sites.add(new Cam4()); + sites.add(new Camsoda()); sites.add(new Chaturbate()); sites.add(new MyFreeCams()); - sites.add(new Camsoda()); - sites.add(new Cam4()); loadConfig(); createHttpClient(); bus = new AsyncEventBus(Executors.newSingleThreadExecutor()); @@ -198,7 +200,7 @@ public class CamrecApplication extends Application { } private void createHttpClient() { - httpClient = new HttpClient() { + httpClient = new HttpClient("camrec") { @Override public boolean login() throws IOException { return false; diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index f579a948..133d37a2 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -208,7 +208,7 @@ public class ThumbCell extends StackPane { LOG.trace("Removing invalid resolution value for {}", model.getName()); model.invalidateCacheEntries(); } - + Thread.sleep(500); } catch (IOException | InterruptedException e1) { LOG.warn("Couldn't update resolution tag for model {}", model.getName(), e1);