import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:dio_http_cache_lts/dio_http_cache_lts.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:injectable/injectable.dart'; import 'package:native_dio_client/native_dio_client.dart'; import 'package:revanced_manager/models/patch.dart'; import 'package:revanced_manager/utils/check_for_gms.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_dio/sentry_dio.dart'; @lazySingleton class GithubAPI { late Dio _dio = Dio(); final DioCacheManager _dioCacheManager = DioCacheManager(CacheConfig()); final Options _cacheOptions = buildCacheOptions( const Duration(hours: 6), maxStale: const Duration(days: 1), ); final Map repoAppPath = { 'com.google.android.youtube': 'youtube', 'com.google.android.apps.youtube.music': 'music', 'com.twitter.android': 'twitter', 'com.reddit.frontpage': 'reddit', 'com.zhiliaoapp.musically': 'tiktok', 'de.dwd.warnapp': 'warnwetter', 'com.garzotto.pflotsh.ecmwf_a': 'ecmwf', 'com.spotify.music': 'spotify', }; void initialize() async { try { bool isGMSInstalled = await checkForGMS(); if (!isGMSInstalled) { _dio = Dio(BaseOptions( baseUrl: 'https://api.github.com', )); print('GitHub API: Using default engine + $isGMSInstalled'); } else { _dio = Dio(BaseOptions( baseUrl: 'https://api.github.com', )) ..httpClientAdapter = NativeAdapter(); print('ReVanced API: Using CronetEngine + $isGMSInstalled'); } _dio.interceptors.add(_dioCacheManager.interceptor); _dio.addSentry( captureFailedRequests: true, ); } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); } } Future clearAllCache() async { try { await _dioCacheManager.clearAll(); } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); } } Future?> _getLatestRelease(String repoName) async { try { var response = await _dio.get( '/repos/$repoName/releases/latest', options: _cacheOptions, ); return response.data; } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); return null; } } Future> getCommits( String packageName, String repoName, DateTime since, ) async { String path = 'src/main/kotlin/app/revanced/patches/${repoAppPath[packageName]}'; try { var response = await _dio.get( '/repos/$repoName/commits', queryParameters: { 'path': path, 'since': since.toIso8601String(), }, options: _cacheOptions, ); List commits = response.data; return commits .map( (commit) => (commit['commit']['message']).split('\n')[0] + ' - ' + commit['commit']['author']['name'] + '\n' as String, ) .toList(); } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); return List.empty(); } } Future getLatestReleaseFile(String extension, String repoName) async { try { Map? release = await _getLatestRelease(repoName); if (release != null) { Map? asset = (release['assets'] as List).firstWhereOrNull( (asset) => (asset['name'] as String).endsWith(extension), ); if (asset != null) { return await DefaultCacheManager().getSingleFile( asset['browser_download_url'], ); } } } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); return null; } return null; } Future> getPatches(String repoName) async { List patches = []; try { File? f = await getLatestReleaseFile('.json', repoName); if (f != null) { List list = jsonDecode(f.readAsStringSync()); patches = list.map((patch) => Patch.fromJson(patch)).toList(); } } on Exception catch (e, s) { await Sentry.captureException(e, stackTrace: s); return List.empty(); } return patches; } }