├── .gitignore ├── Resources ├── Config.h ├── TwitchAdBlock.plist ├── include ├── AmazonIVSPlayer │ ├── IVSCue.h │ ├── IVSPlayer.h │ └── IVSTextMetadataCue.h ├── Twitch │ ├── VersionLabel.h │ ├── TheaterAdController.h │ ├── URLController.h │ ├── LiveHLSURLProvider.h │ ├── HeadlinerFollowingAdManager.h │ ├── SettingsDisclosureCell.h │ ├── AppSettingsViewController.h │ ├── AccountMenuViewController.h │ ├── FollowingViewController.h │ ├── PreferenceSettingsViewController.h │ ├── TWHLSProvider.h │ ├── SimpleTableViewCell.h │ ├── TheaterViewController.h │ ├── ConfigurableAccessoryTableViewCell.h │ └── SettingsSwitchTableViewCell.h ├── CoreServices │ ├── LSResourceProxy.h │ ├── _LSQueryResult.h │ ├── LSBundleProxy.h │ └── LSApplicationProxy.h ├── TwitchKit │ └── TKGraphQL.h └── TwitchCoreUI │ ├── TWThemeableTableViewCell.h │ ├── TWThemeableViewController.h │ ├── TWBaseTableViewCell.h │ ├── TWBaseViewController.h │ ├── UIFont+TwitchCoreUI.h │ ├── StandardTextField.h │ ├── TWBaseInfiniteScrollingViewController.h │ ├── BaseTableViewCell.h │ ├── TWBaseCollectionViewController.h │ ├── TWDefaultThemeManager.h │ ├── TWCoreUITheme.h │ ├── TWBaseTableViewController.h │ └── TWThemeableView.h ├── NSURL+TwitchAdBlock.h ├── NSURLSession+TwitchAdBlock.h ├── .gitmodules ├── NSData+TwitchAdBlock.h ├── Settings.h ├── TWAdBlockSettingsTextFieldTableViewCell.h ├── layout └── Library │ └── Application Support │ └── TwitchAdBlock.bundle │ ├── zh-Hant.lproj │ └── Localizable.strings │ ├── en.lproj │ └── Localizable.strings │ └── es.lproj │ └── Localizable.strings ├── TWAdBlockAssetResourceLoaderDelegate.h ├── TWAdBlockSettingsTextField.h ├── control ├── TWAdBlockSettingsViewController.h ├── NSURLSession+TwitchAdBlock.m ├── Tweak.h ├── LICENSE ├── Makefile ├── TWAdBlockSettingsTextFieldTableViewCell.x ├── NSURL+TwitchAdBlock.m ├── TWAdBlockAssetResourceLoaderDelegate.m ├── NSData+TwitchAdBlock.m ├── Settings.x ├── TWAdBlockSettingsTextField.x ├── .github └── workflows │ └── build.yml ├── Sideloaded.x ├── TWAdBlockSettingsViewController.x └── Tweak.x /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipa -------------------------------------------------------------------------------- /Resources: -------------------------------------------------------------------------------- 1 | layout/Library/Application Support -------------------------------------------------------------------------------- /Config.h: -------------------------------------------------------------------------------- 1 | #define PROXY_ADDR @"proxy.level3tjg.me:6375" -------------------------------------------------------------------------------- /TwitchAdBlock.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "tv.twitch" ); }; } 2 | -------------------------------------------------------------------------------- /include/AmazonIVSPlayer/IVSCue.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface IVSCue : NSObject 4 | @end -------------------------------------------------------------------------------- /include/Twitch/VersionLabel.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch12VersionLabel : UILabel 4 | @end -------------------------------------------------------------------------------- /include/CoreServices/LSResourceProxy.h: -------------------------------------------------------------------------------- 1 | #import "_LSQueryResult.h" 2 | 3 | @interface LSResourceProxy : _LSQueryResult 4 | @end -------------------------------------------------------------------------------- /include/CoreServices/_LSQueryResult.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _LSQueryResult : NSObject 4 | @end -------------------------------------------------------------------------------- /include/TwitchKit/TKGraphQL.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC9TwitchKit9TKGraphQL : NSObject 4 | @end -------------------------------------------------------------------------------- /include/Twitch/TheaterAdController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch19TheaterAdController 4 | @end -------------------------------------------------------------------------------- /include/Twitch/URLController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch13URLController : NSObject 4 | @end -------------------------------------------------------------------------------- /include/Twitch/LiveHLSURLProvider.h: -------------------------------------------------------------------------------- 1 | #import "TWHLSProvider.h" 2 | 3 | @interface _TtC6Twitch18LiveHLSURLProvider : TWHLSProvider 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWThemeableTableViewCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TWThemeableTableViewCell : UITableViewCell 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWThemeableViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TWThemeableViewController : UIViewController 4 | @end -------------------------------------------------------------------------------- /include/CoreServices/LSBundleProxy.h: -------------------------------------------------------------------------------- 1 | #import "LSResourceProxy.h" 2 | 3 | @interface LSBundleProxy : LSResourceProxy 4 | - (NSURL *)dataContainerURL; 5 | @end -------------------------------------------------------------------------------- /include/Twitch/HeadlinerFollowingAdManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch27HeadlinerFollowingAdManager : NSObject 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWBaseTableViewCell.h: -------------------------------------------------------------------------------- 1 | #import "TWThemeableTableViewCell.h" 2 | 3 | @interface TWBaseTableViewCell : TWThemeableTableViewCell 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWBaseViewController.h: -------------------------------------------------------------------------------- 1 | #import "TWThemeableViewController.h" 2 | 3 | @interface TWBaseViewController : TWThemeableViewController 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/UIFont+TwitchCoreUI.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIFont (TwitchCoreUI) 4 | @property(class) UIFont *twitchBody; 5 | @end -------------------------------------------------------------------------------- /NSURL+TwitchAdBlock.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSURL (TwitchAdBlock) 4 | - (NSURL *)twab_URLWithProxyURL:(NSURL *)proxyURL; 5 | @end -------------------------------------------------------------------------------- /include/AmazonIVSPlayer/IVSPlayer.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface IVSPlayer : NSObject 4 | @property(nonatomic, copy) NSURL *path; 5 | @end -------------------------------------------------------------------------------- /include/AmazonIVSPlayer/IVSTextMetadataCue.h: -------------------------------------------------------------------------------- 1 | #import "IVSCue.h" 2 | 3 | @interface IVSTextMetadataCue : IVSCue 4 | @property (nonatomic, readonly) NSString *text; 5 | @end -------------------------------------------------------------------------------- /include/Twitch/SettingsDisclosureCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch22SettingsDisclosureCell : TWBaseTableViewCell 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/StandardTextField.h: -------------------------------------------------------------------------------- 1 | #import "TWThemeableView.h" 2 | 3 | @interface _TtC12TwitchCoreUI17StandardTextField 4 | : TWThemeableView 5 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWBaseInfiniteScrollingViewController.h: -------------------------------------------------------------------------------- 1 | #import "TWBaseViewController.h" 2 | 3 | @interface TWBaseInfiniteScrollingViewController : TWBaseViewController 4 | @end -------------------------------------------------------------------------------- /include/Twitch/AppSettingsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch25AppSettingsViewController : TWBaseTableViewController 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/BaseTableViewCell.h: -------------------------------------------------------------------------------- 1 | #include "TwitchCoreUI/TWThemeableTableViewCell.h" 2 | 3 | @interface _TtC12TwitchCoreUI17BaseTableViewCell : TWThemeableTableViewCell 4 | @end -------------------------------------------------------------------------------- /NSURLSession+TwitchAdBlock.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | @interface NSURLSession (TwitchAdBlock) 4 | - (NSURLSession *)twab_proxySessionWithAddress:(NSString *)address; 5 | @end -------------------------------------------------------------------------------- /include/Twitch/AccountMenuViewController.h: -------------------------------------------------------------------------------- 1 | #include "TwitchCoreUI/TWBaseTableViewController.h" 2 | 3 | @interface _TtC6Twitch25AccountMenuViewController : TWBaseTableViewController 4 | @end -------------------------------------------------------------------------------- /include/Twitch/FollowingViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch23FollowingViewController : TWBaseCollectionViewController 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWBaseCollectionViewController.h: -------------------------------------------------------------------------------- 1 | #import "TWBaseInfiniteScrollingViewController.h" 2 | 3 | @interface TWBaseCollectionViewController : TWBaseInfiniteScrollingViewController 4 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWDefaultThemeManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC12TwitchCoreUI21TWDefaultThemeManager : NSObject 4 | + (instancetype)defaultThemeManager; 5 | @end -------------------------------------------------------------------------------- /include/CoreServices/LSApplicationProxy.h: -------------------------------------------------------------------------------- 1 | #import "LSBundleProxy.h" 2 | 3 | @interface LSApplicationProxy : LSBundleProxy 4 | + (instancetype)applicationProxyForIdentifier:(NSString *)identifier; 5 | @end -------------------------------------------------------------------------------- /include/Twitch/PreferenceSettingsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface _TtC6Twitch32PreferenceSettingsViewController 4 | : TWBaseTableViewController 5 | @end -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "fishhook"] 2 | path = fishhook 3 | url = https://github.com/facebook/fishhook 4 | [submodule "TwitchLoginFix"] 5 | path = TwitchLoginFix 6 | url = https://github.com/level3tjg/TwitchLoginFix 7 | -------------------------------------------------------------------------------- /include/Twitch/TWHLSProvider.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface TWHLSProvider : NSObject 4 | - (NSString *)playerTypeStringForRequestType:(NSInteger)requestType; 5 | - (void)requestManifest; 6 | @end -------------------------------------------------------------------------------- /include/Twitch/SimpleTableViewCell.h: -------------------------------------------------------------------------------- 1 | #include "TwitchCoreUI/BaseTableViewCell.h" 2 | 3 | @interface _TtC6Twitch19SimpleTableViewCell : _TtC12TwitchCoreUI17BaseTableViewCell 4 | - (void)configureWithTitle:(NSString *)title; 5 | @end -------------------------------------------------------------------------------- /NSData+TwitchAdBlock.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSData (TwitchAdBlock) 4 | - (NSData *)twab_requestDataForRequest:(NSURLRequest *)request; 5 | - (NSData *)twab_responseDataForRequest:(NSURLRequest *)request; 6 | @end -------------------------------------------------------------------------------- /include/Twitch/TheaterViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TheaterAdController.h" 3 | 4 | @interface _TtC6Twitch21TheaterViewController : UIViewController { 5 | _TtC6Twitch19TheaterAdController *theaterAdController; 6 | } 7 | @end -------------------------------------------------------------------------------- /Settings.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #import "TWAdBlockSettingsViewController.h" -------------------------------------------------------------------------------- /include/Twitch/ConfigurableAccessoryTableViewCell.h: -------------------------------------------------------------------------------- 1 | #import "SimpleTableViewCell.h" 2 | 3 | @interface _TtC6Twitch34ConfigurableAccessoryTableViewCell : _TtC6Twitch19SimpleTableViewCell 4 | @property (nonatomic, assign) BOOL useDefaultBackgroundColor; 5 | @end -------------------------------------------------------------------------------- /TWAdBlockSettingsTextFieldTableViewCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TWAdBlockSettingsTextField.h" 3 | 4 | @interface TWAdBlockSettingsTextFieldTableViewCell : TWBaseTableViewCell 5 | @property(nonatomic, strong) TWAdBlockSettingsTextField *textField; 6 | @end 7 | -------------------------------------------------------------------------------- /layout/Library/Application Support/TwitchAdBlock.bundle/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "settings.adblock.title"="阻擋廣告"; 2 | "settings.adblock.footer"="選擇您是否要阻擋廣告"; 3 | "settings.proxy.title"="廣告阻擋代理伺服器"; 4 | "settings.proxy.footer"="將特定的請求透過位於無廣告國家/地區的伺服器進行代理"; 5 | "settings.custom_proxy.title"="自訂代理伺服器"; 6 | -------------------------------------------------------------------------------- /TWAdBlockAssetResourceLoaderDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "Config.h" 4 | #import "NSURL+TwitchAdBlock.h" 5 | #import "NSURLSession+TwitchAdBlock.h" 6 | 7 | @interface TWAdBlockAssetResourceLoaderDelegate : NSObject 8 | @end -------------------------------------------------------------------------------- /TWAdBlockSettingsTextField.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface TWAdBlockSettingsTextField : _TtC12TwitchCoreUI17StandardTextField 5 | @property(nonatomic, weak) id delegate; 6 | @property(nonatomic, readonly) UITextField *textField; 7 | @end 8 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.level3tjg.twitchadblock 2 | Name: TwitchAdBlock 3 | Version: 1.0.0 4 | Architecture: iphoneos-arm 5 | Description: Block Twitch video ads 6 | Depiction: https://level3tjg.me/repo/depictions/?p=com.level3tjg.twitchadblock 7 | Maintainer: level3tjg 8 | Author: level3tjg 9 | Section: Tweaks 10 | Depends: mobilesubstrate 11 | -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWCoreUITheme.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol TWCoreUITheme 4 | @required 5 | @property(nonatomic, strong, readonly) UIColor *backgroundAccentColor; 6 | @property(nonatomic, strong, readonly) UIColor *backgroundBodyColor; 7 | @property(nonatomic, strong, readonly) UIColor *backgroundInputColor; 8 | @end 9 | -------------------------------------------------------------------------------- /layout/Library/Application Support/TwitchAdBlock.bundle/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "settings.adblock.title"="Ad Block"; 2 | "settings.adblock.footer"="Choose whether or not you want to block ads"; 3 | "settings.proxy.title"="Ad Block Proxy"; 4 | "settings.proxy.footer"="Proxy specific requests through a proxy server based in an ad-free country"; 5 | "settings.custom_proxy.title"="Custom Proxy"; 6 | -------------------------------------------------------------------------------- /layout/Library/Application Support/TwitchAdBlock.bundle/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | "settings.adblock.title"="Bloqueo de anuncios"; 2 | "settings.adblock.footer"="Elige si quieres bloquear los anuncios"; 3 | "settings.proxy.title"="Proxy de bloqueo de anuncios"; 4 | "settings.proxy.footer"="Solicitudes específicas a través de un servidor proxy ubicado en un país sin publicidad"; 5 | "settings.custom_proxy.title"="Proxy personalizado"; 6 | -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWBaseTableViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface TWBaseTableViewController 5 | : UIViewController 6 | @property UITableView *tableView; 7 | - (instancetype)initWithTableViewStyle:(NSInteger)style 8 | themeManager: 9 | (_TtC12TwitchCoreUI21TWDefaultThemeManager *) 10 | themeManager; 11 | @end -------------------------------------------------------------------------------- /include/TwitchCoreUI/TWThemeableView.h: -------------------------------------------------------------------------------- 1 | #import "TWCoreUITheme.h" 2 | #import "TWDefaultThemeManager.h" 3 | #import 4 | 5 | @interface TWThemeableView : UIView 6 | @property BOOL applyShadowPathForElevation; 7 | @property id lastConfiguredTheme; 8 | @property _TtC12TwitchCoreUI21TWDefaultThemeManager *themeManager; 9 | - (instancetype)initWithFrame:(CGRect)frame 10 | themeManager: 11 | (_TtC12TwitchCoreUI21TWDefaultThemeManager *)themeManager; 12 | @end -------------------------------------------------------------------------------- /TWAdBlockSettingsViewController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import "Config.h" 5 | #import "TWAdBlockSettingsTextFieldTableViewCell.h" 6 | 7 | @interface TWAdBlockSettingsViewController 8 | : TWBaseTableViewController 9 | @property(nonatomic, assign) BOOL adblockEnabled; 10 | @property(nonatomic, assign) BOOL proxyEnabled; 11 | @property(nonatomic, assign) BOOL customProxyEnabled; 12 | @end 13 | -------------------------------------------------------------------------------- /include/Twitch/SettingsSwitchTableViewCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol SettingsSwitchTableViewCellDelegate 4 | - (void)settingsCellSwitchToggled:(id)sender; 5 | @end 6 | 7 | @interface _TtC6Twitch27SettingsSwitchTableViewCell : TWBaseTableViewCell 8 | @property BOOL isOn; 9 | @property id delegate; 10 | - (void)configureWithTitle:(NSString *)title 11 | subtitle:(NSString *)subtitle 12 | isEnabled:(BOOL)isEnabled 13 | isOn:(BOOL)isOn 14 | accessibilityIdentifier:(NSString *)accessibilityIdentifier; 15 | @end -------------------------------------------------------------------------------- /NSURLSession+TwitchAdBlock.m: -------------------------------------------------------------------------------- 1 | #import "NSURLSession+TwitchAdBlock.h" 2 | 3 | @implementation NSURLSession (TwitchAdBlock) 4 | - (NSURLSession *)twab_proxySessionWithAddress:(NSString *)address { 5 | NSURLSessionConfiguration *configuration = 6 | self.configuration ?: NSURLSessionConfiguration.defaultSessionConfiguration; 7 | NSArray *addressComponents = [address componentsSeparatedByString:@":"]; 8 | NSString *host = addressComponents[0]; 9 | NSNumber *port = addressComponents.count > 1 ? @(addressComponents[1].integerValue) : @8080; 10 | configuration.connectionProxyDictionary = @{ 11 | @"HTTPSEnable" : @YES, 12 | @"HTTPSProxy" : host, 13 | @"HTTPSPort" : port, 14 | }; 15 | return [NSURLSession sessionWithConfiguration:configuration 16 | delegate:self.delegate 17 | delegateQueue:self.delegateQueue]; 18 | } 19 | @end -------------------------------------------------------------------------------- /Tweak.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import 8 | #import 9 | #import 10 | #import 11 | #import 12 | #import 13 | 14 | #import "Config.h" 15 | #import "NSData+TwitchAdBlock.h" 16 | #import "NSURL+TwitchAdBlock.h" 17 | #import "NSURLSession+TwitchAdBlock.h" 18 | #import "TWAdBlockAssetResourceLoaderDelegate.h" 19 | #import "fishhook/fishhook.h" 20 | 21 | @interface _TtC6Twitch27AssetResourceLoaderDelegate : NSObject 22 | - (BOOL)handleLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest; 23 | @end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 level3tjg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifeq ($(SIDELOADED),1) 2 | MODULES = jailed 3 | endif 4 | 5 | PACKAGE_VERSION = 0.1.5 6 | ifdef APP_VERSION 7 | PACKAGE_VERSION := $(APP_VERSION)-$(PACKAGE_VERSION) 8 | endif 9 | 10 | ifeq ($(STS),1) 11 | PACKAGE_VERSION := $(PACKAGE_VERSION)-STS 12 | endif 13 | ifeq ($(LTS),1) 14 | PACKAGE_VERSION := $(PACKAGE_VERSION)-LTS 15 | endif 16 | 17 | TARGET := iphone:clang:latest:12.4 18 | INSTALL_TARGET_PROCESSES = Twitch 19 | 20 | ARCHS = arm64 21 | 22 | ADDITIONAL_CFLAGS = -Wno-module-import-in-extern-c 23 | 24 | include $(THEOS)/makefiles/common.mk 25 | 26 | TWEAK_NAME = TwitchAdBlock 27 | 28 | $(TWEAK_NAME)_FILES = $(filter-out Sideloaded.x, $(wildcard *.x)) $(wildcard *.*m) fishhook/fishhook.c 29 | $(TWEAK_NAME)_CFLAGS = -fobjc-arc -Iinclude -DPACKAGE_VERSION=@\"$(PACKAGE_VERSION)\" 30 | $(TWEAK_NAME)_LOGOS_DEFAULT_GENERATOR = internal 31 | ifeq ($(SIDELOADED),1) 32 | $(TWEAK_NAME)_FILES += Sideloaded.x 33 | CODESIGN_IPA = 0 34 | ifeq ($(LTS),1) 35 | $(TWEAK_NAME)_INJECT_DYLIBS = $(THEOS_OBJ_DIR)/TwitchLoginFix.dylib 36 | SUBPROJECTS += TwitchLoginFix 37 | include $(THEOS_MAKE_PATH)/aggregate.mk 38 | endif 39 | endif 40 | 41 | include $(THEOS_MAKE_PATH)/tweak.mk 42 | -------------------------------------------------------------------------------- /TWAdBlockSettingsTextFieldTableViewCell.x: -------------------------------------------------------------------------------- 1 | #import "TWAdBlockSettingsTextFieldTableViewCell.h" 2 | 3 | %subclass TWAdBlockSettingsTextFieldTableViewCell : BaseTableViewCell 4 | %property(nonatomic, strong) TWAdBlockSettingsTextField *textField; 5 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 6 | if ((self = %orig)) { 7 | self.textField = [[objc_getClass("TWAdBlockSettingsTextField") alloc] 8 | initWithFrame:self.frame 9 | themeManager:[objc_getClass("_TtC12TwitchCoreUI21TWDefaultThemeManager") 10 | defaultThemeManager]]; 11 | UITextField *textField = object_getIvar( 12 | self.textField, class_getInstanceVariable(object_getClass(self.textField), "textField")); 13 | textField.returnKeyType = UIReturnKeyDone; 14 | [self addSubview:self.textField]; 15 | } 16 | return self; 17 | } 18 | - (void)layoutSubviews { 19 | %orig; 20 | self.textField.frame = self.bounds; 21 | self.textField.layer.cornerRadius = self.layer.cornerRadius; 22 | } 23 | %end 24 | 25 | %ctor { 26 | %init(BaseTableViewCell = 27 | objc_getClass("TWBaseTableViewCell") 28 | ?: objc_getClass("_TtC12TwitchCoreUI17BaseTableViewCell")); 29 | } 30 | -------------------------------------------------------------------------------- /NSURL+TwitchAdBlock.m: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #import "NSURL+TwitchAdBlock.h" 5 | 6 | @implementation NSURL (TwitchAdBlock) 7 | - (NSURL *)twab_URLWithProxyURL:(NSURL *)proxyURL { 8 | BOOL isVOD = [self.path.pathComponents[1] isEqualToString:@"vod"]; 9 | NSString *playlistItem = [self.lastPathComponent stringByDeletingPathExtension]; 10 | __block BOOL isLuminousV1; 11 | dispatch_semaphore_t semaphpore = dispatch_semaphore_create(0); 12 | [[NSURLSession.sharedSession 13 | dataTaskWithRequest:[NSURLRequest 14 | requestWithURL:[proxyURL URLByAppendingPathComponent:@"ping"]] 15 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 16 | isLuminousV1 = [response isKindOfClass:NSHTTPURLResponse.class] && 17 | ((NSHTTPURLResponse *)response).statusCode == 200; 18 | dispatch_semaphore_signal(semaphpore); 19 | }] resume]; 20 | dispatch_semaphore_wait(semaphpore, dispatch_time(DISPATCH_TIME_NOW, 500000000)); 21 | if (isLuminousV1) { 22 | NSString *playlistType = isVOD ? @"vod" : @"playlist"; 23 | return [[proxyURL URLByAppendingPathComponent:playlistType] 24 | URLByAppendingPathComponent:playlistItem]; 25 | } 26 | return self; 27 | } 28 | @end -------------------------------------------------------------------------------- /TWAdBlockAssetResourceLoaderDelegate.m: -------------------------------------------------------------------------------- 1 | #import "TWAdBlockAssetResourceLoaderDelegate.h" 2 | 3 | extern NSUserDefaults *tweakDefaults; 4 | 5 | @implementation TWAdBlockAssetResourceLoaderDelegate 6 | - (BOOL)handleLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { 7 | NSURL *URL = loadingRequest.request.URL; 8 | if (![URL.scheme isEqualToString:@"twab"]) return NO; 9 | AVAssetResourceLoadingDataRequest *dataRequest = loadingRequest.dataRequest; 10 | NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:YES]; 11 | components.scheme = @"https"; 12 | NSMutableURLRequest *request = loadingRequest.request.mutableCopy; 13 | request.URL = components.URL; 14 | NSString *proxy = [tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 15 | ? [tweakDefaults stringForKey:@"TWAdBlockProxy"] 16 | : PROXY_ADDR; 17 | NSURLSession *session = [[NSURLSession alloc] twab_proxySessionWithAddress:proxy]; 18 | [[session dataTaskWithRequest:request 19 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 20 | if (error) return [loadingRequest finishLoadingWithError:error]; 21 | loadingRequest.contentInformationRequest.contentType = AVFileTypeMPEG4; 22 | [dataRequest respondWithData:data]; 23 | [loadingRequest finishLoading]; 24 | }] resume]; 25 | return YES; 26 | } 27 | - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 28 | shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { 29 | return [self handleLoadingRequest:loadingRequest]; 30 | } 31 | - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 32 | shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { 33 | return [self handleLoadingRequest:renewalRequest]; 34 | } 35 | @end -------------------------------------------------------------------------------- /NSData+TwitchAdBlock.m: -------------------------------------------------------------------------------- 1 | #import "NSData+TwitchAdBlock.h" 2 | 3 | @implementation NSData (TwitchAdBlock) 4 | - (NSData *)twab_requestDataForRequest:(NSURLRequest *)request { 5 | if (!request) return self; 6 | NSData *modifiedData; 7 | if ([request.URL.host isEqualToString:@"gql.twitch.tv"] && 8 | [request.URL.path isEqualToString:@"/gql"]) { 9 | NSError *error; 10 | id json = [NSJSONSerialization JSONObjectWithData:self 11 | options:NSJSONReadingMutableContainers 12 | error:&error]; 13 | if (!json || error) return self; 14 | if ([json isKindOfClass:NSMutableDictionary.class]) { 15 | NSMutableDictionary *jsonDictionary = json; 16 | NSString *platform = [NSUUID UUID].UUIDString; 17 | if ([jsonDictionary[@"operationName"] isEqualToString:@"StreamAccessToken"] || 18 | [jsonDictionary[@"query"] containsString:@"StreamAccessToken"] || 19 | [jsonDictionary[@"operationName"] isEqualToString:@"VodAccessToken"]) 20 | jsonDictionary[@"variables"][@"params"][@"platform"] = platform; 21 | else if ([jsonDictionary[@"operationName"] isEqualToString:@"ClipAccessToken"]) 22 | jsonDictionary[@"variables"][@"tokenParams"][@"platform"] = platform; 23 | } 24 | modifiedData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; 25 | if (error) return self; 26 | } 27 | return modifiedData ?: self; 28 | } 29 | - (NSData *)twab_responseDataForRequest:(NSURLRequest *)request { 30 | if (!request) return self; 31 | NSData *modifiedData; 32 | if ([request.URL.host isEqualToString:@"gql.twitch.tv"] && 33 | [request.URL.path isEqualToString:@"/gql"]) { 34 | NSError *error; 35 | id json = [NSJSONSerialization JSONObjectWithData:self 36 | options:NSJSONReadingMutableContainers 37 | error:&error]; 38 | if (!json || error) return self; 39 | if ([json isKindOfClass:NSMutableArray.class]) { 40 | NSMutableArray *jsonArray = json; 41 | for (NSMutableDictionary *operation in jsonArray) { 42 | NSMutableDictionary *feedItems = operation[@"data"][@"feedItems"]; 43 | if (feedItems) 44 | feedItems[@"edges"] = [feedItems[@"edges"] 45 | filteredArrayUsingPredicate:[NSPredicate 46 | predicateWithFormat:@"node.__typename != 'FeedAd'"]]; 47 | } 48 | } 49 | modifiedData = [NSJSONSerialization dataWithJSONObject:json options:0 error:&error]; 50 | if (error) return self; 51 | } 52 | return modifiedData ?: self; 53 | } 54 | @end -------------------------------------------------------------------------------- /Settings.x: -------------------------------------------------------------------------------- 1 | #import "Settings.h" 2 | 3 | extern NSUserDefaults *tweakDefaults; 4 | 5 | %hook _TtC6Twitch25AccountMenuViewController 6 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 7 | if (indexPath.section == [self numberOfSectionsInTableView:tableView] - 1 && 8 | indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - 1) { 9 | UITableViewStyle tableViewStyle = UITableViewStyleGrouped; 10 | if (@available(iOS 13, *)) tableViewStyle = UITableViewStyleInsetGrouped; 11 | TWAdBlockSettingsViewController *adblockSettingsViewController = 12 | [[objc_getClass("TWAdBlockSettingsViewController") alloc] 13 | initWithTableViewStyle:tableViewStyle 14 | themeManager:[objc_getClass("_TtC12TwitchCoreUI21TWDefaultThemeManager") 15 | defaultThemeManager]]; 16 | adblockSettingsViewController.tableView.separatorStyle = 17 | UITableViewCellSeparatorStyleSingleLine; 18 | return [self.navigationController pushViewController:adblockSettingsViewController 19 | 20 | animated:YES]; 21 | } 22 | %orig; 23 | } 24 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 25 | NSInteger numberOfRows = %orig; 26 | if (section == [self numberOfSectionsInTableView:tableView] - 1) numberOfRows++; 27 | return numberOfRows; 28 | } 29 | - (UITableViewCell *)tableView:(UITableView *)tableView 30 | cellForRowAtIndexPath:(NSIndexPath *)indexPath { 31 | if (indexPath.section == [self numberOfSectionsInTableView:tableView] - 1 && 32 | indexPath.row == [self tableView:tableView numberOfRowsInSection:indexPath.section] - 1) { 33 | _TtC6Twitch34ConfigurableAccessoryTableViewCell *cell = 34 | [[objc_getClass("_TtC6Twitch34ConfigurableAccessoryTableViewCell") alloc] 35 | initWithStyle:UITableViewCellStyleSubtitle 36 | reuseIdentifier:@"Twitch.ConfigurableAccessoryTableViewCell"]; 37 | [cell configureWithTitle:@"TwitchAdBlock"]; 38 | NSBundle *twCoreUIBundle = [NSBundle bundleWithIdentifier:@"twitch.TwitchCoreUI"]; 39 | cell.accessoryView = 40 | [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"arrow-forward-Icon" 41 | inBundle:twCoreUIBundle 42 | compatibleWithTraitCollection:nil]]; 43 | cell.useDefaultBackgroundColor = YES; 44 | Ivar customImageViewIvar = class_getInstanceVariable(object_getClass(cell), "customImageView"); 45 | if (!customImageViewIvar) return cell; 46 | UIImageView *customImageView = object_getIvar(cell, customImageViewIvar); 47 | customImageView.image = [UIImage imageNamed:@"Un-Host-Icon" 48 | inBundle:twCoreUIBundle 49 | compatibleWithTraitCollection:nil]; 50 | customImageView.hidden = NO; 51 | return cell; 52 | } 53 | return %orig; 54 | } 55 | %end 56 | -------------------------------------------------------------------------------- /TWAdBlockSettingsTextField.x: -------------------------------------------------------------------------------- 1 | #import "TWAdBlockSettingsTextField.h" 2 | 3 | %subclass TWAdBlockSettingsTextField : _TtC12TwitchCoreUI17StandardTextField 4 | %new 5 | - (id)delegate { 6 | return object_getIvar(self, class_getInstanceVariable(object_getClass(self), "delegate")); 7 | } 8 | %new 9 | - (void)setDelegate:(id)delegate { 10 | object_setIvar(self, class_getInstanceVariable(object_getClass(self), "delegate"), delegate); 11 | } 12 | %new 13 | - (UITextField *)textField { 14 | return object_getIvar(self, class_getInstanceVariable(object_getClass(self), "textField")); 15 | } 16 | - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField { 17 | if (![self.delegate respondsToSelector:@selector(textFieldShouldBeginEditing:)]) return YES; 18 | return [self.delegate textFieldShouldBeginEditing:textField]; 19 | } 20 | - (void)textFieldDidBeginEditing:(UITextField *)textField { 21 | if ([self.delegate respondsToSelector:@selector(textFieldDidBeginEditing:)]) 22 | [self textFieldDidBeginEditing:textField]; 23 | self.backgroundColor = self.lastConfiguredTheme.backgroundBodyColor; 24 | self.layer.borderColor = self.lastConfiguredTheme.backgroundAccentColor.CGColor; 25 | self.layer.borderWidth = 2; 26 | } 27 | - (BOOL)textField:(UITextField *)textField 28 | shouldChangeCharactersInRange:(NSRange)range 29 | replacementString:(NSString *)string { 30 | if (![self.delegate respondsToSelector:@selector(textField: 31 | shouldChangeCharactersInRange:replacementString:)]) 32 | return YES; 33 | return [self.delegate textField:textField 34 | shouldChangeCharactersInRange:range 35 | replacementString:string]; 36 | } 37 | - (BOOL)textFieldShouldEndEditing:(UITextField *)textField { 38 | if (![self.delegate respondsToSelector:@selector(textFieldShouldEndEditing:)]) return YES; 39 | return [self.delegate textFieldShouldEndEditing:textField]; 40 | } 41 | - (void)textFieldDidEndEditing:(UITextField *)textField { 42 | if ([self.delegate respondsToSelector:@selector(textFieldDidEndEditing:)]) 43 | [self.delegate textFieldDidEndEditing:textField]; 44 | self.backgroundColor = self.lastConfiguredTheme.backgroundInputColor; 45 | self.layer.borderWidth = 0; 46 | } 47 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 48 | if (![self.delegate respondsToSelector:@selector(textFieldShouldReturn:)]) 49 | return [textField resignFirstResponder]; 50 | return [self.delegate textFieldShouldReturn:textField]; 51 | } 52 | - (void)textFieldEditingChanged { 53 | } 54 | - (instancetype)initWithFrame:(CGRect)frame 55 | themeManager:(_TtC12TwitchCoreUI21TWDefaultThemeManager *)themeManager { 56 | Class originalClass = object_setClass(self, UIView.class); 57 | if ((self = [self initWithFrame:frame])) { 58 | object_setClass(self, originalClass); 59 | self.themeManager = themeManager; 60 | self.applyShadowPathForElevation = YES; 61 | UITextField *textField = [[objc_getClass("_TtC12TwitchCoreUI13BaseTextField") alloc] init]; 62 | object_setIvar(self, class_getInstanceVariable(object_getClass(self), "textField"), textField); 63 | textField.borderStyle = UITextBorderStyleNone; 64 | textField.spellCheckingType = UITextSpellCheckingTypeNo; 65 | textField.returnKeyType = UIReturnKeyGo; 66 | textField.autocapitalizationType = UITextAutocapitalizationTypeNone; 67 | textField.font = UIFont.twitchBody; 68 | textField.enablesReturnKeyAutomatically = YES; 69 | textField.translatesAutoresizingMaskIntoConstraints = NO; 70 | textField.delegate = self; 71 | [textField addTarget:self 72 | action:@selector(textFieldEditingChanged) 73 | forControlEvents:UIControlEventEditingChanged]; 74 | [self addSubview:textField]; 75 | CGFloat inputPadding = textField.intrinsicContentSize.width * 2; 76 | NSArray *textFieldConstraints = @[ 77 | [self.leftAnchor constraintEqualToAnchor:textField.leftAnchor constant:-inputPadding], 78 | [self.rightAnchor constraintEqualToAnchor:textField.rightAnchor constant:inputPadding], 79 | [self.topAnchor constraintEqualToAnchor:textField.topAnchor], 80 | [self.bottomAnchor constraintEqualToAnchor:textField.bottomAnchor], 81 | ]; 82 | [NSLayoutConstraint activateConstraints:textFieldConstraints]; 83 | } 84 | return self; 85 | } 86 | - (void)dealloc { 87 | self.themeManager = nil; 88 | object_setClass(self, UIView.class); 89 | %orig; 90 | } 91 | %end 92 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | inputs: 7 | create_release: 8 | description: "Build IPA and create draft release" 9 | default: false 10 | type: boolean 11 | ipa_url: 12 | description: "Direct URL to Decrypted IPA file" 13 | type: string 14 | required: false 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Get package info 26 | id: package_info 27 | run: | 28 | version=$(cat Makefile | grep "PACKAGE_VERSION =" | cut -d' ' -f3) 29 | if [ -z $version ]; then 30 | version=$(cat control | grep "Version:" | cut -d' ' -f2) 31 | fi 32 | echo "id=$(cat control | grep "Package:" | cut -d' ' -f2)" >> $GITHUB_OUTPUT 33 | echo "version=$version" >> $GITHUB_OUTPUT 34 | 35 | - name: Install dependencies 36 | if: ${{ inputs.create_release }} 37 | run: | 38 | sudo apt update 39 | sudo apt install -y build-essential checkinstall git autoconf automake libtool-bin rsync llvm xmlstarlet 40 | curl -L https://github.com/libimobiledevice/libplist/releases/download/2.4.0/libplist-2.4.0.tar.bz2 | bzip2 -d | tar -x 41 | cd libplist* 42 | ./configure 43 | sudo make install 44 | sudo ldconfig 45 | 46 | - name: Download IPA (Auto) 47 | if: ${{ inputs.create_release && !inputs.ipa_url }} 48 | uses: level3tjg/decryptedappstore-action@main 49 | with: 50 | appstore_url: "https://apps.apple.com/us/app/twitch-live-streaming/id460177396" 51 | cache: true 52 | path: ${{ github.workspace }}/App.ipa 53 | token: ${{ secrets.DECRYPTEDAPPSTORE_SESSION_TOKEN }} 54 | 55 | - name: Download IPA (Manual) 56 | if: ${{ inputs.create_release && inputs.ipa_url }} 57 | run: | 58 | curl -Lo "${{ github.workspace }}/App.ipa" "${{ inputs.ipa_url }}" 59 | zip -T "${{ github.workspace }}/App.ipa" 60 | 61 | - name: Download STS IPA 62 | if: ${{ inputs.create_release }} 63 | run: | 64 | curl -Lo "${{ github.workspace }}/STS.ipa" "https://nextcloud.level3tjg.me/s/b2WcDdDMN3ZXkYf/download/tv.twitch_19.4.ipa" 65 | zip -T "${{ github.workspace }}/STS.ipa" 66 | 67 | - name: Download LTS IPA 68 | if: ${{ inputs.create_release }} 69 | run: | 70 | curl -Lo "${{ github.workspace }}/LTS.ipa" "https://nextcloud.level3tjg.me/s/QEyiJD3FK6AfxTA/download/tv.twitch_12.8.1.ipa" 71 | zip -T "${{ github.workspace }}/LTS.ipa" 72 | 73 | - name: Get IPA Info 74 | if: ${{ inputs.create_release }} 75 | id: ipa_info 76 | run: | 77 | info=$(unzip -p "${{ github.workspace }}/App.ipa" Payload/*.app/Info.plist) 78 | echo "bundle-id=$(echo $info | xmlstarlet sel -t -v "/plist/dict/key[text()=\"CFBundleIdentifier\"]/following-sibling::*[1]/text()")" >> $GITHUB_OUTPUT 79 | echo "version=$(echo $info | xmlstarlet sel -t -v "/plist/dict/key[text()=\"CFBundleShortVersionString\"]/following-sibling::*[1]/text()")" >> $GITHUB_OUTPUT 80 | 81 | - name: Setup theos 82 | uses: level3tjg/theos-action@main 83 | with: 84 | cache: true 85 | cache-dir-theos: ${{ github.workspace }}/theos 86 | cache-dir-sdks: ${{ github.workspace }}/theos/sdks 87 | 88 | - name: Checkout theos-jailed 89 | if: ${{ inputs.create_release }} 90 | uses: actions/checkout@v4 91 | with: 92 | repository: level3tjg/theos-jailed 93 | path: theos-jailed 94 | submodules: recursive 95 | 96 | - name: Install theos-jailed 97 | if: ${{ inputs.create_release }} 98 | run: | 99 | ./theos-jailed/install 100 | cd libplist* 101 | sudo make uninstall 102 | 103 | - name: Build rootless deb 104 | run: make package 105 | env: 106 | FINALPACKAGE: ${{ inputs.create_release }} 107 | THEOS_PACKAGE_SCHEME: rootless 108 | 109 | - name: Build rootful deb 110 | run: make clean package 111 | env: 112 | FINALPACKAGE: ${{ inputs.create_release }} 113 | 114 | - name: Build IPA 115 | if: ${{ inputs.create_release }} 116 | run: make package 117 | env: 118 | FINALPACKAGE: 1 119 | SIDELOADED: 1 120 | IPA: ${{ github.workspace }}/App.ipa 121 | APP_VERSION: ${{ steps.ipa_info.outputs.version }} 122 | 123 | - name: Build STS IPA 124 | if: ${{ inputs.create_release }} 125 | run: make package 126 | env: 127 | FINALPACKAGE: 1 128 | SIDELOADED: 1 129 | IPA: ${{ github.workspace }}/STS.ipa 130 | APP_VERSION: "19.4" 131 | STS: 1 132 | 133 | - name: Build LTS IPA 134 | if: ${{ inputs.create_release }} 135 | run: make package 136 | env: 137 | FINALPACKAGE: 1 138 | SIDELOADED: 1 139 | IPA: ${{ github.workspace }}/LTS.ipa 140 | APP_VERSION: "12.8.1" 141 | LTS: 1 142 | 143 | - name: Upload artifacts 144 | uses: actions/upload-artifact@v4 145 | with: 146 | name: ${{ steps.package_info.outputs.id }}_${{ steps.package_info.outputs.version }} 147 | path: packages/* 148 | 149 | - name: Create release 150 | if: ${{ inputs.create_release }} 151 | uses: softprops/action-gh-release@v2 152 | with: 153 | draft: true 154 | files: packages/* 155 | tag_name: v${{ steps.ipa_info.outputs.version }}-${{ steps.package_info.outputs.version }} -------------------------------------------------------------------------------- /Sideloaded.x: -------------------------------------------------------------------------------- 1 | // https://github.com/PoomSmart/IAmYouTube/blob/main/Tweak.x 2 | // Allows low latency player while sideloaded 3 | 4 | #import 5 | #import 6 | #import 7 | #import "fishhook/fishhook.h" 8 | 9 | #define TW_BUNDLE_ID @"tv.twitch" 10 | #define TW_NAME @"Twitch" 11 | 12 | %hook NSBundle 13 | 14 | - (NSString *)bundleIdentifier { 15 | NSArray *address = [NSThread callStackReturnAddresses]; 16 | Dl_info info; 17 | if (dladdr((void *)[address[2] longLongValue], &info) == 0) return %orig; 18 | NSString *path = [NSString stringWithUTF8String:info.dli_fname]; 19 | if ([path hasPrefix:NSBundle.mainBundle.bundlePath]) return TW_BUNDLE_ID; 20 | return %orig; 21 | } 22 | 23 | - (id)objectForInfoDictionaryKey:(NSString *)key { 24 | if ([key isEqualToString:@"CFBundleIdentifier"]) return TW_BUNDLE_ID; 25 | if ([key isEqualToString:@"CFBundleDisplayName"] || [key isEqualToString:@"CFBundleName"]) 26 | return TW_NAME; 27 | return %orig; 28 | } 29 | 30 | %end 31 | 32 | // https://github.com/opa334/IGSideloadFix 33 | 34 | NSString *keychainAccessGroup; 35 | NSURL *fakeGroupContainerURL; 36 | 37 | void createDirectoryIfNotExists(NSURL *URL) { 38 | if (![URL checkResourceIsReachableAndReturnError:nil]) { 39 | [[NSFileManager defaultManager] createDirectoryAtURL:URL 40 | withIntermediateDirectories:YES 41 | attributes:nil 42 | error:nil]; 43 | } 44 | } 45 | 46 | %group SideloadedFixes 47 | 48 | %hook NSFileManager 49 | 50 | - (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier { 51 | NSURL *fakeURL = [fakeGroupContainerURL URLByAppendingPathComponent:groupIdentifier]; 52 | 53 | createDirectoryIfNotExists(fakeURL); 54 | createDirectoryIfNotExists([fakeURL URLByAppendingPathComponent:@"Library"]); 55 | createDirectoryIfNotExists([fakeURL URLByAppendingPathComponent:@"Library/Caches"]); 56 | 57 | return fakeURL; 58 | } 59 | 60 | %end 61 | 62 | static void loadKeychainAccessGroup() { 63 | NSDictionary *dummyItem = @{ 64 | (__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword, 65 | (__bridge id)kSecAttrAccount : @"dummyItem", 66 | (__bridge id)kSecAttrService : @"dummyService", 67 | (__bridge id)kSecReturnAttributes : @YES, 68 | }; 69 | 70 | CFTypeRef result; 71 | OSStatus ret = SecItemCopyMatching((__bridge CFDictionaryRef)dummyItem, &result); 72 | if (ret == -25300) { 73 | ret = SecItemAdd((__bridge CFDictionaryRef)dummyItem, &result); 74 | } 75 | 76 | if (ret == 0 && result) { 77 | NSDictionary *resultDict = (__bridge id)result; 78 | keychainAccessGroup = resultDict[(__bridge id)kSecAttrAccessGroup]; 79 | NSLog(@"loaded keychainAccessGroup: %@", keychainAccessGroup); 80 | } 81 | } 82 | 83 | %end 84 | 85 | static OSStatus (*orig_SecItemAdd)(CFDictionaryRef, CFTypeRef *); 86 | static OSStatus hook_SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result) { 87 | if (CFDictionaryContainsKey(attributes, kSecAttrAccessGroup)) { 88 | CFMutableDictionaryRef mutableAttributes = 89 | CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, attributes); 90 | CFDictionarySetValue(mutableAttributes, kSecAttrAccessGroup, 91 | (__bridge void *)keychainAccessGroup); 92 | attributes = CFDictionaryCreateCopy(kCFAllocatorDefault, mutableAttributes); 93 | } 94 | return orig_SecItemAdd(attributes, result); 95 | } 96 | 97 | static OSStatus (*orig_SecItemCopyMatching)(CFDictionaryRef, CFTypeRef *); 98 | static OSStatus hook_SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result) { 99 | if (CFDictionaryContainsKey(query, kSecAttrAccessGroup)) { 100 | CFMutableDictionaryRef mutableQuery = 101 | CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); 102 | CFDictionarySetValue(mutableQuery, kSecAttrAccessGroup, (__bridge void *)keychainAccessGroup); 103 | query = CFDictionaryCreateCopy(kCFAllocatorDefault, mutableQuery); 104 | } 105 | return orig_SecItemCopyMatching(query, result); 106 | } 107 | 108 | static OSStatus (*orig_SecItemUpdate)(CFDictionaryRef, CFDictionaryRef); 109 | static OSStatus hook_SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate) { 110 | if (CFDictionaryContainsKey(query, kSecAttrAccessGroup)) { 111 | CFMutableDictionaryRef mutableQuery = 112 | CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); 113 | CFDictionarySetValue(mutableQuery, kSecAttrAccessGroup, (__bridge void *)keychainAccessGroup); 114 | query = CFDictionaryCreateCopy(kCFAllocatorDefault, mutableQuery); 115 | } 116 | return orig_SecItemUpdate(query, attributesToUpdate); 117 | } 118 | 119 | static OSStatus (*orig_SecItemDelete)(CFDictionaryRef); 120 | static OSStatus hook_SecItemDelete(CFDictionaryRef query) { 121 | if (CFDictionaryContainsKey(query, kSecAttrAccessGroup)) { 122 | CFMutableDictionaryRef mutableQuery = 123 | CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, query); 124 | CFDictionarySetValue(mutableQuery, kSecAttrAccessGroup, (__bridge void *)keychainAccessGroup); 125 | query = CFDictionaryCreateCopy(kCFAllocatorDefault, mutableQuery); 126 | } 127 | return orig_SecItemDelete(query); 128 | } 129 | 130 | static void initSideloadedFixes() { 131 | fakeGroupContainerURL = 132 | [NSURL fileURLWithPath:[NSHomeDirectory() 133 | stringByAppendingPathComponent:@"Documents/FakeGroupContainers"] 134 | isDirectory:YES]; 135 | loadKeychainAccessGroup(); 136 | rebind_symbols( 137 | (struct rebinding[]){ 138 | {"SecItemAdd", (void *)hook_SecItemAdd, (void **)&orig_SecItemAdd}, 139 | {"SecItemCopyMatching", (void *)hook_SecItemCopyMatching, 140 | (void **)&orig_SecItemCopyMatching}, 141 | {"SecItemUpdate", (void *)hook_SecItemUpdate, (void **)&orig_SecItemUpdate}, 142 | {"SecItemDelete", (void *)hook_SecItemDelete, (void **)&orig_SecItemDelete}, 143 | }, 144 | 4); 145 | %init(SideloadedFixes); 146 | } 147 | 148 | %ctor { 149 | %init; 150 | initSideloadedFixes(); 151 | } 152 | -------------------------------------------------------------------------------- /TWAdBlockSettingsViewController.x: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TWAdBlockSettingsViewController.h" 3 | 4 | extern NSBundle *tweakBundle; 5 | extern NSUserDefaults *tweakDefaults; 6 | 7 | #define LOC(x, d) [tweakBundle localizedStringForKey:x value:d table:nil] 8 | 9 | %hook _TtC6Twitch27SettingsSwitchTableViewCell 10 | %new 11 | - (id)delegate { 12 | return object_getIvar(self, class_getInstanceVariable(object_getClass(self), "delegate")); 13 | } 14 | %new 15 | - (void)setDelegate:(id)delegate { 16 | object_setIvar(self, class_getInstanceVariable(object_getClass(self), "delegate"), delegate); 17 | } 18 | %new 19 | - (BOOL)isOn { 20 | return [object_getIvar( 21 | self, class_getInstanceVariable(object_getClass(self), "$__lazy_storage_$_switchView")) isOn]; 22 | } 23 | %new 24 | - (void)configureWithTitle:(NSString *)title 25 | subtitle:(NSString *)subtitle 26 | isEnabled:(BOOL)isEnabled 27 | isOn:(BOOL)isOn 28 | accessibilityIdentifier:(NSString *)accessibilityIdentifier { 29 | self.textLabel.text = title; 30 | self.detailTextLabel.text = subtitle; 31 | UISwitch *switchView = object_getIvar( 32 | self, class_getInstanceVariable(object_getClass(self), "$__lazy_storage_$_switchView")); 33 | switchView.enabled = isEnabled; 34 | switchView.on = isOn; 35 | self.accessibilityIdentifier = accessibilityIdentifier; 36 | } 37 | - (void)settingsSwitchToggled { 38 | if (![self.delegate respondsToSelector:@selector(settingsCellSwitchToggled:)]) 39 | return %orig; 40 | [self.delegate settingsCellSwitchToggled:self]; 41 | } 42 | %end 43 | 44 | %subclass TWAdBlockSettingsViewController : TWBaseTableViewController 45 | %property(nonatomic, assign) BOOL adblockEnabled; 46 | %property(nonatomic, assign) BOOL proxyEnabled; 47 | %property(nonatomic, assign) BOOL customProxyEnabled; 48 | - (instancetype)initWithTableViewStyle:(NSInteger)tableViewStyle themeManager:(id)themeManager { 49 | if ((self = %orig)) { 50 | self.adblockEnabled = [tweakDefaults boolForKey:@"TWAdBlockEnabled"]; 51 | self.proxyEnabled = [tweakDefaults boolForKey:@"TWAdBlockProxyEnabled"]; 52 | self.customProxyEnabled = [tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"]; 53 | } 54 | return self; 55 | } 56 | - (void)viewDidLoad { 57 | %orig; 58 | self.title = @"TwitchAdBlock"; 59 | } 60 | %new 61 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 62 | return self.adblockEnabled ? 3 : 2; 63 | } 64 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 65 | switch (section) { 66 | case 0: 67 | return 1; 68 | case 1: 69 | return self.adblockEnabled ? self.proxyEnabled ? self.customProxyEnabled ? 3 : 2 : 1 : 0; 70 | default: 71 | return 0; 72 | } 73 | } 74 | - (UITableViewCell *)tableView:(UITableView *)tableView 75 | cellForRowAtIndexPath:(NSIndexPath *)indexPath { 76 | UITableViewCell *cell; 77 | switch (indexPath.section) { 78 | case 0: 79 | cell = [[objc_getClass("_TtC6Twitch27SettingsSwitchTableViewCell") alloc] 80 | initWithStyle:UITableViewCellStyleDefault 81 | reuseIdentifier:@"AdBlockSwitchCell"]; 82 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell 83 | configureWithTitle:LOC(@"settings.adblock.title", @"Ad Block") 84 | subtitle:nil 85 | isEnabled:YES 86 | isOn:[tweakDefaults boolForKey:@"TWAdBlockEnabled"] 87 | accessibilityIdentifier:@"AdBlockSwitchCell"]; 88 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell setDelegate:self]; 89 | return cell; 90 | case 1: 91 | switch (indexPath.row) { 92 | case 0: 93 | cell = [[objc_getClass("_TtC6Twitch27SettingsSwitchTableViewCell") alloc] 94 | initWithStyle:UITableViewCellStyleDefault 95 | reuseIdentifier:@"AdBlockProxySwitchCell"]; 96 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell 97 | configureWithTitle:LOC(@"settings.proxy.title", @"Ad Block Proxy") 98 | subtitle:nil 99 | isEnabled:YES 100 | isOn:[tweakDefaults boolForKey:@"TWAdBlockProxyEnabled"] 101 | accessibilityIdentifier:@"AdBlockProxySwitchCell"]; 102 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell setDelegate:self]; 103 | return cell; 104 | case 1: 105 | cell = [[objc_getClass("_TtC6Twitch27SettingsSwitchTableViewCell") alloc] 106 | initWithStyle:UITableViewCellStyleDefault 107 | reuseIdentifier:@"AdBlockCustomProxySwitchCell"]; 108 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell 109 | configureWithTitle:LOC(@"settings.custom_proxy.title", @"Custom Proxy") 110 | subtitle:nil 111 | isEnabled:YES 112 | isOn:[tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 113 | accessibilityIdentifier:@"AdBlockCustomProxySwitchCell"]; 114 | [(_TtC6Twitch27SettingsSwitchTableViewCell *)cell setDelegate:self]; 115 | return cell; 116 | case 2: 117 | cell = [[objc_getClass("TWAdBlockSettingsTextFieldTableViewCell") alloc] 118 | initWithStyle:UITableViewCellStyleDefault 119 | reuseIdentifier:@"TWAdBlockProxy"]; 120 | TWAdBlockSettingsTextField *textField = 121 | ((TWAdBlockSettingsTextFieldTableViewCell *)cell).textField; 122 | textField.textField.placeholder = PROXY_ADDR; 123 | textField.textField.text = [tweakDefaults stringForKey:@"TWAdBlockProxy"]; 124 | textField.delegate = self; 125 | return cell; 126 | } 127 | default: 128 | return nil; 129 | } 130 | } 131 | %new 132 | - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section { 133 | NSString *title; 134 | switch (section) { 135 | case 0: 136 | title = LOC(@"settings.adblock.footer", @"Choose whether or not you want to block ads"); 137 | break; 138 | case 1: 139 | title = LOC(@"settings.proxy.footer", 140 | @"Proxy specific requests through a proxy server based in an ad-free country"); 141 | if (self.adblockEnabled) break; 142 | case 2: { 143 | _TtC6Twitch12VersionLabel *versionLabel = 144 | [[objc_getClass("_TtC6Twitch12VersionLabel") alloc] initWithFrame:CGRectZero]; 145 | versionLabel.text = @"TwitchAdBlock v" PACKAGE_VERSION; 146 | UIStackView *footerStackView = 147 | [[UIStackView alloc] initWithArrangedSubviews:@[ versionLabel ]]; 148 | return footerStackView; 149 | } 150 | default: 151 | return nil; 152 | } 153 | UITableViewHeaderFooterView *footerView = 154 | [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:nil]; 155 | footerView.textLabel.text = title; 156 | footerView.textLabel.numberOfLines = 0; 157 | return footerView; 158 | } 159 | %new 160 | - (void)settingsCellSwitchToggled:(UISwitch *)sender { 161 | if ([sender.accessibilityIdentifier isEqualToString:@"AdBlockSwitchCell"]) { 162 | [tweakDefaults setBool:sender.isOn forKey:@"TWAdBlockEnabled"]; 163 | self.adblockEnabled = sender.isOn; 164 | 165 | NSIndexSet *sections = [NSIndexSet indexSetWithIndex:1]; 166 | if (sender.isOn) 167 | [self.tableView insertSections:sections withRowAnimation:UITableViewRowAnimationFade]; 168 | else 169 | [self.tableView deleteSections:sections withRowAnimation:UITableViewRowAnimationFade]; 170 | } else if ([sender.accessibilityIdentifier isEqualToString:@"AdBlockProxySwitchCell"]) { 171 | [tweakDefaults setBool:sender.isOn forKey:@"TWAdBlockProxyEnabled"]; 172 | self.proxyEnabled = sender.isOn; 173 | 174 | NSMutableArray *indexPaths = [NSMutableArray array]; 175 | [indexPaths addObject:[NSIndexPath indexPathForRow:1 inSection:1]]; 176 | if (self.customProxyEnabled) [indexPaths addObject:[NSIndexPath indexPathForRow:2 inSection:1]]; 177 | if (sender.isOn) 178 | [self.tableView insertRowsAtIndexPaths:indexPaths 179 | withRowAnimation:UITableViewRowAnimationFade]; 180 | else 181 | [self.tableView deleteRowsAtIndexPaths:indexPaths 182 | withRowAnimation:UITableViewRowAnimationFade]; 183 | } else if ([sender.accessibilityIdentifier isEqualToString:@"AdBlockCustomProxySwitchCell"]) { 184 | [tweakDefaults setBool:sender.isOn forKey:@"TWAdBlockCustomProxyEnabled"]; 185 | self.customProxyEnabled = sender.isOn; 186 | 187 | NSArray *indexPaths = @[ [NSIndexPath indexPathForRow:2 inSection:1] ]; 188 | if (sender.isOn) 189 | [self.tableView insertRowsAtIndexPaths:indexPaths 190 | withRowAnimation:UITableViewRowAnimationFade]; 191 | else 192 | [self.tableView deleteRowsAtIndexPaths:indexPaths 193 | withRowAnimation:UITableViewRowAnimationFade]; 194 | } 195 | 196 | [tweakDefaults synchronize]; 197 | } 198 | %new 199 | - (void)textFieldDidEndEditing:(UITextField *)textField { 200 | [tweakDefaults setValue:textField.text forKey:@"TWAdBlockProxy"]; 201 | } 202 | %end 203 | -------------------------------------------------------------------------------- /Tweak.x: -------------------------------------------------------------------------------- 1 | #import 2 | #import "Tweak.h" 3 | 4 | NSBundle *tweakBundle; 5 | NSUserDefaults *tweakDefaults; 6 | TWAdBlockAssetResourceLoaderDelegate *assetResourceLoaderDelegate; 7 | 8 | // Server-side video ad blocking 9 | 10 | %hook NSURLSession 11 | - (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request { 12 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 13 | if (![request isKindOfClass:NSMutableURLRequest.class]) request = request.mutableCopy; 14 | ((NSMutableURLRequest *)request).HTTPBody = [request.HTTPBody twab_requestDataForRequest:request]; 15 | if (![tweakDefaults boolForKey:@"TWAdBlockProxyEnabled"]) return %orig; 16 | NSString *proxy = [tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 17 | ? [tweakDefaults stringForKey:@"TWAdBlockProxy"] 18 | : PROXY_ADDR; 19 | if (![request.URL.host isEqualToString:@"usher.ttvnw.net"]) return %orig; 20 | NSURL *proxyURL = [NSURL URLWithString:proxy]; 21 | if ([proxyURL.scheme hasPrefix:@"http"]) 22 | ((NSMutableURLRequest *)request).URL = [request.URL twab_URLWithProxyURL:proxyURL]; 23 | else 24 | return &%orig([self twab_proxySessionWithAddress:proxy], _cmd, request); 25 | return %orig; 26 | } 27 | - (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request 28 | fromData:(NSData *)bodyData { 29 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 30 | if (![request isKindOfClass:NSMutableURLRequest.class]) request = request.mutableCopy; 31 | bodyData = [bodyData twab_requestDataForRequest:request]; 32 | if (![tweakDefaults boolForKey:@"TWAdBlockProxyEnabled"]) return %orig; 33 | NSString *proxy = [tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 34 | ? [tweakDefaults stringForKey:@"TWAdBlockProxy"] 35 | : PROXY_ADDR; 36 | if (![request.URL.host isEqualToString:@"usher.ttvnw.net"]) return %orig; 37 | NSURL *proxyURL = [NSURL URLWithString:proxy]; 38 | if ([proxyURL.scheme hasPrefix:@"http"]) 39 | ((NSMutableURLRequest *)request).URL = [request.URL twab_URLWithProxyURL:proxyURL]; 40 | else 41 | return &%orig([self twab_proxySessionWithAddress:proxy], _cmd, request, bodyData); 42 | return %orig; 43 | } 44 | %end 45 | 46 | %hook AVURLAsset 47 | - (instancetype)initWithURL:(NSURL *)URL options:(NSDictionary *)options { 48 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"] || 49 | ![tweakDefaults boolForKey:@"TWAdBlockProxyEnabled"] || 50 | ![URL.scheme isEqualToString:@"https"] || ![URL.host isEqualToString:@"usher.ttvnw.net"]) 51 | return %orig; 52 | NSURL *proxyURL = [NSURL URLWithString:[tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 53 | ? [tweakDefaults stringForKey:@"TWAdBlockProxy"] 54 | : PROXY_ADDR]; 55 | if ([proxyURL.scheme hasPrefix:@"http"]) 56 | return %orig([URL twab_URLWithProxyURL:proxyURL], options); 57 | NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:YES]; 58 | components.scheme = @"twab"; 59 | URL = components.URL; 60 | if ((self = %orig)) { 61 | [self.resourceLoader setDelegate:assetResourceLoaderDelegate 62 | queue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)]; 63 | } 64 | return self; 65 | } 66 | %end 67 | 68 | %hook _TtC6Twitch27AssetResourceLoaderDelegate 69 | %new 70 | - (BOOL)handleLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { 71 | NSURL *URL = loadingRequest.request.URL; 72 | if (![URL.scheme isEqualToString:@"twab"]) return NO; 73 | AVAssetResourceLoadingDataRequest *dataRequest = loadingRequest.dataRequest; 74 | NSURLComponents *components = [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:YES]; 75 | components.scheme = @"https"; 76 | NSMutableURLRequest *request = loadingRequest.request.mutableCopy; 77 | request.URL = components.URL; 78 | NSString *proxy = [tweakDefaults boolForKey:@"TWAdBlockCustomProxyEnabled"] 79 | ? [tweakDefaults stringForKey:@"TWAdBlockProxy"] 80 | : PROXY_ADDR; 81 | NSURLSession *session = [[NSURLSession alloc] twab_proxySessionWithAddress:proxy]; 82 | [[session dataTaskWithRequest:request 83 | completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 84 | if (error) return [loadingRequest finishLoadingWithError:error]; 85 | loadingRequest.contentInformationRequest.contentType = AVFileTypeMPEG4; 86 | [dataRequest respondWithData:data]; 87 | [loadingRequest finishLoading]; 88 | }] resume]; 89 | return YES; 90 | } 91 | - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 92 | shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { 93 | return ![self handleLoadingRequest:loadingRequest] ? %orig : YES; 94 | } 95 | - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 96 | shouldWaitForRenewalOfRequestedResource:(AVAssetResourceRenewalRequest *)renewalRequest { 97 | return ![self handleLoadingRequest:renewalRequest] ? %orig : YES; 98 | } 99 | %end 100 | 101 | %hook AVPlayer 102 | - (instancetype)init { 103 | if ((self = %orig)) { 104 | [self addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:NULL]; 105 | } 106 | return self; 107 | } 108 | %new 109 | - (void)observeValueForKeyPath:(NSString *)keyPath 110 | ofObject:(id)object 111 | change:(NSDictionary *)change 112 | context:(void *)context { 113 | if ([keyPath isEqualToString:@"status"] && 114 | [change[NSKeyValueChangeNewKey] integerValue] == AVPlayerStatusReadyToPlay) 115 | [self play]; 116 | } 117 | %end 118 | 119 | // Client-side video ad blocking 120 | 121 | static void removeAdControllers(void *ptr) { 122 | if (((uintptr_t)ptr & 0xFFFF800000000000) != 0) return; 123 | id obj = (__bridge id)ptr; 124 | Ivar theaterAdControllerIvar = 125 | class_getInstanceVariable(object_getClass(obj), "theaterAdController"); 126 | if (!theaterAdControllerIvar) return; 127 | id theaterAdController = object_getIvar(obj, theaterAdControllerIvar); 128 | const char *ivars[] = {"displayAdController", "streamDisplayAdStateManager", "vastAdController"}; 129 | for (int i = 0; i < sizeof(ivars) / sizeof(ivars[0]); i++) { 130 | Ivar adControllerIvar = 131 | class_getInstanceVariable(object_getClass(theaterAdController), ivars[i]); 132 | if (adControllerIvar) object_setIvar(theaterAdController, adControllerIvar, nil); 133 | } 134 | } 135 | 136 | static void *(*orig_swift_unknownObjectWeakAssign)(void *, void *); 137 | static void *hook_swift_unknownObjectWeakAssign(void *ref, void *value) { 138 | void *result = orig_swift_unknownObjectWeakAssign(ref, value); 139 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return result; 140 | removeAdControllers(value); 141 | return result; 142 | } 143 | 144 | static void *(*orig_swift_unknownObjectWeakLoadStrong)(void *); 145 | static void *hook_swift_unknownObjectWeakLoadStrong(void *ref) { 146 | void *result = orig_swift_unknownObjectWeakLoadStrong(ref); 147 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return result; 148 | removeAdControllers(result); 149 | return result; 150 | } 151 | 152 | // Block ads in feed tab 153 | 154 | %hook _TtC9TwitchKit18TKURLSessionClient 155 | - (void)URLSession:(NSURLSession *)session 156 | dataTask:(NSURLSessionDataTask *)dataTask 157 | didReceiveData:(NSData *)data { 158 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 159 | %orig(session, dataTask, [data twab_responseDataForRequest:dataTask.currentRequest]); 160 | } 161 | %end 162 | 163 | // Block ads in following tab 164 | 165 | %hook _TtC6Twitch23FollowingViewController 166 | - (instancetype)initWithGraphQL:(_TtC9TwitchKit9TKGraphQL *)graphQL 167 | themeManager:(_TtC12TwitchCoreUI21TWDefaultThemeManager *)themeManager { 168 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 169 | if ((self = %orig)) { 170 | Ivar headlinerManagerIvar = 171 | class_getInstanceVariable(object_getClass(self), "headlinerManager"); 172 | if (headlinerManagerIvar) { 173 | Ivar displayAdStateManagerIvar = 174 | class_getInstanceVariable(object_getClass(self), "displayAdStateManager"); 175 | if (displayAdStateManagerIvar) object_setIvar(self, displayAdStateManagerIvar, nil); 176 | } 177 | } 178 | return self; 179 | } 180 | - (instancetype)initWithGraphQL:(_TtC9TwitchKit9TKGraphQL *)graphQL 181 | themeManager:(_TtC12TwitchCoreUI21TWDefaultThemeManager *)themeManager 182 | urlController:(_TtC6Twitch13URLController *)urlController { 183 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 184 | if ((self = %orig)) { 185 | Ivar headlinerManagerIvar = 186 | class_getInstanceVariable(object_getClass(self), "headlinerManager"); 187 | if (headlinerManagerIvar) { 188 | Ivar displayAdStateManagerIvar = 189 | class_getInstanceVariable(object_getClass(self), "displayAdStateManager"); 190 | if (displayAdStateManagerIvar) object_setIvar(self, displayAdStateManagerIvar, nil); 191 | } 192 | } 193 | return self; 194 | } 195 | %end 196 | 197 | %hook _TtC6Twitch27HeadlinerFollowingAdManager 198 | + (instancetype)shared { 199 | if (![tweakDefaults boolForKey:@"TWAdBlockEnabled"]) return %orig; 200 | _TtC6Twitch27HeadlinerFollowingAdManager *shared = %orig; 201 | if (shared) { 202 | Ivar displayAdStateManagerIvar = 203 | class_getInstanceVariable(object_getClass(shared), "displayAdStateManager"); 204 | if (displayAdStateManagerIvar) object_setIvar(shared, displayAdStateManagerIvar, nil); 205 | } 206 | return shared; 207 | } 208 | %end 209 | 210 | // Block update prompt 211 | 212 | %hook TWAppUpdatePrompt 213 | + (void)startMonitoringSavantSettingsToShowPromptIfNeeded { 214 | } 215 | %end 216 | 217 | %ctor { 218 | rebind_symbols( 219 | (struct rebinding[]){ 220 | {"swift_unknownObjectWeakAssign", (void *)hook_swift_unknownObjectWeakAssign, 221 | (void **)&orig_swift_unknownObjectWeakAssign}, 222 | {"swift_unknownObjectWeakLoadStrong", (void *)hook_swift_unknownObjectWeakLoadStrong, 223 | (void **)&orig_swift_unknownObjectWeakLoadStrong}, 224 | }, 225 | 2); 226 | tweakBundle = [NSBundle bundleWithPath:[NSBundle.mainBundle pathForResource:@"TwitchAdBlock" 227 | ofType:@"bundle"]]; 228 | if (!tweakBundle) 229 | tweakBundle = [NSBundle 230 | bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/TwitchAdBlock.bundle")]; 231 | tweakDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"com.level3tjg.twitchadblock"]; 232 | if (![tweakDefaults objectForKey:@"TWAdBlockEnabled"]) 233 | [tweakDefaults setBool:YES forKey:@"TWAdBlockEnabled"]; 234 | if (![tweakDefaults objectForKey:@"TWAdBlockProxyEnabled"]) 235 | [tweakDefaults setBool:NO forKey:@"TWAdBlockProxyEnabled"]; 236 | if (![tweakDefaults objectForKey:@"TWAdBlockCustomProxyEnabled"]) 237 | [tweakDefaults setBool:NO forKey:@"TWAdBlockCustomProxyEnabled"]; 238 | assetResourceLoaderDelegate = [[TWAdBlockAssetResourceLoaderDelegate alloc] init]; 239 | } 240 | --------------------------------------------------------------------------------