feat: rule system

This commit is contained in:
rhunk
2023-08-22 18:41:23 +02:00
parent 2b90ee2deb
commit 5c3ad5d588
29 changed files with 345 additions and 334 deletions

View File

@ -63,8 +63,12 @@ interface BridgeInterface {
/**
* Get rules for a given user or conversation
*/
List<String> getRules(String uuid);
List<String> getRules(String objectType, String uuid);
/**
* Update rule for a giver user or conversation
*/
void setRule(String uuid, String type, boolean state);
/**
* Sync groups and friends

View File

@ -28,6 +28,42 @@
}
},
"rules": {
"modes": {
"blacklist": "Blacklist mode",
"whitelist": "Whitelist mode"
},
"properties": {
"auto_download": {
"name": "Auto download",
"description": "Auto download snaps when viewed",
"options": {
"blacklist": "Exclude from Auto Download",
"whitelist": "Auto Download"
}
},
"stealth": {
"name": "Stealth Mode",
"description": "Prevents anyone from knowing you've opened their Snaps/Chats and conversations",
"options": {
"blacklist": "Exclude from stealth mode",
"whitelist": "Stealth mode"
}
},
"auto_save": {
"name": "Auto Save",
"description": "Saves chat messages when viewed",
"options": {
"blacklist": "Exclude from auto save",
"whitelist": "Auto save"
}
},
"hide_chat_feed": {
"name": "Hide from chat feed"
}
}
},
"action": {
"clean_cache": "Clean Cache",
"clear_message_logger": "Clear Message Logger",
@ -239,6 +275,10 @@
}
}
},
"rules": {
"name": "Rules",
"description": "Manage automatic features\nThe social tab lets you assign a rule to an object"
},
"camera": {
"name": "Camera",
"description": "Adjust the right settings for the perfect snap",

View File

@ -15,10 +15,8 @@ import me.rhunk.snapenhance.ModContext
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.bridge.types.FileActionType
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.messaging.MessagingRule
import me.rhunk.snapenhance.core.messaging.SocialScope
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.data.LocalePair
import me.rhunk.snapenhance.util.SerializableDataObject
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import kotlin.system.exitProcess
@ -138,9 +136,10 @@ class BridgeClient(
fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends)
fun getRulesFromId(type: SocialScope, targetUuid: String): List<MessagingRule> {
return service.getRules(type.name, targetUuid).map {
SerializableDataObject.fromJson(it, MessagingRule::class.java)
}.toList()
fun getRules(targetUuid: String): List<MessagingRuleType> {
return service.getRules(targetUuid).map { MessagingRuleType.getByName(it) }
}
fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean)
= service.setRule(targetUuid, type.key, state)
}

View File

@ -71,9 +71,8 @@ open class ConfigContainer(
fun fromJson(json: JsonObject) {
properties.forEach { (key, _) ->
val jsonElement = json.get(key.name) ?: return@forEach
key.dataType.deserializeAny(jsonElement)?.let {
properties[key]?.setAny(it)
}
//TODO: check incoming values
properties[key]?.setAny(key.dataType.deserializeAny(jsonElement))
}
}

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.core.config
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import kotlin.reflect.KProperty
@ -10,17 +11,43 @@ data class PropertyPair<T>(
val name get() = key.name
}
enum class FeatureNotice(
val id: Int,
val key: String
) {
UNSTABLE(0b0001, "unstable"),
MAY_BAN(0b0010, "may_ban"),
MAY_BREAK_INTERNAL_BEHAVIOR(0b0100, "may_break_internal_behavior"),
MAY_CAUSE_CRASHES(0b1000, "may_cause_crashes");
}
enum class ConfigFlag(
val id: Int
) {
NO_TRANSLATE(0b0001),
HIDDEN(0b0010),
FOLDER(0b0100),
NO_DISABLE_KEY(0b1000)
}
class ConfigParams(
private var _flags: Int? = null,
private var _notices: Int? = null,
var shouldTranslate: Boolean = true,
var isHidden: Boolean = false,
var isFolder: Boolean = false,
var icon: String? = null,
var disabledKey: String? = null,
var icon: String? = null
var customTranslationPath: String? = null,
var customOptionTranslationPath: String? = null
) {
val notices get() = _notices?.let { FeatureNotice.values().filter { flag -> it and flag.id != 0 } } ?: emptyList()
fun addNotices(vararg flags: FeatureNotice) {
this._notices = (this._notices ?: 0) or flags.fold(0) { acc, featureNotice -> acc or featureNotice.id }
val flags get() = _flags?.let { ConfigFlag.values().filter { flag -> it and flag.id != 0 } } ?: emptyList()
fun addNotices(vararg values: FeatureNotice) {
this._notices = (this._notices ?: 0) or values.fold(0) { acc, featureNotice -> acc or featureNotice.id }
}
fun addFlags(vararg values: ConfigFlag) {
this._flags = (this._flags ?: 0) or values.fold(0) { acc, flag -> acc or flag.id }
}
}
@ -55,12 +82,28 @@ data class PropertyKey<T>(
) {
val parentKey by lazy { _parent() }
fun propertyTranslationPath(): String {
return if (parentKey != null) {
"${parentKey!!.propertyTranslationPath()}.properties.$name"
} else {
"features.properties.$name"
fun propertyOption(translation: LocaleWrapper, key: String): String {
if (key == "null") {
return translation[params.disabledKey ?: "manager.features.disabled"]
}
return if (!params.flags.contains(ConfigFlag.NO_TRANSLATE))
translation[params.customOptionTranslationPath?.let {
"$it.$key"
} ?: "features.options.${name}.$key"]
else key
}
fun propertyName() = propertyTranslationPath() + ".name"
fun propertyDescription() = propertyTranslationPath() + ".description"
fun propertyTranslationPath(): String {
params.customTranslationPath?.let {
return it
}
return parentKey?.let {
"${it.propertyTranslationPath()}.properties.$name"
} ?: "features.properties.$name"
}
}

View File

@ -1,6 +1,7 @@
package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.ConfigFlag
import me.rhunk.snapenhance.core.config.FeatureNotice
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
@ -8,9 +9,9 @@ class Camera : ConfigContainer() {
val disable = boolean("disable_camera")
val immersiveCameraPreview = boolean("immersive_camera_preview") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
val overridePreviewResolution = unique("override_preview_resolution", *CameraTweaks.resolutions.toTypedArray())
{ shouldTranslate = false }
{ addFlags(ConfigFlag.NO_TRANSLATE) }
val overridePictureResolution = unique("override_picture_resolution", *CameraTweaks.resolutions.toTypedArray())
{ shouldTranslate = false }
{ addFlags(ConfigFlag.NO_TRANSLATE) }
val forceHighestFrameRate = boolean("force_highest_frame_rate") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) }
val forceCameraSourceEncoding = boolean("force_camera_source_encoding")
}

View File

@ -1,10 +1,11 @@
package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.ConfigFlag
import me.rhunk.snapenhance.core.config.FeatureNotice
class DownloaderConfig : ConfigContainer() {
val saveFolder = string("save_folder") { isFolder = true }
val saveFolder = string("save_folder") { addFlags(ConfigFlag.FOLDER) }
val autoDownloadOptions = multiple("auto_download_options",
"friend_snaps",
"friend_stories",

View File

@ -6,7 +6,7 @@ import me.rhunk.snapenhance.data.NotificationType
class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.MAY_BAN) }
val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY")
val autoUpdater = unique("auto_updater", "EVERY_LAUNCH", "DAILY", "WEEKLY").apply { set("DAILY") }
val disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads")
val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.MAY_BAN) }

View File

@ -7,6 +7,7 @@ class RootConfig : ConfigContainer() {
val userInterface = container("user_interface", UserInterfaceTweaks()) { icon = "RemoveRedEye"}
val messaging = container("messaging", MessagingTweaks()) { icon = "Send" }
val global = container("global", Global()) { icon = "MiscellaneousServices" }
val rules = container("rules", Rules()) { icon = "Rule" }
val camera = container("camera", Camera()) { icon = "Camera"}
val experimental = container("experimental", Experimental()) { icon = "Science" }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" }

View File

@ -0,0 +1,26 @@
package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.PropertyValue
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.core.messaging.RuleState
class Rules : ConfigContainer() {
private val rules = mutableMapOf<MessagingRuleType, PropertyValue<String>>()
fun getRuleState(ruleType: MessagingRuleType): RuleState? {
return rules[ruleType]?.getNullable()?.let { RuleState.getByName(it) }
}
init {
MessagingRuleType.values().filter { it.listMode }.forEach { ruleType ->
rules[ruleType] = unique(ruleType.key,"whitelist", "blacklist") {
customTranslationPath = "rules.properties.${ruleType.key}"
customOptionTranslationPath = "rules.modes"
}.apply {
set("whitelist")
}
}
}
}

View File

@ -2,15 +2,22 @@ package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.FeatureNotice
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
class UserInterfaceTweaks : ConfigContainer() {
val enableAppAppearance = boolean("enable_app_appearance")
val friendFeedMenuButtons = multiple(
"friend_feed_menu_buttons","conversation_info", *MessagingRuleType.values().toList().filter { it.listMode }.map { it.key }.toTypedArray()
).apply {
set(mutableListOf("conversation_info", MessagingRuleType.STEALTH.key))
}
val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1)
val amoledDarkMode = boolean("amoled_dark_mode") { addNotices(FeatureNotice.UNSTABLE) }
val mapFriendNameTags = boolean("map_friend_nametags")
val streakExpirationInfo = boolean("streak_expiration_info")
val hideStorySections = multiple("hide_story_sections", "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you")
val hideUiComponents = multiple(
"hide_ui_components",
val hideStorySections = multiple("hide_story_sections",
"hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you")
val hideUiComponents = multiple("hide_ui_components",
"hide_voice_record_button",
"hide_stickers_button",
"hide_cognac_button",
@ -27,7 +34,4 @@ class UserInterfaceTweaks : ConfigContainer() {
"ngs_search_icon_container"
)
val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") { addNotices(FeatureNotice.UNSTABLE) }
val friendFeedMenuButtons = multiple("friend_feed_menu_buttons", "auto_download_blacklist", "anti_auto_save", "stealth_mode", "conversation_info")
val friendFeedMenuPosition = integer("friend_feed_menu_position", defaultValue = 1)
val enableFriendFeedMenuBar = boolean("enable_friend_feed_menu_bar")
}

View File

@ -3,9 +3,15 @@ package me.rhunk.snapenhance.core.messaging
import me.rhunk.snapenhance.util.SerializableDataObject
enum class Mode {
BLACKLIST,
WHITELIST
enum class RuleState(
val key: String
) {
BLACKLIST("blacklist"),
WHITELIST("whitelist");
companion object {
fun getByName(name: String) = values().first { it.key == name }
}
}
enum class SocialScope(
@ -18,11 +24,20 @@ enum class SocialScope(
enum class MessagingRuleType(
val key: String,
val socialScope: SocialScope,
val listMode: Boolean
) {
DOWNLOAD("download", SocialScope.FRIEND),
STEALTH("stealth", SocialScope.GROUP),
AUTO_SAVE("auto_save", SocialScope.GROUP);
AUTO_DOWNLOAD("auto_download", true),
STEALTH("stealth", true),
AUTO_SAVE("auto_save", true),
HIDE_CHAT_FEED("hide_chat_feed", false);
fun translateOptionKey(optionKey: String): String {
return "rules.properties.${key}.options.${optionKey}"
}
companion object {
fun getByName(name: String) = values().first { it.key == name }
}
}
data class FriendStreaks(
@ -50,8 +65,8 @@ data class MessagingFriendInfo(
data class MessagingRule(
val id: Int,
val type: MessagingRuleType,
val socialScope: SocialScope,
val targetUuid: String,
//val mode: Mode?,
val subject: String
) : SerializableDataObject()

View File

@ -86,6 +86,23 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
val myUserId by lazy {
safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase ->
val cursor = arroyoDatabase.rawQuery(buildString {
append("SELECT * FROM required_values WHERE key = 'USERID'")
}, null)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation null
}
val userId = cursor.getString(cursor.getColumnIndex("value"))
cursor.close()
userId
}!!
}
fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
return safeDatabaseOperation(openMain()) {
readDatabaseObject(
@ -157,7 +174,7 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
fun getDMConversationIdFromUserId(userId: String): UserConversationLink? {
fun getConversationLinkFromUserId(userId: String): UserConversationLink? {
return safeDatabaseOperation(openArroyo()) {
readDatabaseObject(
UserConversationLink(),
@ -169,6 +186,22 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
fun getDMOtherParticipant(conversationId: String): String? {
return safeDatabaseOperation(openArroyo()) { cursor ->
val query = cursor.rawQuery(
"SELECT * FROM user_conversation WHERE client_conversation_id = ? AND conversation_type = 0",
arrayOf(conversationId)
)
val participants = mutableListOf<String>()
while (query.moveToNext()) {
participants.add(query.getString(query.getColumnIndex("user_id")))
}
query.close()
participants.firstOrNull { it != myUserId }
}
}
fun getStoryEntryFromId(storyId: String): StoryEntry? {
return safeDatabaseOperation(openMain()) {
readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId))
@ -194,23 +227,6 @@ class DatabaseAccess(private val context: ModContext) : Manager {
}
}
fun getMyUserId(): String? {
return safeDatabaseOperation(openArroyo()) { arroyoDatabase: SQLiteDatabase ->
val cursor = arroyoDatabase.rawQuery(buildString {
append("SELECT * FROM required_values WHERE key = 'USERID'")
}, null)
if (!cursor.moveToFirst()) {
cursor.close()
return@safeDatabaseOperation null
}
val userId = cursor.getString(cursor.getColumnIndex("value"))
cursor.close()
userId
}
}
fun getMessagesFromConversationId(
conversationId: String,
limit: Int

View File

@ -0,0 +1,33 @@
package me.rhunk.snapenhance.features
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.core.messaging.RuleState
abstract class MessagingRuleFeature(name: String, val ruleType: MessagingRuleType, loadParams: Int = 0) : Feature(name, loadParams) {
init {
if (!ruleType.listMode) throw IllegalArgumentException("Rule type must be a list mode")
}
fun getRuleState() = context.config.rules.getRuleState(ruleType)
fun setState(conversationId: String, state: Boolean) {
context.bridgeClient.setRule(
context.database.getDMOtherParticipant(conversationId) ?: conversationId,
ruleType,
state
)
}
fun getState(conversationId: String) =
context.bridgeClient.getRules(
context.database.getDMOtherParticipant(conversationId) ?: conversationId
).contains(ruleType) && getRuleState() != null
fun canUseRule(conversationId: String): Boolean {
val state = getState(conversationId)
if (context.config.rules.getRuleState(ruleType) == RuleState.BLACKLIST) {
return !state
}
return state
}
}

View File

@ -68,7 +68,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, {
hideBitmojiPresence || stealthMode.isStealth(openedConversationUUID.toString())
hideBitmojiPresence || stealthMode.canUseRule(openedConversationUUID.toString())
}) {
it.setResult(null)
}
@ -86,7 +86,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
}
Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, {
hideTypingNotification || stealthMode.isStealth(openedConversationUUID.toString())
hideTypingNotification || stealthMode.canUseRule(openedConversationUUID.toString())
}) {
it.setResult(null)
}

View File

@ -1,19 +0,0 @@
package me.rhunk.snapenhance.features.impl.downloader
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.features.BridgeFileFeature
import me.rhunk.snapenhance.features.FeatureLoadParams
class AntiAutoDownload : BridgeFileFeature("AntiAutoDownload", BridgeFileType.ANTI_AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() {
readFile()
}
fun setUserIgnored(userId: String, state: Boolean) {
setState(userId.hashCode().toLong().toString(16), state)
}
fun isUserIgnored(userId: String): Boolean {
return exists(userId.hashCode().toLong().toString(16))
}
}

View File

@ -9,6 +9,7 @@ import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.Logger.xposedLog
import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo
@ -24,8 +25,8 @@ import me.rhunk.snapenhance.download.data.InputMedia
import me.rhunk.snapenhance.download.data.MediaFilter
import me.rhunk.snapenhance.download.data.SplitMediaAssetType
import me.rhunk.snapenhance.download.data.toKeyPair
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.MessagingRuleFeature
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.hook.HookAdapter
@ -47,7 +48,7 @@ import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class)
class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
class MediaDownloader : MessagingRuleFeature("MediaDownloader", MessagingRuleType.AUTO_DOWNLOAD, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
private var lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
private var lastSeenMapParams: ParamMap? = null
@ -230,7 +231,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val senderId = conversationMessage.senderId!!
val conversationId = conversationMessage.clientConversationId!!
if (!forceDownload && context.feature(AntiAutoDownload::class).isUserIgnored(senderId)) {
if (!forceDownload && canUseRule(senderId)) {
return
}
@ -272,7 +273,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val author = context.database.getFriendInfo(
if (storyUserId == null || storyUserId == "null")
context.database.getMyUserId()!!
context.database.myUserId
else storyUserId
) ?: throw Exception("Friend not found in database")
val authorName = author.usernameForSorting!!

View File

@ -37,8 +37,6 @@ class MessageLogger : Feature("MessageLogger",
private val fetchedMessages = mutableListOf<Long>()
private val deletedMessageCache = mutableMapOf<Long, JsonObject>()
private val myUserId by lazy { context.database.getMyUserId() }
fun isMessageRemoved(conversationId: String, orderKey: Long) = deletedMessageCache.containsKey(computeMessageIdentifier(conversationId, orderKey))
fun deleteMessage(conversationId: String, clientMessageId: Long) {
@ -83,7 +81,7 @@ class MessageLogger : Feature("MessageLogger",
if (message.messageState != MessageState.COMMITTED) return
//exclude messages sent by me
if (message.senderId.toString() == myUserId) return
if (message.senderId.toString() == context.database.myUserId) return
val conversationId = message.messageDescriptor.conversationId.toString()
val serverIdentifier = computeMessageIdentifier(conversationId, message.orderKey)

View File

@ -12,7 +12,7 @@ class PreventReadReceipts : Feature("PreventReadReceipts", loadParams = FeatureL
val preventReadReceipts by context.config.messaging.preventReadReceipts
val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{
if (preventReadReceipts) return@hook true
context.feature(StealthMode::class).isStealth(it.toString())
context.feature(StealthMode::class).canUseRule(it.toString())
}
arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->

View File

@ -1,19 +1,6 @@
package me.rhunk.snapenhance.features.impl.spying
import me.rhunk.snapenhance.bridge.types.BridgeFileType
import me.rhunk.snapenhance.features.BridgeFileFeature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.features.MessagingRuleFeature
class StealthMode : BridgeFileFeature("StealthMode", BridgeFileType.STEALTH, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
override fun onActivityCreate() {
readFile()
}
fun setStealth(conversationId: String, stealth: Boolean) {
setState(conversationId.hashCode().toLong().toString(16), stealth)
}
fun isStealth(conversationId: String): Boolean {
return exists(conversationId.hashCode().toLong().toString(16))
}
}
class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH)

View File

@ -1,11 +1,12 @@
package me.rhunk.snapenhance.features.impl.tweaks
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.data.MessageState
import me.rhunk.snapenhance.data.wrapper.impl.Message
import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.MessagingRuleFeature
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.features.impl.spying.StealthMode
@ -15,14 +16,12 @@ import me.rhunk.snapenhance.util.CallbackBuilder
import me.rhunk.snapenhance.util.ktx.getObjectField
import java.util.concurrent.Executors
class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
class AutoSave : MessagingRuleFeature("Auto Save", MessagingRuleType.AUTO_SAVE, loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) {
private val asyncSaveExecutorService = Executors.newSingleThreadExecutor()
private val messageLogger by lazy { context.feature(MessageLogger::class) }
private val messaging by lazy { context.feature(Messaging::class) }
private val myUserId by lazy { context.database.getMyUserId() }
private val fetchConversationWithMessagesCallbackClass by lazy { context.mappings.getMappedClass("callbacks", "FetchConversationWithMessagesCallback") }
private val callbackClass by lazy { context.mappings.getMappedClass("callbacks", "Callback") }
@ -62,7 +61,7 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR
}
private fun canSaveMessage(message: Message): Boolean {
if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == myUserId }) return false
if (message.messageMetadata.savedBy.any { uuid -> uuid.toString() == context.database.myUserId }) return false
val contentType = message.messageContent.contentType.toString()
return autoSaveFilter.any { it == contentType }
@ -74,8 +73,8 @@ class AutoSave : Feature("Auto Save", loadParams = FeatureLoadParams.ACTIVITY_CR
with(context.feature(Messaging::class)) {
if (openedConversationUUID == null) return@canSave false
val conversation = openedConversationUUID.toString()
if (context.feature(StealthMode::class).isStealth(conversation)) return@canSave false
if (context.feature(AntiAutoSave::class).isConversationIgnored(conversation)) return@canSave false
if (context.feature(StealthMode::class).canUseRule(conversation)) return@canSave false
if (canUseRule(conversation)) return@canSave false
}
return true
}

View File

@ -154,7 +154,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
.toString()
context.database.getMyUserId()?.let { context.database.getFriendInfo(it) }?.let { myUser ->
context.database.myUserId.let { context.database.getFriendInfo(it) }?.let { myUser ->
cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input")
updateNotification(notificationId) { notification ->

View File

@ -7,7 +7,6 @@ import me.rhunk.snapenhance.features.FeatureLoadParams
import me.rhunk.snapenhance.features.impl.AutoUpdater
import me.rhunk.snapenhance.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode
import me.rhunk.snapenhance.features.impl.experiments.AppPasscode
@ -76,7 +75,6 @@ class FeatureManager(private val context: ModContext) : Manager {
register(AutoSave::class)
register(UITweaks::class)
register(ConfigurationOverride::class)
register(AntiAutoDownload::class)
register(GalleryMediaSendOverride::class)
register(AntiAutoSave::class)
register(UnlimitedSnapViewTime::class)

View File

@ -4,32 +4,23 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.Switch
import android.widget.Toast
import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.wrapper.impl.FriendActionButton
import me.rhunk.snapenhance.database.objects.ConversationMessage
import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.database.objects.UserConversationLink
import me.rhunk.snapenhance.features.MessagingRuleFeature
import me.rhunk.snapenhance.features.impl.Messaging
import me.rhunk.snapenhance.features.impl.downloader.AntiAutoDownload
import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.spying.StealthMode
import me.rhunk.snapenhance.features.impl.tweaks.AntiAutoSave
import me.rhunk.snapenhance.features.impl.tweaks.AutoSave
import me.rhunk.snapenhance.ui.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.menu.AbstractMenu
import me.rhunk.snapenhance.util.snap.BitmojiSelfie
@ -106,17 +97,18 @@ class FriendFeedInfoMenu : AbstractMenu() {
context.config.messaging.messagePreviewLength.get()
)?.reversed()
if (messages.isNullOrEmpty()) {
Toast.makeText(androidCtx, "No messages found", Toast.LENGTH_SHORT).show()
if (messages == null) {
context.longToast("Can't fetch messages")
return
}
val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!!
.map { context.database.getFriendInfo(it)!! }
.associateBy { it.userId!! }
val messageBuilder = StringBuilder()
messages.forEach{ message: ConversationMessage ->
messages.forEach { message ->
val sender: FriendInfo? = participants[message.senderId]
var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.contentType).name
@ -164,7 +156,6 @@ class FriendFeedInfoMenu : AbstractMenu() {
.append("\n")
}
//alert dialog
val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
builder.setTitle(context.translation["conversation_preview.title"])
@ -182,22 +173,6 @@ class FriendFeedInfoMenu : AbstractMenu() {
builder.show()
}
private fun createEmojiDrawable(text: String, width: Int, height: Int, textSize: Float, disabled: Boolean = false): Drawable {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val paint = Paint()
paint.textSize = textSize
paint.color = Color.BLACK
paint.textAlign = Paint.Align.CENTER
canvas.drawText(text, width / 2f, height.toFloat() - paint.descent(), paint)
if (disabled) {
paint.color = Color.RED
paint.strokeWidth = 5f
canvas.drawLine(0f, 0f, width.toFloat(), height.toFloat(), paint)
}
return BitmapDrawable(context.resources, bitmap)
}
private fun getCurrentConversationInfo(): Pair<String, String?> {
val messaging = context.feature(Messaging::class)
val focusedConversationTargetUser: String? = messaging.lastFetchConversationUserUUID?.toString()
@ -213,7 +188,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//old conversation fetch
val conversationId = if (messaging.lastFetchConversationUUID == null && focusedConversationTargetUser != null) {
val conversation: UserConversationLink = context.database.getDMConversationIdFromUserId(focusedConversationTargetUser) ?: throw IllegalStateException("No conversation found")
val conversation: UserConversationLink = context.database.getConversationLinkFromUserId(focusedConversationTargetUser) ?: throw IllegalStateException("No conversation found")
conversation.clientConversationId!!.trim().lowercase()
} else {
messaging.lastFetchConversationUUID.toString()
@ -244,148 +219,38 @@ class FriendFeedInfoMenu : AbstractMenu() {
val (conversationId, targetUser) = getCurrentConversationInfo()
if (!context.config.userInterface.enableFriendFeedMenuBar.get()) {
//preview button
val previewButton = Button(viewModel.context).apply {
text = modContext.translation["friend_menu_option.preview"]
ViewAppearanceHelper.applyTheme(this, viewModel.width)
setOnClickListener {
showPreview(
targetUser,
conversationId,
context
)
}
}
//stealth switch
val stealthSwitch = Switch(viewModel.context).apply {
text = modContext.translation["friend_menu_option.stealth_mode"]
isChecked = modContext.feature(StealthMode::class).isStealth(conversationId)
ViewAppearanceHelper.applyTheme(this)
setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
modContext.feature(StealthMode::class).setStealth(
conversationId,
isChecked
)
}
}
if (friendFeedMenuOptions.contains("anti_auto_save")) {
createToggleFeature(viewConsumer,
"friend_menu_option.anti_auto_save",
{ context.feature(AntiAutoSave::class).isConversationIgnored(conversationId) },
{ context.feature(AntiAutoSave::class).setConversationIgnored(conversationId, it) }
)
}
run {
val userId = context.database.getFeedEntryByConversationId(conversationId)?.friendUserId ?: return@run
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
createToggleFeature(viewConsumer,
"friend_menu_option.auto_download_blacklist",
{ context.feature(AntiAutoDownload::class).isUserIgnored(userId) },
{ context.feature(AntiAutoDownload::class).setUserIgnored(userId, it) }
)
}
}
if (friendFeedMenuOptions.contains("stealth_mode")) {
viewConsumer(stealthSwitch)
}
if (friendFeedMenuOptions.contains("conversation_info")) {
viewConsumer(previewButton)
}
return
}
val menuButtonBar = LinearLayout(viewModel.context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER
}
fun createActionButton(icon: String, isDisabled: Boolean? = null, onClick: (Boolean) -> Unit) {
//FIXME: hardcoded values
menuButtonBar.addView(LinearLayout(viewModel.context).apply {
layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
gravity = Gravity.CENTER
isClickable = false
var isLineThrough = isDisabled ?: false
FriendActionButton.new(viewModel.context).apply {
fun setLineThrough(value: Boolean) {
setIconDrawable(createEmojiDrawable(icon, 60, 60, 50f, if (isDisabled == null) false else value))
}
setLineThrough(isLineThrough)
(instanceNonNull() as View).apply {
layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
setMargins(0, 40, 0, 40)
}
setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
isLineThrough = !isLineThrough
onClick(isLineThrough)
setLineThrough(isLineThrough)
}
false
}
}
}.also { addView(it.instanceNonNull() as View) }
})
}
if (friendFeedMenuOptions.contains("auto_download_blacklist")) {
run {
val userId =
context.database.getFeedEntryByConversationId(conversationId)?.friendUserId
?: return@run
createActionButton(
"\u2B07\uFE0F",
isDisabled = !context.feature(AntiAutoDownload::class).isUserIgnored(userId)
) {
context.feature(AntiAutoDownload::class).setUserIgnored(userId, !it)
}
}
}
if (friendFeedMenuOptions.contains("anti_auto_save")) {
//diskette
createActionButton("\uD83D\uDCAC",
isDisabled = !context.feature(AntiAutoSave::class)
.isConversationIgnored(conversationId)
) {
context.feature(AntiAutoSave::class).setConversationIgnored(conversationId, !it)
}
}
if (friendFeedMenuOptions.contains("stealth_mode")) {
//eyes
createActionButton(
"\uD83D\uDC7B",
isDisabled = !context.feature(StealthMode::class).isStealth(conversationId)
) { isChecked ->
context.feature(StealthMode::class).setStealth(
val previewButton = Button(viewModel.context).apply {
text = modContext.translation["friend_menu_option.preview"]
ViewAppearanceHelper.applyTheme(this, viewModel.width)
setOnClickListener {
showPreview(
targetUser,
conversationId,
!isChecked
context
)
}
}
if (friendFeedMenuOptions.contains("conversation_info")) {
//user
createActionButton("\uD83D\uDC64") {
showPreview(
targetUser,
conversationId,
viewModel.context
)
}
viewConsumer(previewButton)
}
viewConsumer(menuButtonBar)
val rules: Array<MessagingRuleFeature> = arrayOf(
StealthMode::class,
AutoSave::class,
MediaDownloader::class
).map { modContext.feature(it) }.toTypedArray()
rules.forEach { ruleFeature ->
if (!friendFeedMenuOptions.contains(ruleFeature.ruleType.key)) return@forEach
Logger.debug("${ruleFeature.ruleType.key} ${ruleFeature.getRuleState()}")
val ruleState = ruleFeature.getRuleState() ?: return@forEach
createToggleFeature(viewConsumer,
ruleFeature.ruleType.translateOptionKey(ruleState.key),
{ ruleFeature.getState(conversationId) },
{ ruleFeature.setState(conversationId, it) }
)
}
}
}