diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/json/BaseJsonHook.kt b/app/src/main/java/app/revanced/twitter/patches/hook/json/BaseJsonHook.kt new file mode 100644 index 00000000..96838370 --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/json/BaseJsonHook.kt @@ -0,0 +1,9 @@ +package app.revanced.twitter.patches.hook.json + +import org.json.JSONObject + +abstract class BaseJsonHook : JsonHook { + abstract fun apply(json: JSONObject) + + override fun transform(json: JSONObject) = json.apply { apply(json) } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHook.kt b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHook.kt new file mode 100644 index 00000000..e5c2f0ef --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHook.kt @@ -0,0 +1,15 @@ +package app.revanced.twitter.patches.hook.json + +import app.revanced.twitter.patches.hook.patch.Hook +import org.json.JSONObject + +interface JsonHook : Hook { + /** + * Transform a JSONObject. + * + * @param json The JSONObject. + */ + fun transform(json: JSONObject): JSONObject + + override fun hook(type: JSONObject) = transform(type) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt new file mode 100644 index 00000000..9817b6aa --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/json/JsonHookPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.twitter.patches.hook.json + +import app.revanced.twitter.utils.json.JsonUtils.parseJson +import app.revanced.twitter.utils.stream.StreamUtils +import org.json.JSONException +import java.io.IOException +import java.io.InputStream + +object JsonHookPatch { + private val hooks = buildList { + // Modified by corresponding patch. + } + + @JvmStatic + fun parseJsonHook(jsonInputStream: InputStream): InputStream { + var jsonObject = try { + parseJson(jsonInputStream) + } catch (ignored: IOException) { + return jsonInputStream // Unreachable. + } catch (ignored: JSONException) { + return jsonInputStream + } + + for (hook in hooks) jsonObject = hook.hook(jsonObject) + + return StreamUtils.fromString(jsonObject.toString()) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/patch/Hook.kt b/app/src/main/java/app/revanced/twitter/patches/hook/patch/Hook.kt new file mode 100644 index 00000000..3cf57c2f --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/patch/Hook.kt @@ -0,0 +1,11 @@ +package app.revanced.twitter.patches.hook.patch + +import androidx.annotation.NonNull + +interface Hook { + /** + * Hook the given type. + * @param type The type to hook + */ + fun hook(@NonNull type: T): T +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/patch/ads/AdsHook.kt b/app/src/main/java/app/revanced/twitter/patches/hook/patch/ads/AdsHook.kt new file mode 100644 index 00000000..cf84a52b --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/patch/ads/AdsHook.kt @@ -0,0 +1,15 @@ +package app.revanced.twitter.patches.hook.patch.ads + +import app.revanced.twitter.patches.hook.json.BaseJsonHook +import app.revanced.twitter.patches.hook.twifucker.TwiFucker +import org.json.JSONObject + + +object AdsHook : BaseJsonHook() { + /** + * Strips JSONObject from promoted ads. + * + * @param json The JSONObject. + */ + override fun apply(json: JSONObject) = TwiFucker.hidePromotedAds(json) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt new file mode 100644 index 00000000..c79254e2 --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFucker.kt @@ -0,0 +1,177 @@ +package app.revanced.twitter.patches.hook.twifucker + +import android.util.Log +import app.revanced.twitter.patches.hook.twifucker.TwiFuckerUtils.forEach +import app.revanced.twitter.patches.hook.twifucker.TwiFuckerUtils.forEachIndexed +import org.json.JSONArray +import org.json.JSONObject + +// https://raw.githubusercontent.com/Dr-TSNG/TwiFucker/880cdf1c1622e54ab45561ffcb4f53d94ed97bae/app/src/main/java/icu/nullptr/twifucker/hook/JsonHook.kt +internal object TwiFucker { + // root + private fun JSONObject.jsonGetInstructions(): JSONArray? = + optJSONObject("timeline")?.optJSONArray("instructions") + + private fun JSONObject.jsonGetData(): JSONObject? = optJSONObject("data") + + private fun JSONObject.jsonHasRecommendedUsers(): Boolean = has("recommended_users") + + private fun JSONObject.jsonRemoveRecommendedUsers() { + remove("recommended_users") + } + + private fun JSONObject.jsonCheckAndRemoveRecommendedUsers() { + if (jsonHasRecommendedUsers()) { + Log.d("revanced", "Handle recommended users: $this") + jsonRemoveRecommendedUsers() + } + } + + private fun JSONObject.jsonHasThreads(): Boolean = has("threads") + + private fun JSONObject.jsonRemoveThreads() { + remove("threads") + } + + private fun JSONObject.jsonCheckAndRemoveThreads() { + if (jsonHasThreads()) { + Log.d("revabced", "Handle threads: $this") + jsonRemoveThreads() + } + } + + // data + private fun JSONObject.dataGetInstructions(): JSONArray? { + val timeline = optJSONObject("user_result")?.optJSONObject("result") + ?.optJSONObject("timeline_response")?.optJSONObject("timeline") + ?: optJSONObject("timeline_response")?.optJSONObject("timeline") + ?: optJSONObject("timeline_response") + return timeline?.optJSONArray("instructions") + } + + private fun JSONObject.dataCheckAndRemove() { + dataGetInstructions()?.forEach { instruction -> + instruction.instructionCheckAndRemove() + } + } + + private fun JSONObject.dataGetLegacy(): JSONObject? = + optJSONObject("tweet_result")?.optJSONObject("result")?.let { + if (it.has("tweet")) { + it.optJSONObject("tweet") + } else { + it + } + }?.optJSONObject("legacy") + + + // entry + private fun JSONObject.entryHasPromotedMetadata(): Boolean = + optJSONObject("content")?.optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("tweet") + ?.has("promotedMetadata") == true || optJSONObject("content")?.optJSONObject("content") + ?.has("tweetPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.has("tweetPromotedMetadata") == true + + private fun JSONObject.entryGetContentItems(): JSONArray? = + optJSONObject("content")?.optJSONArray("items") + ?: optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items") + + private fun JSONObject.entryIsTweetDetailRelatedTweets(): Boolean = + optString("entryId").startsWith("tweetdetailrelatedtweets-") + + private fun JSONObject.entryGetTrends(): JSONArray? = + optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items") + + // trend + private fun JSONObject.trendHasPromotedMetadata(): Boolean = + optJSONObject("item")?.optJSONObject("content")?.optJSONObject("trend") + ?.has("promotedMetadata") == true + + private fun JSONArray.trendRemoveAds() { + val trendRemoveIndex = mutableListOf() + forEachIndexed { trendIndex, trend -> + if (trend.trendHasPromotedMetadata()) { + Log.d("revanced", "Handle trends ads $trendIndex $trend") + trendRemoveIndex.add(trendIndex) + } + } + for (i in trendRemoveIndex.asReversed()) { + remove(i) + } + } + + // instruction + private fun JSONObject.instructionTimelineAddEntries(): JSONArray? = optJSONArray("entries") + + private fun JSONObject.instructionGetAddEntries(): JSONArray? = + optJSONObject("addEntries")?.optJSONArray("entries") + + private fun JSONObject.instructionCheckAndRemove() { + instructionTimelineAddEntries()?.entriesRemoveAnnoyance() + instructionGetAddEntries()?.entriesRemoveAnnoyance() + } + + // entries + private fun JSONArray.entriesRemoveTimelineAds() { + val removeIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + entry.entryGetTrends()?.trendRemoveAds() + + if (entry.entryHasPromotedMetadata()) { + Log.d("revanced", "Handle timeline ads $entryIndex $entry") + removeIndex.add(entryIndex) + } + + val innerRemoveIndex = mutableListOf() + val contentItems = entry.entryGetContentItems() + contentItems?.forEachIndexed inner@{ itemIndex, item -> + if (item.entryHasPromotedMetadata()) { + Log.d("revanced", "Handle timeline replies ads $entryIndex $entry") + if (contentItems.length() == 1) { + removeIndex.add(entryIndex) + } else { + innerRemoveIndex.add(itemIndex) + } + return@inner + } + } + for (i in innerRemoveIndex.asReversed()) { + contentItems?.remove(i) + } + } + for (i in removeIndex.reversed()) { + remove(i) + } + } + + private fun JSONArray.entriesRemoveTweetDetailRelatedTweets() { + val removeIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + + if (entry.entryIsTweetDetailRelatedTweets()) { + Log.d("revanced", "Handle tweet detail related tweets $entryIndex $entry") + removeIndex.add(entryIndex) + } + } + for (i in removeIndex.reversed()) { + remove(i) + } + } + + private fun JSONArray.entriesRemoveAnnoyance() { + entriesRemoveTimelineAds() + entriesRemoveTweetDetailRelatedTweets() + } + + fun hideRecommendedUsers(json: JSONObject) { + json.jsonCheckAndRemoveRecommendedUsers() + } + + fun hidePromotedAds(json: JSONObject) { + json.jsonGetInstructions()?.forEach { instruction -> + instruction.instructionCheckAndRemove() + } + json.jsonGetData()?.dataCheckAndRemove() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFuckerUtils.kt b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFuckerUtils.kt new file mode 100644 index 00000000..33e44cb4 --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/patches/hook/twifucker/TwiFuckerUtils.kt @@ -0,0 +1,22 @@ +package app.revanced.twitter.patches.hook.twifucker + +import org.json.JSONArray +import org.json.JSONObject + +internal object TwiFuckerUtils { + inline fun JSONArray.forEach(action: (JSONObject) -> Unit) { + (0 until this.length()).forEach { i -> + if (this[i] is JSONObject) { + action(this[i] as JSONObject) + } + } + } + + inline fun JSONArray.forEachIndexed(action: (index: Int, JSONObject) -> Unit) { + (0 until this.length()).forEach { i -> + if (this[i] is JSONObject) { + action(i, this[i] as JSONObject) + } + } + } +} diff --git a/app/src/main/java/app/revanced/twitter/utils/json/JsonUtils.kt b/app/src/main/java/app/revanced/twitter/utils/json/JsonUtils.kt new file mode 100644 index 00000000..e0102a4e --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/utils/json/JsonUtils.kt @@ -0,0 +1,13 @@ +package app.revanced.twitter.utils.json + +import app.revanced.twitter.utils.stream.StreamUtils +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.InputStream + +object JsonUtils { + @JvmStatic + @Throws(IOException::class, JSONException::class) + fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream)) +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/twitter/utils/stream/StreamUtils.kt b/app/src/main/java/app/revanced/twitter/utils/stream/StreamUtils.kt new file mode 100644 index 00000000..ccbcc823 --- /dev/null +++ b/app/src/main/java/app/revanced/twitter/utils/stream/StreamUtils.kt @@ -0,0 +1,24 @@ +package app.revanced.twitter.utils.stream + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +object StreamUtils { + @Throws(IOException::class) + fun toString(inputStream: InputStream): String { + ByteArrayOutputStream().use { result -> + val buffer = ByteArray(1024) + var length: Int + while (inputStream.read(buffer).also { length = it } != -1) { + result.write(buffer, 0, length) + } + return result.toString() + } + } + + fun fromString(string: String): InputStream { + return ByteArrayInputStream(string.toByteArray()) + } +} \ No newline at end of file