mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-05-28 20:40:13 +02:00
feat: remote media resolver
This commit is contained in:
parent
cb55923fd8
commit
d3d8e22957
@ -1,5 +1,3 @@
|
|||||||
import groovy.json.JsonSlurper
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
@ -102,4 +100,5 @@ dependencies {
|
|||||||
implementation 'com.google.code.gson:gson:2.10.1'
|
implementation 'com.google.code.gson:gson:2.10.1'
|
||||||
implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS'
|
implementation 'com.arthenica:ffmpeg-kit-full-gpl:5.1.LTS'
|
||||||
implementation 'org.osmdroid:osmdroid-android:6.1.16'
|
implementation 'org.osmdroid:osmdroid-android:6.1.16'
|
||||||
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ object Constants {
|
|||||||
val MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(3, 3, 5, 1, 1)
|
val MESSAGE_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(3, 3, 5, 1, 1)
|
||||||
val ARROYO_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 3, 3, 5, 1, 1)
|
val ARROYO_EXTERNAL_MEDIA_ENCRYPTION_PROTO_PATH = intArrayOf(4, 4, 3, 3, 5, 1, 1)
|
||||||
val ARROYO_STRING_CHAT_MESSAGE_PROTO = intArrayOf(4, 4, 2, 1)
|
val ARROYO_STRING_CHAT_MESSAGE_PROTO = intArrayOf(4, 4, 2, 1)
|
||||||
val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3, 2, 2)
|
val ARROYO_URL_KEY_PROTO_PATH = intArrayOf(4, 5, 1, 3)
|
||||||
|
|
||||||
const val ARROYO_ENCRYPTION_PROTO_INDEX = 19
|
const val ARROYO_ENCRYPTION_PROTO_INDEX = 19
|
||||||
const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4
|
const val ARROYO_ENCRYPTION_PROTO_INDEX_V2 = 4
|
||||||
|
@ -29,14 +29,13 @@ import me.rhunk.snapenhance.util.EncryptionUtils
|
|||||||
import me.rhunk.snapenhance.util.MediaDownloaderHelper
|
import me.rhunk.snapenhance.util.MediaDownloaderHelper
|
||||||
import me.rhunk.snapenhance.util.MediaType
|
import me.rhunk.snapenhance.util.MediaType
|
||||||
import me.rhunk.snapenhance.util.PreviewUtils
|
import me.rhunk.snapenhance.util.PreviewUtils
|
||||||
import me.rhunk.snapenhance.util.download.CdnDownloader
|
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
|
||||||
import me.rhunk.snapenhance.util.getObjectField
|
import me.rhunk.snapenhance.util.getObjectField
|
||||||
import me.rhunk.snapenhance.util.protobuf.ProtoReader
|
import me.rhunk.snapenhance.util.protobuf.ProtoReader
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.lang.StringBuilder
|
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
@ -308,7 +307,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
for (i in 0 until baseUrlNodeList.length) {
|
for (i in 0 until baseUrlNodeList.length) {
|
||||||
val baseUrlNode = baseUrlNodeList.item(i)
|
val baseUrlNode = baseUrlNodeList.item(i)
|
||||||
val baseUrl = baseUrlNode.textContent
|
val baseUrl = baseUrlNode.textContent
|
||||||
baseUrlNode.textContent = "${CdnDownloader.CF_ST_CDN_D}$baseUrl"
|
baseUrlNode.textContent = "${RemoteMediaResolver.CF_ST_CDN_D}$baseUrl"
|
||||||
}
|
}
|
||||||
|
|
||||||
val xmlData = ByteArrayOutputStream()
|
val xmlData = ByteArrayOutputStream()
|
||||||
@ -405,14 +404,13 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
val messageReader = ProtoReader(message.message_content!!)
|
val messageReader = ProtoReader(message.message_content!!)
|
||||||
val urlKey: String = messageReader.getString(*ARROYO_URL_KEY_PROTO_PATH)!!
|
val urlProto: ByteArray = messageReader.getByteArray(*ARROYO_URL_KEY_PROTO_PATH)!!
|
||||||
|
|
||||||
//download the message content
|
//download the message content
|
||||||
try {
|
try {
|
||||||
context.shortToast("Querying $urlKey")
|
val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(urlProto, canMergeOverlay(), isPreviewMode) {
|
||||||
val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, canMergeOverlay(), isPreviewMode) {
|
|
||||||
EncryptionUtils.decryptInputStreamFromArroyo(it, contentType, messageReader)
|
EncryptionUtils.decryptInputStreamFromArroyo(it, contentType, messageReader)
|
||||||
}[MediaType.ORIGINAL] ?: throw Exception("Failed to download media for key $urlKey")
|
}[MediaType.ORIGINAL] ?: throw Exception("Failed to download media")
|
||||||
val fileType = FileType.fromByteArray(downloadedMedia)
|
val fileType = FileType.fromByteArray(downloadedMedia)
|
||||||
|
|
||||||
if (isPreviewMode) {
|
if (isPreviewMode) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package me.rhunk.snapenhance.features.impl.extras
|
package me.rhunk.snapenhance.features.impl.extras
|
||||||
|
|
||||||
import me.rhunk.snapenhance.Logger
|
|
||||||
import me.rhunk.snapenhance.config.ConfigProperty
|
import me.rhunk.snapenhance.config.ConfigProperty
|
||||||
import me.rhunk.snapenhance.features.Feature
|
import me.rhunk.snapenhance.features.Feature
|
||||||
import me.rhunk.snapenhance.features.FeatureLoadParams
|
import me.rhunk.snapenhance.features.FeatureLoadParams
|
||||||
|
@ -189,9 +189,8 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
.flatten()
|
.flatten()
|
||||||
|
|
||||||
mediaReferences.forEach { media ->
|
mediaReferences.forEach { media ->
|
||||||
val mediaContent = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray()
|
val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray()
|
||||||
val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString)
|
val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString)
|
||||||
val urlKey = ProtoReader(mediaContent).getString(2, 2) ?: return@forEach
|
|
||||||
runCatching {
|
runCatching {
|
||||||
//download the media
|
//download the media
|
||||||
val mediaInfo = ProtoReader(contentData).let {
|
val mediaInfo = ProtoReader(contentData).let {
|
||||||
@ -201,11 +200,11 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH)
|
return@let it.readPath(*Constants.MESSAGE_SNAP_ENCRYPTION_PROTO_PATH)
|
||||||
}?: return@runCatching
|
}?: return@runCatching
|
||||||
|
|
||||||
val downloadedMedia = MediaDownloaderHelper.downloadMediaFromKey(urlKey, mergeOverlay = false, isPreviewMode = false) {
|
val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference, mergeOverlay = false, isPreviewMode = false) {
|
||||||
if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX))
|
if (mediaInfo.exists(Constants.ARROYO_ENCRYPTION_PROTO_INDEX))
|
||||||
EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX)
|
EncryptionUtils.decryptInputStream(it, false, mediaInfo, Constants.ARROYO_ENCRYPTION_PROTO_INDEX)
|
||||||
else it
|
else it
|
||||||
}[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media from key $urlKey")
|
}[MediaType.ORIGINAL] ?: throw Throwable("Failed to download media")
|
||||||
|
|
||||||
val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!!
|
val bitmapPreview = PreviewUtils.createPreview(downloadedMedia, mediaType == MediaReferenceType.VIDEO)!!
|
||||||
val notificationBuilder = XposedHelpers.newInstance(
|
val notificationBuilder = XposedHelpers.newInstance(
|
||||||
@ -220,7 +219,6 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
|
|||||||
return@onEach
|
return@onEach
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Logger.xposedLog("Failed to send preview notification", it)
|
Logger.xposedLog("Failed to send preview notification", it)
|
||||||
Logger.xposedLog("urlKey: $urlKey")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package me.rhunk.snapenhance.features.impl.ui.menus
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Switch
|
import android.widget.Switch
|
||||||
|
@ -5,12 +5,12 @@ import me.rhunk.snapenhance.ModContext
|
|||||||
import me.rhunk.snapenhance.features.Feature
|
import me.rhunk.snapenhance.features.Feature
|
||||||
import me.rhunk.snapenhance.features.FeatureLoadParams
|
import me.rhunk.snapenhance.features.FeatureLoadParams
|
||||||
import me.rhunk.snapenhance.features.impl.ConfigEnumKeys
|
import me.rhunk.snapenhance.features.impl.ConfigEnumKeys
|
||||||
import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass
|
|
||||||
import me.rhunk.snapenhance.features.impl.Messaging
|
import me.rhunk.snapenhance.features.impl.Messaging
|
||||||
import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload
|
import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload
|
||||||
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
|
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
|
||||||
import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave
|
|
||||||
import me.rhunk.snapenhance.features.impl.experiments.AppPasscode
|
import me.rhunk.snapenhance.features.impl.experiments.AppPasscode
|
||||||
|
import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass
|
||||||
|
import me.rhunk.snapenhance.features.impl.extras.AntiAutoSave
|
||||||
import me.rhunk.snapenhance.features.impl.extras.AutoSave
|
import me.rhunk.snapenhance.features.impl.extras.AutoSave
|
||||||
import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction
|
import me.rhunk.snapenhance.features.impl.extras.DisableVideoLengthRestriction
|
||||||
import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride
|
import me.rhunk.snapenhance.features.impl.extras.GalleryMediaSendOverride
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package me.rhunk.snapenhance.mapping.impl
|
package me.rhunk.snapenhance.mapping.impl
|
||||||
|
|
||||||
import me.rhunk.snapenhance.Logger
|
|
||||||
import me.rhunk.snapenhance.Logger.debug
|
import me.rhunk.snapenhance.Logger.debug
|
||||||
import me.rhunk.snapenhance.mapping.Mapper
|
import me.rhunk.snapenhance.mapping.Mapper
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
@ -3,7 +3,7 @@ package me.rhunk.snapenhance.util
|
|||||||
import com.arthenica.ffmpegkit.FFmpegKit
|
import com.arthenica.ffmpegkit.FFmpegKit
|
||||||
import me.rhunk.snapenhance.Logger
|
import me.rhunk.snapenhance.Logger
|
||||||
import me.rhunk.snapenhance.data.FileType
|
import me.rhunk.snapenhance.data.FileType
|
||||||
import me.rhunk.snapenhance.util.download.CdnDownloader
|
import me.rhunk.snapenhance.util.download.RemoteMediaResolver
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
@ -16,9 +16,9 @@ enum class MediaType {
|
|||||||
ORIGINAL, OVERLAY
|
ORIGINAL, OVERLAY
|
||||||
}
|
}
|
||||||
object MediaDownloaderHelper {
|
object MediaDownloaderHelper {
|
||||||
fun downloadMediaFromKey(key: String, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> {
|
fun downloadMediaFromReference(mediaReference: ByteArray, mergeOverlay: Boolean, isPreviewMode: Boolean, decryptionCallback: (InputStream) -> InputStream): Map<MediaType, ByteArray> {
|
||||||
val inputStream: InputStream = CdnDownloader.downloadWithDefaultEndpoints(key) ?: throw FileNotFoundException("Unable to get $key from cdn list. Check the logs for more info")
|
val inputStream: InputStream = RemoteMediaResolver.downloadBoltMedia(mediaReference) ?: throw FileNotFoundException("Unable to get media key. Check the logs for more info")
|
||||||
val content = decryptionCallback(inputStream).readBytes().also { inputStream.close() }
|
val content = decryptionCallback(inputStream).readBytes()
|
||||||
val fileType = FileType.fromByteArray(content)
|
val fileType = FileType.fromByteArray(content)
|
||||||
val isZipFile = fileType == FileType.ZIP
|
val isZipFile = fileType == FileType.ZIP
|
||||||
|
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
package me.rhunk.snapenhance.util.download
|
|
||||||
|
|
||||||
import me.rhunk.snapenhance.Constants
|
|
||||||
import me.rhunk.snapenhance.util.protobuf.ProtoWriter
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.net.URL
|
|
||||||
import java.util.Base64
|
|
||||||
import javax.net.ssl.HttpsURLConnection
|
|
||||||
|
|
||||||
object CdnDownloader {
|
|
||||||
private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http"
|
|
||||||
const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/"
|
|
||||||
|
|
||||||
private fun queryRemoteContent(url: String): InputStream? {
|
|
||||||
try {
|
|
||||||
val connection = URL(url).openConnection() as HttpsURLConnection
|
|
||||||
connection.requestMethod = "GET"
|
|
||||||
connection.instanceFollowRedirects = true
|
|
||||||
connection.setRequestProperty("User-Agent", Constants.USER_AGENT)
|
|
||||||
return connection.inputStream
|
|
||||||
} catch (ignored: Throwable) {
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadWithDefaultEndpoints(key: String): InputStream? {
|
|
||||||
val payload = ProtoWriter().apply {
|
|
||||||
write(2) {
|
|
||||||
writeString(2, key)
|
|
||||||
writeBuffer(3, byteArrayOf())
|
|
||||||
writeBuffer(3, byteArrayOf())
|
|
||||||
writeConstant(6, 6)
|
|
||||||
writeConstant(10, 4)
|
|
||||||
writeConstant(12, 1)
|
|
||||||
}
|
|
||||||
}.toByteArray()
|
|
||||||
return queryRemoteContent(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(payload))
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,51 @@
|
|||||||
|
package me.rhunk.snapenhance.util.download
|
||||||
|
|
||||||
|
import me.rhunk.snapenhance.Constants
|
||||||
|
import me.rhunk.snapenhance.Logger
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.util.Base64
|
||||||
|
|
||||||
|
object RemoteMediaResolver {
|
||||||
|
private const val BOLT_HTTP_RESOLVER_URL = "https://aws.api.snapchat.com/bolt-http"
|
||||||
|
const val CF_ST_CDN_D = "https://cf-st.sc-cdn.net/d/"
|
||||||
|
|
||||||
|
private val urlCache = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
private val okHttpClient = OkHttpClient.Builder()
|
||||||
|
.followRedirects(true)
|
||||||
|
.addInterceptor { chain ->
|
||||||
|
val request = chain.request()
|
||||||
|
val requestUrl = request.url.toString()
|
||||||
|
|
||||||
|
if (urlCache.containsKey(requestUrl)) {
|
||||||
|
val cachedUrl = urlCache[requestUrl]!!
|
||||||
|
return@addInterceptor chain.proceed(request.newBuilder().url(cachedUrl).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.proceed(request).apply {
|
||||||
|
val responseUrl = this.request.url.toString()
|
||||||
|
if (responseUrl.startsWith("https://cf-st.sc-cdn.net")) {
|
||||||
|
urlCache[requestUrl] = responseUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun downloadBoltMedia(protoKey: ByteArray): InputStream? {
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(BOLT_HTTP_RESOLVER_URL + "/resolve?co=" + Base64.getUrlEncoder().encodeToString(protoKey))
|
||||||
|
.addHeader("User-Agent", Constants.USER_AGENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
okHttpClient.newCall(request).execute().use { response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
Logger.log("Unexpected code $response")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return ByteArrayInputStream(response.body.bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user