chore: Separate extensions by app (#3905)

This commit is contained in:
oSumAtrIX
2024-12-05 12:12:48 +01:00
committed by GitHub
parent 69ec47cbef
commit cc40246e60
314 changed files with 371 additions and 148 deletions

View File

@ -0,0 +1,8 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:twitch:stub"))
compileOnly(libs.okhttp)
compileOnly(libs.retrofit)
compileOnly(libs.annotation)
compileOnly(libs.appcompat)
}

View File

@ -0,0 +1 @@
<manifest/>

View File

@ -0,0 +1,14 @@
package app.revanced.extension.twitch;
public class Utils {
/* Called from SettingsPatch smali */
public static int getStringId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string");
}
/* Called from SettingsPatch smali */
public static int getDrawableId(String name) {
return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable");
}
}

View File

@ -0,0 +1,27 @@
package app.revanced.extension.twitch.adblock;
import okhttp3.Request;
public interface IAdblockService {
static boolean isVod(Request request) {
return request.url().pathSegments().contains("vod");
}
static String channelName(Request request) {
for (String pathSegment : request.url().pathSegments()) {
if (pathSegment.endsWith(".m3u8")) {
return pathSegment.replace(".m3u8", "");
}
}
return null;
}
String friendlyName();
Integer maxAttempts();
Boolean isAvailable();
Request rewriteHlsRequest(Request originalRequest);
}

View File

@ -0,0 +1,47 @@
package app.revanced.extension.twitch.adblock;
import app.revanced.extension.shared.Logger;
import okhttp3.HttpUrl;
import okhttp3.Request;
import static app.revanced.extension.shared.StringRef.str;
public class LuminousService implements IAdblockService {
@Override
public String friendlyName() {
return str("revanced_proxy_luminous");
}
@Override
public Integer maxAttempts() {
return 2;
}
@Override
public Boolean isAvailable() {
return true;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist";
var url = HttpUrl.parse("https://eu.luminous.dev/" +
type +
"/" +
IAdblockService.channelName(originalRequest) +
".m3u8" +
"%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue"
);
if (url == null) {
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.build();
}
}

View File

@ -0,0 +1,96 @@
package app.revanced.extension.twitch.adblock;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.twitch.api.RetrofitClient;
import okhttp3.HttpUrl;
import okhttp3.Request;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static app.revanced.extension.shared.StringRef.str;
public class PurpleAdblockService implements IAdblockService {
private final Map<String, Boolean> tunnels = new HashMap<>() {{
put("https://eu1.jupter.ga", false);
put("https://eu2.jupter.ga", false);
}};
@Override
public String friendlyName() {
return str("revanced_proxy_purpleadblock");
}
@Override
public Integer maxAttempts() {
return 3;
}
@Override
public Boolean isAvailable() {
for (String tunnel : tunnels.keySet()) {
var success = true;
try {
var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute();
if (!response.isSuccessful()) {
Logger.printException(() ->
"PurpleAdBlock tunnel $tunnel returned an error: HTTP code " + response.code()
);
Logger.printDebug(response::message);
try (var errorBody = response.errorBody()) {
if (errorBody != null) {
Logger.printDebug(() -> {
try {
return errorBody.string();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
success = false;
}
} catch (Exception ex) {
Logger.printException(() -> "PurpleAdBlock tunnel $tunnel is unavailable", ex);
success = false;
}
// Cache availability data
tunnels.put(tunnel, success);
if (success)
return true;
}
return false;
}
@Override
public Request rewriteHlsRequest(Request originalRequest) {
for (Map.Entry<String, Boolean> entry : tunnels.entrySet()) {
if (!entry.getValue()) continue;
var server = entry.getKey();
// Compose new URL
var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest));
if (url == null) {
Logger.printException(() -> "Failed to parse rewritten URL");
return null;
}
// Overwrite old request
return new Request.Builder()
.get()
.url(url)
.build();
}
Logger.printException(() -> "No tunnels are available");
return null;
}
}

View File

@ -0,0 +1,12 @@
package app.revanced.extension.twitch.api;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Url;
/* only used for service pings */
public interface PurpleAdblockApi {
@GET /* root */
Call<ResponseBody> ping(@Url String baseUrl);
}

View File

@ -0,0 +1,120 @@
package app.revanced.extension.twitch.api;
import androidx.annotation.NonNull;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.twitch.adblock.IAdblockService;
import app.revanced.extension.twitch.adblock.LuminousService;
import app.revanced.extension.twitch.adblock.PurpleAdblockService;
import app.revanced.extension.twitch.settings.Settings;
import okhttp3.Interceptor;
import okhttp3.Response;
import java.io.IOException;
import static app.revanced.extension.shared.StringRef.str;
public class RequestInterceptor implements Interceptor {
private IAdblockService activeService = null;
private static final String PROXY_DISABLED = str("revanced_block_embedded_ads_entry_1");
private static final String LUMINOUS_SERVICE = str("revanced_block_embedded_ads_entry_2");
private static final String PURPLE_ADBLOCK_SERVICE = str("revanced_block_embedded_ads_entry_3");
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
var originalRequest = chain.request();
if (Settings.BLOCK_EMBEDDED_ADS.get().equals(PROXY_DISABLED)) {
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Intercepted request to URL:" + originalRequest.url());
// Skip if not HLS manifest request
if (!originalRequest.url().host().contains("usher.ttvnw.net")) {
return chain.proceed(originalRequest);
}
final String isVod;
if (IAdblockService.isVod(originalRequest)) isVod = "yes";
else isVod = "no";
Logger.printDebug(() -> "Found HLS manifest request. Is VOD? " +
isVod +
"; Channel: " +
IAdblockService.channelName(originalRequest)
);
// None of the services support VODs currently
if (IAdblockService.isVod(originalRequest)) return chain.proceed(originalRequest);
updateActiveService();
if (activeService != null) {
var available = activeService.isAvailable();
var rewritten = activeService.rewriteHlsRequest(originalRequest);
if (!available || rewritten == null) {
Utils.showToastShort(String.format(
str("revanced_embedded_ads_service_unavailable"), activeService.friendlyName()
));
return chain.proceed(originalRequest);
}
Logger.printDebug(() -> "Rewritten HLS stream URL: " + rewritten.url());
var maxAttempts = activeService.maxAttempts();
for (var i = 1; i <= maxAttempts; i++) {
// Execute rewritten request and close body to allow multiple proceed() calls
var response = chain.proceed(rewritten);
response.close();
if (!response.isSuccessful()) {
int attempt = i;
Logger.printException(() -> "Request failed (attempt " +
attempt +
"/" + maxAttempts + "): HTTP error " +
response.code() +
" (" + response.message() + ")"
);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Logger.printException(() -> "Failed to sleep", e);
}
} else {
// Accept response from ad blocker
Logger.printDebug(() -> "Ad-blocker used");
return chain.proceed(rewritten);
}
}
// maxAttempts exceeded; giving up on using the ad blocker
Utils.showToastLong(String.format(
str("revanced_embedded_ads_service_failed"),
activeService.friendlyName())
);
}
// Adblock disabled
return chain.proceed(originalRequest);
}
private void updateActiveService() {
var current = Settings.BLOCK_EMBEDDED_ADS.get();
if (current.equals(LUMINOUS_SERVICE) && !(activeService instanceof LuminousService))
activeService = new LuminousService();
else if (current.equals(PURPLE_ADBLOCK_SERVICE) && !(activeService instanceof PurpleAdblockService))
activeService = new PurpleAdblockService();
else if (current.equals(PROXY_DISABLED))
activeService = null;
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.extension.twitch.api;
import retrofit2.Retrofit;
public class RetrofitClient {
private static RetrofitClient instance = null;
private final PurpleAdblockApi purpleAdblockApi;
private RetrofitClient() {
Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build();
purpleAdblockApi = retrofit.create(PurpleAdblockApi.class);
}
public static synchronized RetrofitClient getInstance() {
if (instance == null) {
instance = new RetrofitClient();
}
return instance;
}
public PurpleAdblockApi getPurpleAdblockApi() {
return purpleAdblockApi;
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AudioAdsPatch {
public static boolean shouldBlockAudioAds() {
return Settings.BLOCK_AUDIO_ADS.get();
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class AutoClaimChannelPointsPatch {
public static boolean shouldAutoClaim() {
return Settings.AUTO_CLAIM_CHANNEL_POINTS.get();
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class DebugModePatch {
public static boolean isDebugModeEnabled() {
return Settings.TWITCH_DEBUG_MODE.get();
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.api.RequestInterceptor;
@SuppressWarnings("unused")
public class EmbeddedAdsPatch {
public static RequestInterceptor createRequestInterceptor() {
return new RequestInterceptor();
}
}

View File

@ -0,0 +1,51 @@
package app.revanced.extension.twitch.patches;
import static app.revanced.extension.shared.StringRef.str;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.SpannedString;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import androidx.annotation.Nullable;
import app.revanced.extension.twitch.settings.Settings;
import tv.twitch.android.shared.chat.util.ClickableUsernameSpan;
@SuppressWarnings("unused")
public class ShowDeletedMessagesPatch {
/**
* Injection point.
*/
public static boolean shouldUseSpoiler() {
return "spoiler".equals(Settings.SHOW_DELETED_MESSAGES.get());
}
public static boolean shouldCrossOut() {
return "cross-out".equals(Settings.SHOW_DELETED_MESSAGES.get());
}
@Nullable
public static Spanned reformatDeletedMessage(Spanned original) {
if (!shouldCrossOut())
return null;
SpannableStringBuilder ssb = new SpannableStringBuilder(original);
ssb.setSpan(new StrikethroughSpan(), 0, original.length(), 0);
ssb.append(" (").append(str("revanced_deleted_msg")).append(")");
ssb.setSpan(new StyleSpan(Typeface.ITALIC), original.length(), ssb.length(), 0);
// Gray-out username
ClickableUsernameSpan[] usernameSpans = original.getSpans(0, original.length(), ClickableUsernameSpan.class);
if (usernameSpans.length > 0) {
ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#ADADB8")), 0, original.getSpanEnd(usernameSpans[0]), 0);
}
return new SpannedString(ssb);
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.extension.twitch.patches;
import app.revanced.extension.twitch.settings.Settings;
@SuppressWarnings("unused")
public class VideoAdsPatch {
public static boolean shouldBlockVideoAds() {
return Settings.BLOCK_VIDEO_ADS.get();
}
}

View File

@ -0,0 +1,112 @@
package app.revanced.extension.twitch.settings;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.ActionBar;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.twitch.settings.preference.ReVancedPreferenceFragment;
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
import tv.twitch.android.settings.SettingsActivity;
import java.util.ArrayList;
import java.util.List;
/**
* Hooks AppCompatActivity.
* <p>
* This class is responsible for injecting our own fragment by replacing the AppCompatActivity.
* @noinspection unused
*/
public class AppCompatActivityHook {
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
/**
* Launches SettingsActivity and show ReVanced settings
*/
public static void startSettingsActivity() {
Logger.printDebug(() -> "Launching ReVanced settings");
final var context = Utils.getContext();
if (context != null) {
Intent intent = new Intent(context, SettingsActivity.class);
Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_REVANCED_SETTINGS, true);
intent.putExtras(bundle);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
/**
* Helper for easy access in smali
* @return Returns string resource id
*/
public static int getReVancedSettingsString() {
return app.revanced.extension.twitch.Utils.getStringId("revanced_settings");
}
/**
* Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated
* @return Returns a modified list of menu groups
*/
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
if (groups.isEmpty()) {
// Create new menu group if none exist yet
List<Object> items = new ArrayList<>();
items.add(revancedEntry);
groups.add(new SettingsMenuGroup(items));
} else {
// Add to last menu group
int groupIdx = groups.size() - 1;
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
items.add(revancedEntry);
groups.add(new SettingsMenuGroup(items));
}
Logger.printDebug(() -> settingGroups.size() + " menu groups in list");
return groups;
}
/**
* Intercepts settings menu group onclick events
* @return Returns true if handled, otherwise false
*/
@SuppressWarnings("rawtypes")
public static boolean handleSettingMenuOnClick(Enum item) {
Logger.printDebug(() -> "item " + item.ordinal() + " clicked");
if (item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) {
return false;
}
startSettingsActivity();
return true;
}
/**
* Intercepts fragment loading in SettingsActivity.onCreate
* @return Returns true if the revanced settings have been requested by the user, otherwise false
*/
public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) {
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
Logger.printDebug(() -> "Revanced settings not requested");
return false; // User wants to enter another settings fragment
}
Logger.printDebug(() -> "ReVanced settings requested");
ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment();
ActionBar supportActionBar = base.getSupportActionBar();
if (supportActionBar != null)
supportActionBar.setTitle(app.revanced.extension.twitch.Utils.getStringId("revanced_settings"));
base.getFragmentManager()
.beginTransaction()
.replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment)
.commit();
return true;
}
}

View File

@ -0,0 +1,25 @@
package app.revanced.extension.twitch.settings;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.StringSetting;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
public class Settings extends BaseSettings {
/* Ads */
public static final BooleanSetting BLOCK_VIDEO_ADS = new BooleanSetting("revanced_block_video_ads", TRUE);
public static final BooleanSetting BLOCK_AUDIO_ADS = new BooleanSetting("revanced_block_audio_ads", TRUE);
public static final StringSetting BLOCK_EMBEDDED_ADS = new StringSetting("revanced_block_embedded_ads", "luminous");
/* Chat */
public static final StringSetting SHOW_DELETED_MESSAGES = new StringSetting("revanced_show_deleted_messages", "cross-out");
public static final BooleanSetting AUTO_CLAIM_CHANNEL_POINTS = new BooleanSetting("revanced_auto_claim_channel_points", TRUE);
/* Misc */
/**
* Not to be confused with {@link BaseSettings#DEBUG}.
*/
public static final BooleanSetting TWITCH_DEBUG_MODE = new BooleanSetting("revanced_twitch_debug_mode", FALSE, true);
}

View File

@ -0,0 +1,23 @@
package app.revanced.extension.twitch.settings.preference;
import android.content.Context;
import android.graphics.Color;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
public class CustomPreferenceCategory extends PreferenceCategory {
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onBindView(View rootView) {
super.onBindView(rootView);
if(rootView instanceof TextView) {
((TextView) rootView).setTextColor(Color.parseColor("#8161b3"));
}
}
}

View File

@ -0,0 +1,21 @@
package app.revanced.extension.twitch.settings.preference;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
import app.revanced.extension.twitch.settings.Settings;
/**
* Preference fragment for ReVanced settings
*/
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
@Override
protected void initialize() {
super.initialize();
// Do anything that forces this apps Settings bundle to load.
if (Settings.BLOCK_VIDEO_ADS.get()) {
Logger.printDebug(() -> "Block video ads enabled"); // Any statement that references the app settings.
}
}
}

View File

@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 33
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@ -0,0 +1 @@
<manifest/>

View File

@ -0,0 +1,14 @@
package tv.twitch.android.feature.settings.menu;
import java.util.List;
// Dummy
public final class SettingsMenuGroup {
public SettingsMenuGroup(List<Object> settingsMenuItems) {
throw new UnsupportedOperationException("Stub");
}
public List<Object> getSettingsMenuItems() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@ -0,0 +1,5 @@
package tv.twitch.android.settings;
import android.app.Activity;
public class SettingsActivity extends Activity {}

View File

@ -0,0 +1,9 @@
package tv.twitch.android.shared.chat.util;
import android.text.style.ClickableSpan;
import android.view.View;
public final class ClickableUsernameSpan extends ClickableSpan {
@Override
public void onClick(View widget) {}
}