Implement follow / unfollow

This commit is contained in:
0xb00bface 2021-11-07 16:10:03 +01:00
parent 0d47952d3d
commit 18e4a43699
5 changed files with 158 additions and 112 deletions

View File

@ -102,7 +102,6 @@ public class CherryTvUpdateService extends PaginatedScheduledService {
for (int i = 0; i < broadcasts.length(); i++) {
JSONObject broadcast = broadcasts.getJSONObject(i);
CherryTvModel model = site.createModel(broadcast.optString("username"));
model.setId(broadcast.getString("id"));
model.setDisplayName(broadcast.optString("title"));
model.setDescription(broadcast.optString("description"));
model.setPreview(broadcast.optString("thumbnailUrl"));

View File

@ -1,8 +1,16 @@
package ctbrec.io;
import static ctbrec.io.HttpConstants.*;
import static java.nio.charset.StandardCharsets.*;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import ctbrec.Config;
import ctbrec.LoggingInterceptor;
import ctbrec.Settings.ProxyType;
import okhttp3.*;
import okhttp3.OkHttpClient.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
@ -14,44 +22,16 @@ import java.nio.file.Files;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
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;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import static ctbrec.io.HttpConstants.ACCEPT_ENCODING_GZIP;
import static ctbrec.io.HttpConstants.CONTENT_ENCODING;
import static java.nio.charset.StandardCharsets.UTF_8;
public abstract class HttpClient {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
@ -59,11 +39,11 @@ public abstract class HttpClient {
private static final ConnectionPool GLOBAL_HTTP_CONN_POOL = new ConnectionPool(10, 2, TimeUnit.MINUTES);
protected OkHttpClient client;
protected CookieJarImpl cookieJar = new CookieJarImpl();
protected CookieJarImpl cookieJar;
protected Config config;
protected boolean loggedIn = false;
protected int loginTries = 0;
private String name;
private final String name;
protected HttpClient(String name, Config config) {
this.name = name;
@ -145,8 +125,7 @@ public abstract class HttpClient {
.connectionPool(GLOBAL_HTTP_CONN_POOL)
.connectTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS)
.readTimeout(config.getSettings().httpTimeout, TimeUnit.MILLISECONDS)
//.addNetworkInterceptor(new LoggingInterceptor())
;
.addNetworkInterceptor(new LoggingInterceptor());
ProxyType proxyType = config.getSettings().proxyType;
if (proxyType == ProxyType.HTTP) {
@ -157,7 +136,7 @@ public abstract class HttpClient {
}
}
// if transport layer security (TLS) is switched on, accept the self signed cert from the server
// if transport layer security (TLS) is switched on, accept the self-signed cert from the server
if (config.getSettings().transportLayerSecurity) {
acceptAllTlsCerts(builder);
}
@ -177,8 +156,8 @@ public abstract class HttpClient {
X509Certificate[] x509Certificates = new X509Certificate[0];
return x509Certificates;
}
@Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
@Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException {}
@Override public void checkServerTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ }
@Override public void checkClientTrusted(final X509Certificate[] chain, final String authType) { /* noop*/ }
};
try {
@ -190,7 +169,7 @@ public abstract class HttpClient {
sslContext.init(keyManagers, trustManagers, secureRandom);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
builder.sslSocketFactory(sslSocketFactory, x509TrustManager);
builder.hostnameVerifier((name, sslSession) -> true);
builder.hostnameVerifier((hostname, sslSession) -> true);
} catch (KeyManagementException | NoSuchAlgorithmException e) {
LOG.error("Couldn't install trust managers for TLS connections");
}
@ -254,17 +233,14 @@ public abstract class HttpClient {
}
private okhttp3.Authenticator createHttpProxyAuthenticator(String username, String password) {
return new okhttp3.Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(username, password);
return response.request().newBuilder().header("Proxy-Authorization", credential).build();
}
return (route, response) -> {
String credential = Credentials.basic(username, password);
return response.request().newBuilder().header("Proxy-Authorization", credential).build();
};
}
public static class SocksProxyAuth extends Authenticator {
private PasswordAuthentication auth;
private final PasswordAuthentication auth;
private SocksProxyAuth(String user, String password) {
auth = new PasswordAuthentication(user, password == null ? new char[]{} : password.toCharArray());
@ -327,16 +303,16 @@ public abstract class HttpClient {
public static String gunzipBody(Response response) throws IOException {
if (Objects.equals(response.header(CONTENT_ENCODING), ACCEPT_ENCODING_GZIP)) {
GZIPInputStream gzipIn = new GZIPInputStream(response.body().byteStream());
GZIPInputStream gzipIn = new GZIPInputStream(Objects.requireNonNull(response.body()).byteStream());
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int len = -1;
int len;
while ((len = gzipIn.read(b)) >= 0) {
bos.write(b, 0, len);
}
return bos.toString(StandardCharsets.UTF_8.toString());
} else {
return response.body().string();
return Objects.requireNonNull(response.body()).string();
}
}

View File

@ -69,7 +69,7 @@ public class CherryTv extends AbstractSite {
@Override
public synchronized boolean login() throws IOException {
return false;
return getHttpClient().login();
}
@Override
@ -99,7 +99,7 @@ public class CherryTv extends AbstractSite {
@Override
public boolean supportsFollow() {
return false;
return true;
}
@Override
@ -129,7 +129,7 @@ public class CherryTv extends AbstractSite {
try (Response response = getHttpClient().execute(req)) {
if (response.isSuccessful()) {
JSONObject json = new JSONObject(Objects.requireNonNull(response.body()).string());
LOG.debug(json.toString(2));
LOG.trace(json.toString(2));
JSONObject data = json.getJSONObject("data");
JSONObject searchResult = data.getJSONObject("searchResult");
JSONArray streamers = searchResult.getJSONArray("streamers");
@ -159,7 +159,8 @@ public class CherryTv extends AbstractSite {
@Override
public boolean credentialsAvailable() {
return false;
String username = getConfig().getSettings().cherryTvUsername;
return username != null && !username.trim().isEmpty();
}
@Override

View File

@ -64,14 +64,18 @@ public class CherryTvHttpClient extends HttpClient {
try (Response response = execute(request)) {
if (response.isSuccessful()) {
JSONObject resp = new JSONObject(Objects.requireNonNull(response.body()).string());
LOG.trace(resp.toString(2));
JSONObject data = resp.getJSONObject("data");
JSONObject login = data.getJSONObject("login");
loggedIn = login.optBoolean("success");
String jwt = login.optString("token");
saveAsSessionCookie(jwt);
LOG.debug("Login successful");
return loggedIn;
if (resp.has("data")) {
JSONObject data = resp.getJSONObject("data");
JSONObject login = data.getJSONObject("login");
loggedIn = login.optBoolean("success");
String jwt = login.optString("token");
saveAsSessionCookie(jwt);
LOG.debug("Login successful");
return loggedIn;
} else {
LOG.error(resp.toString(2));
return false;
}
} else {
throw new HttpException(response.code(), response.message());
}

View File

@ -9,9 +9,12 @@ import com.squareup.moshi.JsonWriter;
import ctbrec.AbstractModel;
import ctbrec.Config;
import ctbrec.NotImplementedExcetion;
import ctbrec.StringUtil;
import ctbrec.io.HttpException;
import ctbrec.recorder.download.StreamSource;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.json.JSONException;
import org.json.JSONObject;
@ -21,9 +24,6 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
@ -55,26 +55,10 @@ public class CherryTvModel extends AbstractModel {
.build();
try (Response resp = site.getHttpClient().execute(req)) {
String body = Objects.requireNonNull(resp.body()).string();
Files.write(Paths.get("/tmp/mdl.html"), body.getBytes(StandardCharsets.UTF_8));
Matcher m = NEXT_DATA.matcher(body);
if (m.find()) {
JSONObject json = new JSONObject(m.group(1));
JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState");
online = false;
onlineState = OFFLINE;
for (Iterator<String> iter = apolloState.keys(); iter.hasNext();) {
String key = iter.next();
if (key.startsWith("Broadcast:")) {
JSONObject broadcast = apolloState.getJSONObject(key);
setDisplayName(broadcast.optString("title"));
id = broadcast.getString("id");
online = broadcast.optString("showStatus").equalsIgnoreCase("Public")
&& broadcast.optString("broadcastStatus").equalsIgnoreCase("Live");
onlineState = online ? ONLINE : OFFLINE;
masterPlaylistUrl = broadcast.optString("pullUrl", null);
break;
}
}
updateModelProperties(json);
} else {
LOG.error("NEXT_DATA not found in model page {}", getUrl());
return false;
@ -86,6 +70,27 @@ public class CherryTvModel extends AbstractModel {
return online;
}
private void updateModelProperties(JSONObject json) {
LOG.trace(json.toString(2));
JSONObject apolloState = json.getJSONObject("props").getJSONObject("pageProps").getJSONObject("apolloState");
online = false;
onlineState = OFFLINE;
for (Iterator<String> iter = apolloState.keys(); iter.hasNext(); ) {
String key = iter.next();
if (key.startsWith("Broadcast:")) {
JSONObject broadcast = apolloState.getJSONObject(key);
setDisplayName(broadcast.optString("title"));
online = broadcast.optString("showStatus").equalsIgnoreCase("Public")
&& broadcast.optString("broadcastStatus").equalsIgnoreCase("Live");
onlineState = online ? ONLINE : OFFLINE;
masterPlaylistUrl = broadcast.optString("pullUrl", null);
} else if (key.startsWith("Streamer:")) {
JSONObject streamer = apolloState.getJSONObject(key);
id = streamer.getString("id");
}
}
}
public void setOnline(boolean online) {
this.online = online;
}
@ -125,7 +130,7 @@ public class CherryTvModel extends AbstractModel {
String baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
String segmentUri = baseUrl + playlist.getUri();
src.mediaPlaylistUrl = segmentUri;
if(src.mediaPlaylistUrl.contains("?")) {
if (src.mediaPlaylistUrl.contains("?")) {
src.mediaPlaylistUrl = src.mediaPlaylistUrl.substring(0, src.mediaPlaylistUrl.lastIndexOf('?'));
}
LOG.trace("Media playlist {}", src.mediaPlaylistUrl);
@ -172,18 +177,18 @@ public class CherryTvModel extends AbstractModel {
@Override
public int[] getStreamResolution(boolean failFast) throws ExecutionException {
if(resolution == null) {
if(failFast) {
if (resolution == null) {
if (failFast) {
return new int[2];
}
try {
if(!isOnline()) {
if (!isOnline()) {
return new int[2];
}
List<StreamSource> sources = getStreamSources();
Collections.sort(sources);
StreamSource best = sources.get(sources.size()-1);
resolution = new int[] {best.width, best.height};
StreamSource best = sources.get(sources.size() - 1);
resolution = new int[]{best.width, best.height};
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
@ -192,38 +197,97 @@ public class CherryTvModel extends AbstractModel {
LOG.warn("Couldn't determine stream resolution for {} - {}", getName(), e.getMessage());
resolution = new int[2];
}
return resolution;
} else {
return resolution;
}
return resolution;
}
@Override
public boolean follow() throws IOException {
// POST https://cherry.tv/graphql
// {"operationName":"follow","variables":{"userId":"1391"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f"}}}
return false;
return followUnfollow("follow", "a7a8241014074f5c02ac83863a47f7579e5f03167a7ec424ff65ad045c7fcf6f");
}
@Override
public boolean unfollow() throws IOException {
return false;
return followUnfollow("unfollow", "e91f8f5a60d33efb2dfb3348b977b78358862d3a5cd5ef0011a6aa6bb65d0bd4");
}
private boolean followUnfollow(String action, String persistedQueryHash) throws IOException {
Request request = createFollowUnfollowRequest(action, persistedQueryHash);
LOG.debug("Sending follow request for model {} with ID {}", getName(), getId());
try (Response response = getSite().getHttpClient().execute(request)) {
if (response.isSuccessful()) {
String responseBody = Objects.requireNonNull(response.body(), "HTTP response body is null").string();
LOG.debug(responseBody);
JSONObject resp = new JSONObject(responseBody);
if (resp.has("data") && !resp.isNull("data")) {
JSONObject data = resp.getJSONObject("data");
if (data.has(action + "User")) {
return data.getJSONObject(action + "User").optBoolean("success");
}
} else if (resp.has("errors")) {
JSONObject first = resp.getJSONArray("errors").getJSONObject(0);
if (first.optString("message").matches("You have .*? the user")) {
return true;
}
}
LOG.debug(resp.toString(2));
return false;
} else {
throw new HttpException(response.code(), response.message());
}
}
}
private Request createFollowUnfollowRequest(String action, String persistedQueryHash) throws IOException {
if (StringUtil.isBlank(id)) {
try {
// if the id is not set yet, we call isOnline(true), where it gets set
isOnline(true);
} catch (ExecutionException e) {
throw new IOException(e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException(e);
}
}
JSONObject body = new JSONObject()
.put("operationName", action)
.put("variables", new JSONObject()
.put("userId", Objects.requireNonNull(id, "Model ID is null"))
)
.put("query", "mutation " + action + "($userId: ID!) {\n " + action + "User(userId: $userId) {\n success\n __typename\n }\n}\n")
.put("extensions", new JSONObject()
.put("persistedQuery", new JSONObject()
.put("version", 1)
.put("sha256Hash", persistedQueryHash)
)
);
RequestBody requestBody = RequestBody.create(body.toString(), MediaType.parse("application/json"));
return new Request.Builder()
.url(CherryTv.BASE_URL + "/graphql")
.header(REFERER, CherryTv.BASE_URL)
.header(ORIGIN, CherryTv.BASE_URL)
.header(USER_AGENT, Config.getInstance().getSettings().httpUserAgent)
.post(requestBody)
.build();
}
public void mapOnlineState(String roomState) {
switch (roomState) {
case "private":
case "fullprivate":
setOnlineState(PRIVATE);
break;
case "group":
case "public":
setOnlineState(ONLINE);
setOnline(true);
break;
default:
LOG.debug(roomState);
setOnlineState(OFFLINE);
case "private":
case "fullprivate":
setOnlineState(PRIVATE);
break;
case "group":
case "public":
setOnlineState(ONLINE);
setOnline(true);
break;
default:
LOG.debug(roomState);
setOnlineState(OFFLINE);
}
}
@ -237,8 +301,10 @@ public class CherryTvModel extends AbstractModel {
@Override
public void readSiteSpecificData(JsonReader reader) throws IOException {
reader.nextName();
id = reader.nextString();
if (reader.hasNext()) {
reader.nextName();
id = reader.nextString();
}
}
@Override