diff --git a/app/libs/snapmod.jar b/app/libs/snapmod.jar index f56527d..b4eb64c 100644 Binary files a/app/libs/snapmod.jar and b/app/libs/snapmod.jar differ diff --git a/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt b/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt index 8b9a398..fcafb89 100644 --- a/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt +++ b/app/src/main/java/xyz/rodit/snapmod/CustomResources.kt @@ -11,6 +11,7 @@ object CustomResources { string.menu_option_stealth_mode to "Stealth Mode", string.menu_option_preview to "More Information", string.menu_option_auto_save to "Auto-Save Messages", + string.menu_option_auto_download to "Auto-Download Snaps", string.chat_action_playback_speed to "Set Playback Speed" ) @@ -39,6 +40,7 @@ object CustomResources { const val menu_option_stealth_mode = -100000 const val menu_option_preview = -100001 const val menu_option_auto_save = -100002 + const val menu_option_auto_download = -100003 const val chat_action_playback_speed = -200000 } diff --git a/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt b/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt index fd7298e..0574331 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/FeatureContext.kt @@ -11,6 +11,7 @@ import xyz.rodit.xposed.client.http.StreamServer private const val PINNED_CONVERSATIONS_FILE = "pinned.list" private const val STEALTH_CONVERSATIONS_FILE = "stealth.list" private const val AUTO_SAVE_CONVERSATIONS_FILE = "auto_save.list" +private const val AUTO_DOWNLOAD_CONVERSATIONS_FILE = "auto_download.list" class FeatureContext( val appContext: Context, @@ -25,6 +26,7 @@ class FeatureContext( val pinned: ConversationManager = ConversationManager(appContext.filesDir, PINNED_CONVERSATIONS_FILE) val stealth: ConversationManager = ConversationManager(appContext.filesDir, STEALTH_CONVERSATIONS_FILE) val autoSave: ConversationManager = ConversationManager(appContext.filesDir, AUTO_SAVE_CONVERSATIONS_FILE) + val autoDownload: ConversationManager = ConversationManager(appContext.filesDir, AUTO_DOWNLOAD_CONVERSATIONS_FILE) var activity: Activity? = null } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt b/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt index 7d615e8..75b05be 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/FeatureManager.kt @@ -9,6 +9,7 @@ import xyz.rodit.snapmod.features.messagemenu.MessageMenuModifier import xyz.rodit.snapmod.features.notifications.FilterTypes import xyz.rodit.snapmod.features.notifications.ShowMessageContent import xyz.rodit.snapmod.features.opera.OperaModelModifier +import xyz.rodit.snapmod.features.saving.AutoDownloadSnaps import xyz.rodit.snapmod.features.saving.ChatSaving import xyz.rodit.snapmod.features.saving.PublicProfileSaving import xyz.rodit.snapmod.features.saving.StoriesSaving @@ -45,6 +46,7 @@ class FeatureManager(context: FeatureContext) : Contextual(context) { add(::OperaModelModifier) // Saving + add(::AutoDownloadSnaps) add(::ChatSaving) add(::PublicProfileSaving) add(::StoriesSaving) diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/AutoSaveOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/AutoSaveOption.kt deleted file mode 100644 index 6971c04..0000000 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/AutoSaveOption.kt +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.rodit.snapmod.features.chatmenu - -import xyz.rodit.snapmod.CustomResources -import xyz.rodit.snapmod.features.FeatureContext - -class AutoSaveOption(context: FeatureContext) : - ToggleOption(context, "auto_save", CustomResources.string.menu_option_auto_save) { - - override fun shouldCreate(): Boolean { - return true - } - - override fun handleEvent(key: String?) { - context.autoSave.toggle(key) - } - - override fun isToggled(key: String?): Boolean { - return context.autoSave.isEnabled(key) - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt index 13a0fdd..5053556 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ChatMenuModifier.kt @@ -1,5 +1,9 @@ package xyz.rodit.snapmod.features.chatmenu +import xyz.rodit.snapmod.CustomResources.string.menu_option_auto_download +import xyz.rodit.snapmod.CustomResources.string.menu_option_auto_save +import xyz.rodit.snapmod.CustomResources.string.menu_option_stealth_mode +import xyz.rodit.snapmod.Shared import xyz.rodit.snapmod.features.Feature import xyz.rodit.snapmod.features.FeatureContext import xyz.rodit.snapmod.mappings.FriendChatActionHandler @@ -11,16 +15,28 @@ import xyz.rodit.snapmod.util.before const val EVENT_PREFIX = "CUSTOM_ACTION" const val EVENT_DELIMITER = "\u0000:\u0000" +const val PIN_STRING_NAME = "action_menu_pin_conversation" class ChatMenuModifier(context: FeatureContext) : Feature(context) { private val plugins: MutableMap = HashMap() override fun init() { - registerPlugin(PinOption(context)) - registerPlugin(StealthOption(context)) registerPlugin(PreviewOption(context)) - registerPlugin(AutoSaveOption(context)) + + val pinTextResource = context.appContext.resources.getIdentifier( + PIN_STRING_NAME, + "string", + Shared.SNAPCHAT_PACKAGE + ) + registerConversationToggle("pinning", pinTextResource) { it.pinned } + registerConversationToggle("stealth", menu_option_stealth_mode) { it.stealth } + registerConversationToggle("auto_save", menu_option_auto_save) { it.autoSave } + registerConversationToggle("auto_download", menu_option_auto_download) { it.autoDownload } + } + + private fun registerConversationToggle(name: String, textResource: Int, manager: Manager) { + registerPlugin(ConversationToggleOption(context, name, textResource, manager)) } private fun registerPlugin(plugin: MenuPlugin) { diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ConversationToggleOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ConversationToggleOption.kt new file mode 100644 index 0000000..ecddcb4 --- /dev/null +++ b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/ConversationToggleOption.kt @@ -0,0 +1,21 @@ +package xyz.rodit.snapmod.features.chatmenu + +import xyz.rodit.snapmod.features.FeatureContext +import xyz.rodit.snapmod.util.ConversationManager +import xyz.rodit.snapmod.util.getList + +typealias Manager = (FeatureContext) -> ConversationManager + +class ConversationToggleOption( + context: FeatureContext, + name: String, + textResource: Int, + private val manager: Manager +) : ToggleOption(context, name, textResource) { + + override fun shouldCreate() = !context.config.getList("hidden_chat_options").contains(name) + + override fun isToggled(key: String?) = manager(context).isEnabled(key) + + override fun handleEvent(data: String?) = manager(context).toggle(data) +} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PinOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PinOption.kt deleted file mode 100644 index dfb38ab..0000000 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/PinOption.kt +++ /dev/null @@ -1,25 +0,0 @@ -package xyz.rodit.snapmod.features.chatmenu - -import xyz.rodit.snapmod.Shared -import xyz.rodit.snapmod.features.FeatureContext - -const val PIN_STRING_NAME = "action_menu_pin_conversation" - -class PinOption(context: FeatureContext) : ToggleOption( - context, - "pinning", - context.appContext.resources.getIdentifier(PIN_STRING_NAME, "string", Shared.SNAPCHAT_PACKAGE) -) { - - override fun shouldCreate(): Boolean { - return context.config.getBoolean("allow_pin_chats") - } - - override fun handleEvent(key: String?) { - context.pinned.toggle(key) - } - - override fun isToggled(key: String?): Boolean { - return context.pinned.isEnabled(key) - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/StealthOption.kt b/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/StealthOption.kt deleted file mode 100644 index ea469e6..0000000 --- a/app/src/main/java/xyz/rodit/snapmod/features/chatmenu/StealthOption.kt +++ /dev/null @@ -1,20 +0,0 @@ -package xyz.rodit.snapmod.features.chatmenu - -import xyz.rodit.snapmod.CustomResources -import xyz.rodit.snapmod.features.FeatureContext - -class StealthOption(context: FeatureContext) : - ToggleOption(context, "stealth", CustomResources.string.menu_option_stealth_mode) { - - override fun shouldCreate(): Boolean { - return true - } - - override fun handleEvent(key: String?) { - context.stealth.toggle(key) - } - - override fun isToggled(key: String?): Boolean { - return context.stealth.isEnabled(key) - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/saving/AutoDownloadSnaps.kt b/app/src/main/java/xyz/rodit/snapmod/features/saving/AutoDownloadSnaps.kt new file mode 100644 index 0000000..a208288 --- /dev/null +++ b/app/src/main/java/xyz/rodit/snapmod/features/saving/AutoDownloadSnaps.kt @@ -0,0 +1,43 @@ +package xyz.rodit.snapmod.features.saving + +import xyz.rodit.snapmod.features.Feature +import xyz.rodit.snapmod.features.FeatureContext +import xyz.rodit.snapmod.mappings.* +import xyz.rodit.snapmod.util.PathManager +import xyz.rodit.snapmod.util.after +import xyz.rodit.snapmod.util.toUUIDString + +class AutoDownloadSnaps(context: FeatureContext) : Feature(context) { + + private val ignore = hashSetOf() + + override fun performHooks() { + OperaPageViewController.onDisplayStateChanged.after { + val viewController = OperaPageViewController.wrap(it.thisObject) + if (viewController.state.instance != OperaDisplayState.FULLY_DISPLAYED().instance) return@after + + val params = ParamsMap.wrap(viewController.metadata.instance) + val map = params.map + if (!map.containsKey(MessageStoryKeys.getSnapInSavedState().instance)) return@after + + val messageId = map[MessageStoryKeys.getMessageId().instance] as String? + val conversationId = UUID.wrap(map[ConversationStoryKeys.getConversationId().instance]) + if (messageId == null || conversationId.isNull) return@after + + if (!context.config.getBoolean("auto_download_snaps") + && !context.autoDownload.isEnabled(conversationId.toUUIDString()) + ) return@after + + if (ignore.contains(messageId)) return@after + + ignore.add(messageId) + getMediaInfo(context, params) { info -> + downloadOperaMedia( + context, + PathManager.DOWNLOAD_SNAP, + info + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/saving/StoriesSaving.kt b/app/src/main/java/xyz/rodit/snapmod/features/saving/StoriesSaving.kt index abc964b..bd1aee0 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/saving/StoriesSaving.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/saving/StoriesSaving.kt @@ -2,17 +2,12 @@ package xyz.rodit.snapmod.features.saving import xyz.rodit.snapmod.features.Feature import xyz.rodit.snapmod.features.FeatureContext -import xyz.rodit.snapmod.logging.log import xyz.rodit.snapmod.mappings.* import xyz.rodit.snapmod.util.PathManager import xyz.rodit.snapmod.util.after -import xyz.rodit.snapmod.util.download -import xyz.rodit.xposed.client.http.StreamProvider -import xyz.rodit.xposed.client.http.streams.FileProxyStreamProvider import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy -import java.net.URL class StoriesSaving(context: FeatureContext) : Feature(context) { @@ -42,36 +37,11 @@ class StoriesSaving(context: FeatureContext) : Feature(context) { if (method.name != ContextClickHandler.invoke.dexName) return null val map = ParamsMap.wrap(args!![0]) - getMediaInfo(context, map, this::downloadStoryMedia) - - return null - } - - private fun downloadStoryMedia(media: StoryMedia?) { - if (media == null || media.info.isNull) return - - val provider: StreamProvider = FileProxyStreamProvider(context.appContext) { - try { - var stream = URL(media.info.uri).openStream() - val enc = media.info.encryption - if (enc.isNotNull) { - stream = enc.decryptStream(stream) - } - - return@FileProxyStreamProvider stream - } catch (e: Exception) { - log.error("Error opening story media stream.", e) - } - return@FileProxyStreamProvider null + getMediaInfo(context, map) { + downloadOperaMedia(context, PathManager.DOWNLOAD_STORY, it) } - context.download( - PathManager.DOWNLOAD_STORY, - mapOf("u" to media.username), - media.extension, - provider, - media.username + "'s Story" - ) + return null } } } \ No newline at end of file diff --git a/app/src/main/java/xyz/rodit/snapmod/features/saving/StoryHelper.kt b/app/src/main/java/xyz/rodit/snapmod/features/saving/StoryHelper.kt index 96cd938..01eeb4b 100644 --- a/app/src/main/java/xyz/rodit/snapmod/features/saving/StoryHelper.kt +++ b/app/src/main/java/xyz/rodit/snapmod/features/saving/StoryHelper.kt @@ -4,8 +4,12 @@ import xyz.rodit.snapmod.createDummyProxy import xyz.rodit.snapmod.features.FeatureContext import xyz.rodit.snapmod.logging.XLog import xyz.rodit.snapmod.mappings.* +import xyz.rodit.snapmod.util.download import xyz.rodit.snapmod.util.toSnapUUID import xyz.rodit.snapmod.util.toUUIDString +import xyz.rodit.xposed.client.http.StreamProvider +import xyz.rodit.xposed.client.http.streams.FileProxyStreamProvider +import java.net.URL private val mLog = XLog("StoryHelper") @@ -14,8 +18,10 @@ typealias UsernameFetcher = (Map<*, *>) -> String? private val usernameFetchers = listOf( { val session = ContextSession.wrap(it[ContextStoryKeys.getContextSession().instance]) - val snapUsername = session.info.username - if (snapUsername.isNotNull) snapUsername.displayString else null + if (session.isNull) null else { + val snapUsername = session.info.username + if (snapUsername.isNotNull) snapUsername.displayString else null + } }, { val storySnap = @@ -24,6 +30,33 @@ private val usernameFetchers = listOf( } ) +fun downloadOperaMedia(context: FeatureContext, type: String, media: StoryMedia?) { + if (media == null || media.info.isNull) return + + val provider: StreamProvider = FileProxyStreamProvider(context.appContext) { + try { + var stream = URL(media.info.uri).openStream() + val enc = media.info.encryption + if (enc.isNotNull) { + stream = enc.decryptStream(stream) + } + + return@FileProxyStreamProvider stream + } catch (e: Exception) { + mLog.error("Error opening story media stream.", e) + } + return@FileProxyStreamProvider null + } + + context.download( + type, + mapOf("u" to media.username), + media.extension, + provider, + media.username + "'s Story" + ) +} + fun getMediaInfo( context: FeatureContext, metadata: ParamsMap, callback: (StoryMedia?) -> Unit ) { diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 8af9032..310adef 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -97,6 +97,20 @@ NOTE + + Pin + Stealth + Auto Save + Auto Download Snaps + + + + pinning + stealth + auto_save + auto_download + + CHAT STICKER diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1786141..7ea8a6c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,4 +146,10 @@ Filtered Message Types Notifications will not be shown for filtered message types. + + Hidden Chat Options + Hides extra chat options (if you feel the chat menu is too cluttered, for example). + + Auto-Download Snaps + Automatically downloads snaps when viewed. \ No newline at end of file diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index c5643cc..c9e8c63 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -158,11 +158,25 @@ app:iconSpaceReserved="false" app:defaultValue="@array/auto_save_types_defaults" /> + + + + () { [strict] .string "MESSAGE_ID", bind field messageId next reference; + [strict] + .string "SNAP_IN_SAVED_STATE", bind field snapInSavedState next reference; } } @@ -1770,4 +1773,41 @@ class AesCrypto { InputStream encrypt(InputStream) InputStream decrypt(InputStream) +} + +[certain] +class ConversationStoryKeys { + + [late] !MapKey conversationId; + [late] !MapKey senderId; + + static void $() { + [strict] + .string "CONVERSATION_ID", bind field conversationId next reference; + [strict] + .string "SENDER_ID", bind field senderId next reference; + } +} + +[certain, obfuscated] +enum OperaDisplayState { + + [late] static this .fields { NONE, PREPARING, MINIMALLY_DISPLAYED, FULLY_DISPLAYED, ERROR } +} + +class OperaPageViewController { + + !StoryMetadata metadata; + !OperaDisplayState state; + + void $(...) { + .string "OperaPageViewController"; + } + + void onDisplayStateChanged() { + .field !OperaDisplayState->!FULLY_DISPLAYED; + .field !OperaDisplayState->!MINIMALLY_DISPLAYED; + .string contains "display state"; + .string "Opera:moveNext"; + } } \ No newline at end of file