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

@ -123,11 +123,12 @@ class BridgeService : Service() {
).onReceive(intent) ).onReceive(intent)
} }
override fun getRules(objectType: String, uuid: String): MutableList<String> { override fun getRules(uuid: String): List<String> {
remoteSideContext.modDatabase.getRulesFromId(SocialScope.valueOf(objectType), uuid) return remoteSideContext.modDatabase.getRules(uuid).map { it.key }
.let { rules -> }
return rules.map { it.toJson() }.toMutableList()
} override fun setRule(uuid: String, rule: String, state: Boolean) {
remoteSideContext.modDatabase.setRule(uuid, rule, state)
} }
override fun sync(callback: SyncCallback) { override fun sync(callback: SyncCallback) {

View File

@ -6,8 +6,7 @@ import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.core.messaging.FriendStreaks import me.rhunk.snapenhance.core.messaging.FriendStreaks
import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.core.messaging.MessagingRule import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.core.messaging.SocialScope
import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.util.SQLiteDatabaseHelper import me.rhunk.snapenhance.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.util.ktx.getInteger import me.rhunk.snapenhance.util.ktx.getInteger
@ -53,10 +52,8 @@ class ModDatabase(
), ),
"rules" to listOf( "rules" to listOf(
"id INTEGER PRIMARY KEY AUTOINCREMENT", "id INTEGER PRIMARY KEY AUTOINCREMENT",
"scope VARCHAR", "type VARCHAR",
"targetUuid VARCHAR", "targetUuid VARCHAR"
//"mode VARCHAR",
"subject VARCHAR"
), ),
"streaks" to listOf( "streaks" to listOf(
"userId VARCHAR PRIMARY KEY", "userId VARCHAR PRIMARY KEY",
@ -157,34 +154,29 @@ class ModDatabase(
} }
} }
fun getRulesFromId(type: SocialScope, targetUuid: String): List<MessagingRule> { fun getRules(targetUuid: String): List<MessagingRuleType> {
return database.rawQuery("SELECT * FROM rules WHERE scope = ? AND targetUuid = ?", arrayOf(type.name, targetUuid)).use { cursor -> return database.rawQuery("SELECT type FROM rules WHERE targetUuid = ?", arrayOf(
val rules = mutableListOf<MessagingRule>() targetUuid
)).use { cursor ->
val rules = mutableListOf<MessagingRuleType>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
rules.add(MessagingRule( rules.add(MessagingRuleType.getByName(cursor.getStringOrNull("type")!!))
id = cursor.getInteger("id"),
socialScope = SocialScope.valueOf(cursor.getStringOrNull("scope")!!),
targetUuid = cursor.getStringOrNull("targetUuid")!!,
subject = cursor.getStringOrNull("subject")!!
))
} }
rules rules
} }
} }
fun toggleRuleFor(type: SocialScope, targetUuid: String, subject: String, enabled: Boolean) { fun setRule(targetUuid: String, type: String, enabled: Boolean) {
executeAsync { executeAsync {
if (enabled) { if (enabled) {
database.execSQL("INSERT OR REPLACE INTO rules (scope, targetUuid, subject) VALUES (?, ?, ?)", arrayOf( database.execSQL("INSERT OR REPLACE INTO rules (targetUuid, type) VALUES (?, ?)", arrayOf(
type.name,
targetUuid, targetUuid,
subject type
)) ))
} else { } else {
database.execSQL("DELETE FROM rules WHERE scope = ? AND targetUuid = ? AND subject = ?", arrayOf( database.execSQL("DELETE FROM rules WHERE targetUuid = ? AND type = ?", arrayOf(
type.name,
targetUuid, targetUuid,
subject type
)) ))
} }
} }

View File

@ -57,10 +57,7 @@ class Dialogs(
@Composable @Composable
fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) { fun TranslatedText(property: PropertyPair<*>, key: String, modifier: Modifier = Modifier) {
Text( Text(
text = when (key) { text = property.key.propertyOption(translation, key),
"null" -> translation["manager.features.disabled"]
else -> if (property.key.params.shouldTranslate) translation["features.options.${property.key.name}.$key"] else key
},
modifier = Modifier modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 10.dp) .padding(10.dp, 10.dp, 10.dp, 10.dp)
.then(modifier) .then(modifier)

View File

@ -28,6 +28,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Rule
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.BottomSheetScaffoldState import androidx.compose.material3.BottomSheetScaffoldState
@ -71,6 +72,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.rhunk.snapenhance.core.config.ConfigContainer import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.ConfigFlag
import me.rhunk.snapenhance.core.config.DataProcessors import me.rhunk.snapenhance.core.config.DataProcessors
import me.rhunk.snapenhance.core.config.PropertyKey import me.rhunk.snapenhance.core.config.PropertyKey
import me.rhunk.snapenhance.core.config.PropertyPair import me.rhunk.snapenhance.core.config.PropertyPair
@ -172,8 +174,8 @@ class FeaturesSection : Section() {
backStackEntry.arguments?.getString("keyword")?.let { keyword -> backStackEntry.arguments?.getString("keyword")?.let { keyword ->
val properties = allProperties.filter { val properties = allProperties.filter {
it.key.name.contains(keyword, ignoreCase = true) || it.key.name.contains(keyword, ignoreCase = true) ||
context.translation["${it.key.propertyTranslationPath()}.name"].contains(keyword, ignoreCase = true) || context.translation[it.key.propertyName()].contains(keyword, ignoreCase = true) ||
context.translation["${it.key.propertyTranslationPath()}.description"].contains(keyword, ignoreCase = true) context.translation[it.key.propertyDescription()].contains(keyword, ignoreCase = true)
}.map { PropertyPair(it.key, it.value) } }.map { PropertyPair(it.key, it.value) }
PropertiesView(properties) PropertiesView(properties)
@ -199,7 +201,7 @@ class FeaturesSection : Section() {
val propertyValue = property.value val propertyValue = property.value
if (property.key.params.isFolder) { if (property.key.params.flags.contains(ConfigFlag.FOLDER)) {
IconButton(onClick = registerClickCallback { IconButton(onClick = registerClickCallback {
openFolderCallback = { uri -> openFolderCallback = { uri ->
propertyValue.setAny(uri) propertyValue.setAny(uri)
@ -234,11 +236,9 @@ class FeaturesSection : Section() {
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1, maxLines = 1,
modifier = Modifier.widthIn(0.dp, 120.dp), modifier = Modifier.widthIn(0.dp, 120.dp),
text = (propertyValue.getNullable() as? String)?.let{ text = (propertyValue.getNullable() as? String ?: "null").let {
if (property.key.params.shouldTranslate) { property.key.propertyOption(context.translation, it)
context.translation["features.options.${property.name}.$it"] }
} else it
} ?: context.translation["manager.features.disabled"],
) )
} }
@ -353,12 +353,12 @@ class FeaturesSection : Section() {
.padding(all = 10.dp) .padding(all = 10.dp)
) { ) {
Text( Text(
text = context.translation["${property.key.propertyTranslationPath()}.name"], text = context.translation[property.key.propertyName()],
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
Text( Text(
text = context.translation["${property.key.propertyTranslationPath()}.description"], text = context.translation[property.key.propertyDescription()],
fontSize = 12.sp, fontSize = 12.sp,
lineHeight = 15.sp lineHeight = 15.sp
) )

View File

@ -71,23 +71,33 @@ class ScopeContent(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
val scopeRules = context.modDatabase.getRulesFromId(scope, id) val rules = context.modDatabase.getRules(id)
SectionTitle("Rules") SectionTitle("Rules")
ContentCard { ContentCard {
//manager anti features etc //manager anti features etc
MessagingRuleType.values().forEach { feature -> MessagingRuleType.values().forEach { ruleType ->
var featureEnabled by remember { var ruleEnabled by remember {
mutableStateOf(scopeRules.any { it.subject == feature.key }) mutableStateOf(rules.any { it.key == ruleType.key })
} }
val featureEnabledText = if (featureEnabled) "Enabled" else "Disabled"
Row { val ruleState = context.config.root.rules.getRuleState(ruleType)
Text(text = "${feature.key}: $featureEnabledText", maxLines = 1)
Switch(checked = featureEnabled, onCheckedChange = { Row(
context.modDatabase.toggleRuleFor(scope, id, feature.key, it) verticalAlignment = Alignment.CenterVertically,
featureEnabled = it modifier = Modifier.padding(all = 4.dp)
) {
Text(
text = if (ruleType.listMode && ruleState != null) {
context.translation["rules.properties.${ruleType.key}.options.${ruleState.key}"]
} else context.translation["rules.properties.${ruleType.key}.name"],
maxLines = 1,
modifier = Modifier.weight(1f)
)
Switch(checked = ruleEnabled, enabled = if (ruleType.listMode) ruleState != null else true, onCheckedChange = {
context.modDatabase.setRule(id, ruleType.key, it)
ruleEnabled = it
}) })
} }
} }
@ -195,9 +205,9 @@ class ScopeContent(
fontSize = 12.sp, fontSize = 12.sp,
fontWeight = FontWeight.Light fontWeight = FontWeight.Light
) )
Spacer(modifier = Modifier.height(16.dp)) // Spacer(modifier = Modifier.height(16.dp))
DeleteScopeEntityButton() //DeleteScopeEntityButton()
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View File

@ -63,8 +63,12 @@ interface BridgeInterface {
/** /**
* Get rules for a given user or conversation * 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 * 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": { "action": {
"clean_cache": "Clean Cache", "clean_cache": "Clean Cache",
"clear_message_logger": "Clear Message Logger", "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": { "camera": {
"name": "Camera", "name": "Camera",
"description": "Adjust the right settings for the perfect snap", "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.BridgeFileType
import me.rhunk.snapenhance.bridge.types.FileActionType import me.rhunk.snapenhance.bridge.types.FileActionType
import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.messaging.MessagingRule import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.core.messaging.SocialScope
import me.rhunk.snapenhance.data.LocalePair import me.rhunk.snapenhance.data.LocalePair
import me.rhunk.snapenhance.util.SerializableDataObject
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -138,9 +136,10 @@ class BridgeClient(
fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends) fun passGroupsAndFriends(groups: List<String>, friends: List<String>) = service.passGroupsAndFriends(groups, friends)
fun getRulesFromId(type: SocialScope, targetUuid: String): List<MessagingRule> { fun getRules(targetUuid: String): List<MessagingRuleType> {
return service.getRules(type.name, targetUuid).map { return service.getRules(targetUuid).map { MessagingRuleType.getByName(it) }
SerializableDataObject.fromJson(it, MessagingRule::class.java)
}.toList()
} }
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) { fun fromJson(json: JsonObject) {
properties.forEach { (key, _) -> properties.forEach { (key, _) ->
val jsonElement = json.get(key.name) ?: return@forEach val jsonElement = json.get(key.name) ?: return@forEach
key.dataType.deserializeAny(jsonElement)?.let { //TODO: check incoming values
properties[key]?.setAny(it) properties[key]?.setAny(key.dataType.deserializeAny(jsonElement))
}
} }
} }

View File

@ -1,5 +1,6 @@
package me.rhunk.snapenhance.core.config package me.rhunk.snapenhance.core.config
import me.rhunk.snapenhance.bridge.wrapper.LocaleWrapper
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -10,17 +11,43 @@ data class PropertyPair<T>(
val name get() = key.name 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( class ConfigParams(
private var _flags: Int? = null,
private var _notices: Int? = null, private var _notices: Int? = null,
var shouldTranslate: Boolean = true,
var isHidden: Boolean = false, var icon: String? = null,
var isFolder: Boolean = false,
var disabledKey: 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() val notices get() = _notices?.let { FeatureNotice.values().filter { flag -> it and flag.id != 0 } } ?: emptyList()
fun addNotices(vararg flags: FeatureNotice) { val flags get() = _flags?.let { ConfigFlag.values().filter { flag -> it and flag.id != 0 } } ?: emptyList()
this._notices = (this._notices ?: 0) or flags.fold(0) { acc, featureNotice -> acc or featureNotice.id }
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() } val parentKey by lazy { _parent() }
fun propertyTranslationPath(): String { fun propertyOption(translation: LocaleWrapper, key: String): String {
return if (parentKey != null) { if (key == "null") {
"${parentKey!!.propertyTranslationPath()}.properties.$name" return translation[params.disabledKey ?: "manager.features.disabled"]
} else {
"features.properties.$name"
} }
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 package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer 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.core.config.FeatureNotice
import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks
@ -8,9 +9,9 @@ class Camera : ConfigContainer() {
val disable = boolean("disable_camera") val disable = boolean("disable_camera")
val immersiveCameraPreview = boolean("immersive_camera_preview") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) } val immersiveCameraPreview = boolean("immersive_camera_preview") { addNotices(FeatureNotice.MAY_CAUSE_CRASHES) }
val overridePreviewResolution = unique("override_preview_resolution", *CameraTweaks.resolutions.toTypedArray()) val overridePreviewResolution = unique("override_preview_resolution", *CameraTweaks.resolutions.toTypedArray())
{ shouldTranslate = false } { addFlags(ConfigFlag.NO_TRANSLATE) }
val overridePictureResolution = unique("override_picture_resolution", *CameraTweaks.resolutions.toTypedArray()) 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 forceHighestFrameRate = boolean("force_highest_frame_rate") { addNotices(FeatureNotice.MAY_BREAK_INTERNAL_BEHAVIOR) }
val forceCameraSourceEncoding = boolean("force_camera_source_encoding") val forceCameraSourceEncoding = boolean("force_camera_source_encoding")
} }

View File

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

View File

@ -6,7 +6,7 @@ import me.rhunk.snapenhance.data.NotificationType
class Global : ConfigContainer() { class Global : ConfigContainer() {
val snapchatPlus = boolean("snapchat_plus") { addNotices(FeatureNotice.MAY_BAN) } 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 disableMetrics = boolean("disable_metrics")
val blockAds = boolean("block_ads") val blockAds = boolean("block_ads")
val disableVideoLengthRestrictions = boolean("disable_video_length_restrictions") { addNotices(FeatureNotice.MAY_BAN) } 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 userInterface = container("user_interface", UserInterfaceTweaks()) { icon = "RemoveRedEye"}
val messaging = container("messaging", MessagingTweaks()) { icon = "Send" } val messaging = container("messaging", MessagingTweaks()) { icon = "Send" }
val global = container("global", Global()) { icon = "MiscellaneousServices" } val global = container("global", Global()) { icon = "MiscellaneousServices" }
val rules = container("rules", Rules()) { icon = "Rule" }
val camera = container("camera", Camera()) { icon = "Camera"} val camera = container("camera", Camera()) { icon = "Camera"}
val experimental = container("experimental", Experimental()) { icon = "Science" } val experimental = container("experimental", Experimental()) { icon = "Science" }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" } 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.ConfigContainer
import me.rhunk.snapenhance.core.config.FeatureNotice import me.rhunk.snapenhance.core.config.FeatureNotice
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
class UserInterfaceTweaks : ConfigContainer() { class UserInterfaceTweaks : ConfigContainer() {
val enableAppAppearance = boolean("enable_app_appearance") 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 amoledDarkMode = boolean("amoled_dark_mode") { addNotices(FeatureNotice.UNSTABLE) }
val mapFriendNameTags = boolean("map_friend_nametags") val mapFriendNameTags = boolean("map_friend_nametags")
val streakExpirationInfo = boolean("streak_expiration_info") val streakExpirationInfo = boolean("streak_expiration_info")
val hideStorySections = multiple("hide_story_sections", "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you") val hideStorySections = multiple("hide_story_sections",
val hideUiComponents = multiple( "hide_friend_suggestions", "hide_friends", "hide_following", "hide_for_you")
"hide_ui_components", val hideUiComponents = multiple("hide_ui_components",
"hide_voice_record_button", "hide_voice_record_button",
"hide_stickers_button", "hide_stickers_button",
"hide_cognac_button", "hide_cognac_button",
@ -27,7 +34,4 @@ class UserInterfaceTweaks : ConfigContainer() {
"ngs_search_icon_container" "ngs_search_icon_container"
) )
val storyViewerOverride = unique("story_viewer_override", "DISCOVER_PLAYBACK_SEEKBAR", "VERTICAL_STORY_VIEWER") { addNotices(FeatureNotice.UNSTABLE) } 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 import me.rhunk.snapenhance.util.SerializableDataObject
enum class Mode { enum class RuleState(
BLACKLIST, val key: String
WHITELIST ) {
BLACKLIST("blacklist"),
WHITELIST("whitelist");
companion object {
fun getByName(name: String) = values().first { it.key == name }
}
} }
enum class SocialScope( enum class SocialScope(
@ -18,11 +24,20 @@ enum class SocialScope(
enum class MessagingRuleType( enum class MessagingRuleType(
val key: String, val key: String,
val socialScope: SocialScope, val listMode: Boolean
) { ) {
DOWNLOAD("download", SocialScope.FRIEND), AUTO_DOWNLOAD("auto_download", true),
STEALTH("stealth", SocialScope.GROUP), STEALTH("stealth", true),
AUTO_SAVE("auto_save", SocialScope.GROUP); 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( data class FriendStreaks(
@ -50,8 +65,8 @@ data class MessagingFriendInfo(
data class MessagingRule( data class MessagingRule(
val id: Int, val id: Int,
val type: MessagingRuleType,
val socialScope: SocialScope, val socialScope: SocialScope,
val targetUuid: String, val targetUuid: String,
//val mode: Mode?, //val mode: Mode?,
val subject: String
) : SerializableDataObject() ) : 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? { fun getFeedEntryByConversationId(conversationId: String): FriendFeedEntry? {
return safeDatabaseOperation(openMain()) { return safeDatabaseOperation(openMain()) {
readDatabaseObject( 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()) { return safeDatabaseOperation(openArroyo()) {
readDatabaseObject( readDatabaseObject(
UserConversationLink(), 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? { fun getStoryEntryFromId(storyId: String): StoryEntry? {
return safeDatabaseOperation(openMain()) { return safeDatabaseOperation(openMain()) {
readDatabaseObject(StoryEntry(), it, "Story", "storyId = ?", arrayOf(storyId)) 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( fun getMessagesFromConversationId(
conversationId: String, conversationId: String,
limit: Int 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 -> arrayOf("activate", "deactivate", "processTypingActivity").forEach { hook ->
Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, { Hooker.hook(context.classCache.presenceSession, hook, HookStage.BEFORE, {
hideBitmojiPresence || stealthMode.isStealth(openedConversationUUID.toString()) hideBitmojiPresence || stealthMode.canUseRule(openedConversationUUID.toString())
}) { }) {
it.setResult(null) it.setResult(null)
} }
@ -86,7 +86,7 @@ class Messaging : Feature("Messaging", loadParams = FeatureLoadParams.ACTIVITY_C
} }
Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, { Hooker.hook(context.classCache.conversationManager, "sendTypingNotification", HookStage.BEFORE, {
hideTypingNotification || stealthMode.isStealth(openedConversationUUID.toString()) hideTypingNotification || stealthMode.canUseRule(openedConversationUUID.toString())
}) { }) {
it.setResult(null) 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
import me.rhunk.snapenhance.Logger.xposedLog import me.rhunk.snapenhance.Logger.xposedLog
import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.bridge.DownloadCallback
import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.data.ContentType import me.rhunk.snapenhance.data.ContentType
import me.rhunk.snapenhance.data.FileType import me.rhunk.snapenhance.data.FileType
import me.rhunk.snapenhance.data.wrapper.impl.media.MediaInfo 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.MediaFilter
import me.rhunk.snapenhance.download.data.SplitMediaAssetType import me.rhunk.snapenhance.download.data.SplitMediaAssetType
import me.rhunk.snapenhance.download.data.toKeyPair import me.rhunk.snapenhance.download.data.toKeyPair
import me.rhunk.snapenhance.features.Feature
import me.rhunk.snapenhance.features.FeatureLoadParams 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.Messaging
import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.hook.HookAdapter import me.rhunk.snapenhance.hook.HookAdapter
@ -47,7 +48,7 @@ import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
@OptIn(ExperimentalEncodingApi::class) @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 lastSeenMediaInfoMap: MutableMap<SplitMediaAssetType, MediaInfo>? = null
private var lastSeenMapParams: ParamMap? = null private var lastSeenMapParams: ParamMap? = null
@ -230,7 +231,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val senderId = conversationMessage.senderId!! val senderId = conversationMessage.senderId!!
val conversationId = conversationMessage.clientConversationId!! val conversationId = conversationMessage.clientConversationId!!
if (!forceDownload && context.feature(AntiAutoDownload::class).isUserIgnored(senderId)) { if (!forceDownload && canUseRule(senderId)) {
return return
} }
@ -272,7 +273,7 @@ class MediaDownloader : Feature("MediaDownloader", loadParams = FeatureLoadParam
val author = context.database.getFriendInfo( val author = context.database.getFriendInfo(
if (storyUserId == null || storyUserId == "null") if (storyUserId == null || storyUserId == "null")
context.database.getMyUserId()!! context.database.myUserId
else storyUserId else storyUserId
) ?: throw Exception("Friend not found in database") ) ?: throw Exception("Friend not found in database")
val authorName = author.usernameForSorting!! val authorName = author.usernameForSorting!!

View File

@ -37,8 +37,6 @@ class MessageLogger : Feature("MessageLogger",
private val fetchedMessages = mutableListOf<Long>() private val fetchedMessages = mutableListOf<Long>()
private val deletedMessageCache = mutableMapOf<Long, JsonObject>() 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 isMessageRemoved(conversationId: String, orderKey: Long) = deletedMessageCache.containsKey(computeMessageIdentifier(conversationId, orderKey))
fun deleteMessage(conversationId: String, clientMessageId: Long) { fun deleteMessage(conversationId: String, clientMessageId: Long) {
@ -83,7 +81,7 @@ class MessageLogger : Feature("MessageLogger",
if (message.messageState != MessageState.COMMITTED) return if (message.messageState != MessageState.COMMITTED) return
//exclude messages sent by me //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 conversationId = message.messageDescriptor.conversationId.toString()
val serverIdentifier = computeMessageIdentifier(conversationId, message.orderKey) 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 preventReadReceipts by context.config.messaging.preventReadReceipts
val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{ val isConversationInStealthMode: (SnapUUID) -> Boolean = hook@{
if (preventReadReceipts) return@hook true 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 -> arrayOf("mediaMessagesDisplayed", "displayedMessages").forEach { methodName: String ->

View File

@ -1,19 +1,6 @@
package me.rhunk.snapenhance.features.impl.spying package me.rhunk.snapenhance.features.impl.spying
import me.rhunk.snapenhance.bridge.types.BridgeFileType import me.rhunk.snapenhance.core.messaging.MessagingRuleType
import me.rhunk.snapenhance.features.BridgeFileFeature import me.rhunk.snapenhance.features.MessagingRuleFeature
import me.rhunk.snapenhance.features.FeatureLoadParams
class StealthMode : BridgeFileFeature("StealthMode", BridgeFileType.STEALTH, loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) { class StealthMode : MessagingRuleFeature("StealthMode", MessagingRuleType.STEALTH)
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))
}
}

View File

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

View File

@ -154,7 +154,7 @@ class Notifications : Feature("Notifications", loadParams = FeatureLoadParams.IN
val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input") val input = RemoteInput.getResultsFromIntent(intent).getCharSequence("chat_reply_input")
.toString() .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") cachedMessages.computeIfAbsent(conversationId) { mutableListOf() }.add("${myUser.displayName}: $input")
updateNotification(notificationId) { notification -> 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.AutoUpdater
import me.rhunk.snapenhance.features.impl.ConfigurationOverride import me.rhunk.snapenhance.features.impl.ConfigurationOverride
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.MediaDownloader import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode
import me.rhunk.snapenhance.features.impl.experiments.AppPasscode import me.rhunk.snapenhance.features.impl.experiments.AppPasscode
@ -76,7 +75,6 @@ class FeatureManager(private val context: ModContext) : Manager {
register(AutoSave::class) register(AutoSave::class)
register(UITweaks::class) register(UITweaks::class)
register(ConfigurationOverride::class) register(ConfigurationOverride::class)
register(AntiAutoDownload::class)
register(GalleryMediaSendOverride::class) register(GalleryMediaSendOverride::class)
register(AntiAutoSave::class) register(AntiAutoSave::class)
register(UnlimitedSnapViewTime::class) register(UnlimitedSnapViewTime::class)

View File

@ -4,32 +4,23 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory 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.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.Gravity
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.CompoundButton import android.widget.CompoundButton
import android.widget.LinearLayout
import android.widget.Switch import android.widget.Switch
import android.widget.Toast
import me.rhunk.snapenhance.Logger import me.rhunk.snapenhance.Logger
import me.rhunk.snapenhance.data.ContentType 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.ConversationMessage
import me.rhunk.snapenhance.database.objects.FriendInfo import me.rhunk.snapenhance.database.objects.FriendInfo
import me.rhunk.snapenhance.database.objects.UserConversationLink 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.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.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.ViewAppearanceHelper
import me.rhunk.snapenhance.ui.menu.AbstractMenu import me.rhunk.snapenhance.ui.menu.AbstractMenu
import me.rhunk.snapenhance.util.snap.BitmojiSelfie import me.rhunk.snapenhance.util.snap.BitmojiSelfie
@ -106,17 +97,18 @@ class FriendFeedInfoMenu : AbstractMenu() {
context.config.messaging.messagePreviewLength.get() context.config.messaging.messagePreviewLength.get()
)?.reversed() )?.reversed()
if (messages.isNullOrEmpty()) { if (messages == null) {
Toast.makeText(androidCtx, "No messages found", Toast.LENGTH_SHORT).show() context.longToast("Can't fetch messages")
return return
} }
val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!! val participants: Map<String, FriendInfo> = context.database.getConversationParticipants(conversationId)!!
.map { context.database.getFriendInfo(it)!! } .map { context.database.getFriendInfo(it)!! }
.associateBy { it.userId!! } .associateBy { it.userId!! }
val messageBuilder = StringBuilder() val messageBuilder = StringBuilder()
messages.forEach{ message: ConversationMessage -> messages.forEach { message ->
val sender: FriendInfo? = participants[message.senderId] val sender: FriendInfo? = participants[message.senderId]
var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.contentType).name var messageString: String = message.getMessageAsString() ?: ContentType.fromId(message.contentType).name
@ -164,7 +156,6 @@ class FriendFeedInfoMenu : AbstractMenu() {
.append("\n") .append("\n")
} }
//alert dialog //alert dialog
val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) val builder = ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity)
builder.setTitle(context.translation["conversation_preview.title"]) builder.setTitle(context.translation["conversation_preview.title"])
@ -182,22 +173,6 @@ class FriendFeedInfoMenu : AbstractMenu() {
builder.show() 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?> { private fun getCurrentConversationInfo(): Pair<String, String?> {
val messaging = context.feature(Messaging::class) val messaging = context.feature(Messaging::class)
val focusedConversationTargetUser: String? = messaging.lastFetchConversationUserUUID?.toString() val focusedConversationTargetUser: String? = messaging.lastFetchConversationUserUUID?.toString()
@ -213,7 +188,7 @@ class FriendFeedInfoMenu : AbstractMenu() {
//old conversation fetch //old conversation fetch
val conversationId = if (messaging.lastFetchConversationUUID == null && focusedConversationTargetUser != null) { 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() conversation.clientConversationId!!.trim().lowercase()
} else { } else {
messaging.lastFetchConversationUUID.toString() messaging.lastFetchConversationUUID.toString()
@ -244,148 +219,38 @@ class FriendFeedInfoMenu : AbstractMenu() {
val (conversationId, targetUser) = getCurrentConversationInfo() val (conversationId, targetUser) = getCurrentConversationInfo()
if (!context.config.userInterface.enableFriendFeedMenuBar.get()) { val previewButton = Button(viewModel.context).apply {
//preview button text = modContext.translation["friend_menu_option.preview"]
val previewButton = Button(viewModel.context).apply { ViewAppearanceHelper.applyTheme(this, viewModel.width)
text = modContext.translation["friend_menu_option.preview"] setOnClickListener {
ViewAppearanceHelper.applyTheme(this, viewModel.width) showPreview(
setOnClickListener { targetUser,
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(
conversationId, conversationId,
!isChecked context
) )
} }
} }
if (friendFeedMenuOptions.contains("conversation_info")) { if (friendFeedMenuOptions.contains("conversation_info")) {
//user viewConsumer(previewButton)
createActionButton("\uD83D\uDC64") {
showPreview(
targetUser,
conversationId,
viewModel.context
)
}
} }
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) }
)
}
} }
} }