Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Android TV login #379

Merged
merged 6 commits into from
Dec 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions miner/docs/modules/ROOT/examples/global-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,8 @@
},
"description" : "Return a score if a drop campaign may be progressed by watching this stream.",
"required" : ["type"]
} ]
}
]
}
}
},
Expand Down Expand Up @@ -582,13 +583,37 @@
"const" : "mobile"
}
},
"description" : "Login though Twitch's Passport API (as mobile).",
"description" : "Deprecated. Login though Twitch's Passport API (as mobile).",
"required" : ["type"]
}, {
"description" : "Login method to use."
}
]
} ]
}, {
"allOf" : [
{
"type" : "object",
"properties" : {
"authenticationFolder" : {
"$ref" : "#/$defs/Path",
"description" : "Path to a folder that contains authentication files used to log back in after a restart. Default: ./authentication"
},
"password" : {
"type" : "string",
"description" : "Password of your Twitch account."
},
"type" : {
"const" : "tv"
}
},
"description" : "Login though Twitch's Oauth API (as android tv).",
"required" : ["type"]
}, {
"description" : "Login method to use."
}
]
}
]
},
"reloadEvery" : {
"type" : "integer",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,8 @@
},
"description" : "Return a score if a drop campaign may be progressed by watching this stream.",
"required" : ["type"]
} ]
}
]
}
}
},
Expand Down
4 changes: 2 additions & 2 deletions miner/docs/modules/ROOT/pages/configuration/global.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ Examples of this file can be found in link:https://github.com/Rakambda/ChannelPo
[WARNING]
====
link:https://twitter.com/TwitchSupport/status/1575571090994102272[Twitch changed their authentication method in September 2022], now requiring calls to an integrity endpoint.
Old methods using `HTTP` may not work anymore.
Old methods using `HTTP` or `MOBILE` may not work anymore.

Options working :

* `BROWSER`: It is in my opinion the safest but is requiring a selenium node/grid to be available (beware of `SE_NODE_GRID_URL` as CDP connection is required).
* `MOBILE`: Simulates using the Android app, easy to set up.
* `TV`: Simulates using the Android TV app, easy to set up.
====

== Streamer directories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,17 @@ private <T> Optional<GQLResponse<T>> postGqlRequest(@NotNull IGQLOperation<T> op
log.debug("Sending GQL operation {}", operation);
var integrity = integrityProvider.getIntegrity();

var response = unirest.post(ENDPOINT)
var request = unirest.post(ENDPOINT)
.header(AUTHORIZATION, "OAuth " + twitchLogin.getAccessToken())
.header(CLIENT_INTEGRITY_HEADER, integrity.getToken())
.header(CLIENT_ID_HEADER, twitchLogin.getTwitchClient().getClientId())
.header(CLIENT_SESSION_ID_HEADER, integrity.getClientSessionId())
.header(CLIENT_VERSION_HEADER, integrity.getClientVersion())
.header(X_DEVICE_ID_HEADER, integrity.getXDeviceId())
.header(CLIENT_ID_HEADER, twitchLogin.getTwitchClient().getClientId());

integrity.ifPresent(i -> request
.header(CLIENT_INTEGRITY_HEADER, i.getToken())
.header(CLIENT_SESSION_ID_HEADER, i.getClientSessionId())
.header(CLIENT_VERSION_HEADER, i.getClientVersion())
.header(X_DEVICE_ID_HEADER, i.getXDeviceId()));

var response = request
.body(operation)
.asObject(operation.getResponseType());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package fr.rakambda.channelpointsminer.miner.api.gql.integrity;

import org.jetbrains.annotations.NotNull;
import java.util.Optional;

public interface IIntegrityProvider{
void invalidate();

@NotNull
IntegrityData getIntegrity() throws IntegrityException;
Optional<IntegrityData> getIntegrity() throws IntegrityException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ public void invalidate(){

@Override
@NotNull
public IntegrityData getIntegrity() throws IntegrityException{
public Optional<IntegrityData> getIntegrity() throws IntegrityException{
synchronized(this){
if(Objects.nonNull(currentIntegrity) && currentIntegrity.getExpiration().minus(Duration.ofMinutes(5)).isAfter(TimeFactory.now())){
return currentIntegrity;
return Optional.of(currentIntegrity);
}

log.info("Querying new integrity token");
Expand All @@ -51,7 +51,7 @@ public IntegrityData getIntegrity() throws IntegrityException{
CommonUtils.randomSleep(10000, 1);
currentIntegrity = extractGQLIntegrity(browser);
log.debug("Got new integrity token {}", currentIntegrity);
return currentIntegrity;
return Optional.of(currentIntegrity);
}
catch(LoginException e){
throw new IntegrityException("Failed to get integrity", e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityResponse;
import fr.rakambda.channelpointsminer.miner.api.gql.version.IVersionProvider;
import fr.rakambda.channelpointsminer.miner.api.gql.version.VersionException;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchClient;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchLogin;
import fr.rakambda.channelpointsminer.miner.factory.TimeFactory;
import kong.unirest.core.UnirestInstance;
Expand All @@ -14,6 +15,7 @@
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import static kong.unirest.core.HeaderNames.AUTHORIZATION;

@RequiredArgsConstructor
Expand All @@ -25,7 +27,7 @@ public class HttpIntegrityProvider implements IIntegrityProvider{
private static final String CLIENT_VERSION_HEADER = "Client-Version";
private static final String X_DEVICE_ID_HEADER = "X-Device-ID";

private static final String CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko";
private static final String CLIENT_ID = TwitchClient.WEB.getClientId();
private static final String DEFAULT_CLIENT_VERSION = "ef928475-9403-42f2-8a34-55784bd08e16";

private final TwitchLogin twitchLogin;
Expand All @@ -39,10 +41,10 @@ public class HttpIntegrityProvider implements IIntegrityProvider{

@Override
@NotNull
public IntegrityData getIntegrity() throws IntegrityException{
public Optional<IntegrityData> getIntegrity() throws IntegrityException{
synchronized(this){
if(Objects.nonNull(currentIntegrity) && currentIntegrity.getExpiration().minus(Duration.ofMinutes(5)).isAfter(TimeFactory.now())){
return currentIntegrity;
return Optional.of(currentIntegrity);
}

var clientVersion = getClientVersion();
Expand Down Expand Up @@ -73,7 +75,7 @@ public IntegrityData getIntegrity() throws IntegrityException{
.clientVersion(clientVersion)
.xDeviceId(xDeviceId)
.build();
return currentIntegrity;
return Optional.of(currentIntegrity);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityData;
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityException;
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityResponse;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchClient;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchLogin;
import fr.rakambda.channelpointsminer.miner.factory.TimeFactory;
import kong.unirest.core.UnirestInstance;
Expand All @@ -12,6 +13,7 @@
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.util.Objects;
import java.util.Optional;
import static kong.unirest.core.HeaderNames.AUTHORIZATION;

@RequiredArgsConstructor
Expand All @@ -23,7 +25,7 @@ public class MobileIntegrityProvider implements IIntegrityProvider{
private static final String CLIENT_VERSION_HEADER = "Client-Version";
private static final String X_DEVICE_ID_HEADER = "X-Device-ID";

private static final String CLIENT_ID = "kd1unb4b3q4t58fwlpcbzcbnm76a8fp";
private static final String CLIENT_ID = TwitchClient.MOBILE.getClientId();
private static final String CLIENT_VERSION = "32d439b2-bd5b-4e35-b82a-fae10b04da70";

private final TwitchLogin twitchLogin;
Expand All @@ -36,10 +38,10 @@ public class MobileIntegrityProvider implements IIntegrityProvider{

@Override
@NotNull
public IntegrityData getIntegrity() throws IntegrityException{
public Optional<IntegrityData> getIntegrity() throws IntegrityException{
synchronized(this){
if(Objects.nonNull(currentIntegrity) && currentIntegrity.getExpiration().minus(Duration.ofMinutes(5)).isAfter(TimeFactory.now())){
return currentIntegrity;
return Optional.of(currentIntegrity);
}

log.info("Querying new integrity token");
Expand Down Expand Up @@ -68,7 +70,7 @@ public IntegrityData getIntegrity() throws IntegrityException{
.clientVersion(CLIENT_VERSION)
.xDeviceId(xDeviceId)
.build();
return currentIntegrity;
return Optional.of(currentIntegrity);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package fr.rakambda.channelpointsminer.miner.api.gql.integrity.http;

import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IIntegrityProvider;
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityData;
import fr.rakambda.channelpointsminer.miner.api.gql.integrity.IntegrityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;

@RequiredArgsConstructor
@Log4j2
public class NoIntegrityProvider implements IIntegrityProvider{
@Override
@NotNull
public Optional<IntegrityData> getIntegrity() throws IntegrityException{
return Optional.empty();
}

@Override
public void invalidate(){
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.jetbrains.annotations.NotNull;
import java.io.IOException;

public interface IPassportApi{
public interface ILoginProvider{
/**
* Attempts a login towards Twitch. If a previous authentication file exists, it'll be restored. Else a login will be performed.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
@RequiredArgsConstructor
public enum TwitchClient{
MOBILE("kd1unb4b3q4t58fwlpcbzcbnm76a8fp"),
WEB("kimne78kx3ncx6brgo4mv6wki5h1ko");
WEB("kimne78kx3ncx6brgo4mv6wki5h1ko"),
ANDROID_TV("ue6666qo983tsx6so1t0vnawi233wa");

private final String clientId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package fr.rakambda.channelpointsminer.miner.api.passport;

import com.fasterxml.jackson.core.type.TypeReference;
import fr.rakambda.channelpointsminer.miner.util.json.JacksonUtils;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;

@RequiredArgsConstructor
public class TwitchLoginCacher{
private final Path userAuthenticationFile;

/**
* Restore authentication from a file.
*
* @return {@link TwitchLogin} if authentication was restored, empty otherwise.
*
* @throws IOException Failed to read authentication file.
*/
@NotNull
public Optional<TwitchLogin> restoreAuthentication() throws IOException{
if(!Files.exists(userAuthenticationFile)){
return Optional.empty();
}

var twitchLogin = JacksonUtils.read(Files.newInputStream(userAuthenticationFile), new TypeReference<TwitchLogin>(){});
return Optional.of(twitchLogin);
}

/**
* Save authentication received from response into a file.
*
* @param twitchLogin Authentication to save.
*
* @throws IOException File failed to write.
*/
public void saveAuthentication(@NotNull TwitchLogin twitchLogin) throws IOException{
Files.createDirectories(userAuthenticationFile.getParent());
JacksonUtils.write(Files.newOutputStream(userAuthenticationFile, CREATE, TRUNCATE_EXISTING), twitchLogin);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fr.rakambda.channelpointsminer.miner.api.passport.browser;

import fr.rakambda.channelpointsminer.miner.api.passport.IPassportApi;
import fr.rakambda.channelpointsminer.miner.api.passport.ILoginProvider;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchClient;
import fr.rakambda.channelpointsminer.miner.api.passport.TwitchLogin;
import fr.rakambda.channelpointsminer.miner.api.passport.exceptions.LoginException;
Expand All @@ -17,7 +17,7 @@

@Log4j2
@RequiredArgsConstructor
public class BrowserPassportApi implements IPassportApi{
public class BrowserLoginProvider implements ILoginProvider{
private final BrowserConfiguration browserConfiguration;

@Override
Expand Down
Loading