feat: friend tracker (#969)
Co-authored-by: rhunk <101876869+rhunk@users.noreply.github.com> Co-authored-by: Jacob Thomas <41988041+bocajthomas@users.noreply.github.com>
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
"home_logs": "Logs",
|
||||
"logger_history": "Logger History",
|
||||
"logged_stories": "Logged Stories",
|
||||
"friend_tracker": "Friend Tracker",
|
||||
"edit_rule": "Edit Rule",
|
||||
"social": "Social",
|
||||
"manage_scope": "Manage Scope",
|
||||
"messaging_preview": "Preview",
|
||||
@@ -878,20 +880,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"session_events": {
|
||||
"name": "Session Events",
|
||||
"description": "Records session events",
|
||||
"properties": {
|
||||
"capture_duplex_events": {
|
||||
"name": "Capture Duplex Events",
|
||||
"description": "Capture presence and messaging events when a session is active"
|
||||
},
|
||||
"allow_running_in_background": {
|
||||
"name": "Allow Running in Background",
|
||||
"description": "Allows session to run in the background"
|
||||
}
|
||||
}
|
||||
},
|
||||
"spoof": {
|
||||
"name": "Spoof",
|
||||
"description": "Spoof various information about you",
|
||||
@@ -1039,6 +1027,20 @@
|
||||
"description": "Disables the anonymization of logs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"friend_tracker": {
|
||||
"name": "Friend Tracker",
|
||||
"description": "Records friend's activity on Snapchat",
|
||||
"properties": {
|
||||
"record_messaging_events": {
|
||||
"name": "Record Messaging Events",
|
||||
"description": "Records messaging events such as sending a opening a snap, reading a message, etc."
|
||||
},
|
||||
"allow_running_in_background": {
|
||||
"name": "Allow Running in Background",
|
||||
"description": "Allows the tracker to run in the background. Note: This will significantly drain your battery"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
package me.rhunk.snapenhance.common.action
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Chat
|
||||
import androidx.compose.material.icons.filled.CleaningServices
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material.icons.filled.PersonOutline
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
|
||||
enum class EnumAction(
|
||||
val key: String,
|
||||
val icon: ImageVector,
|
||||
val exitOnFinish: Boolean = false,
|
||||
) {
|
||||
EXPORT_CHAT_MESSAGES("export_chat_messages"),
|
||||
EXPORT_MEMORIES("export_memories"),
|
||||
BULK_MESSAGING_ACTION("bulk_messaging_action"),
|
||||
MANAGE_FRIEND_LIST("manage_friend_list"),
|
||||
CLEAN_CACHE("clean_snapchat_cache", exitOnFinish = true);
|
||||
EXPORT_CHAT_MESSAGES("export_chat_messages", Icons.AutoMirrored.Default.Chat),
|
||||
EXPORT_MEMORIES("export_memories", Icons.Default.Image),
|
||||
BULK_MESSAGING_ACTION("bulk_messaging_action", Icons.Default.DeleteOutline),
|
||||
CLEAN_CACHE("clean_snapchat_cache", Icons.Default.CleaningServices, exitOnFinish = true),
|
||||
MANAGE_FRIEND_LIST("manage_friend_list", Icons.Default.PersonOutline);
|
||||
|
||||
companion object {
|
||||
const val ACTION_PARAMETER = "se_action"
|
||||
|
||||
@@ -2,25 +2,34 @@ package me.rhunk.snapenhance.common.bridge.wrapper
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonObject
|
||||
import kotlinx.coroutines.*
|
||||
import me.rhunk.snapenhance.bridge.logger.LoggerInterface
|
||||
import me.rhunk.snapenhance.common.data.StoryData
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper
|
||||
import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull
|
||||
import me.rhunk.snapenhance.common.util.ktx.getIntOrNull
|
||||
import me.rhunk.snapenhance.common.util.ktx.getLongOrNull
|
||||
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
|
||||
import me.rhunk.snapenhance.common.util.protobuf.ProtoReader
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
class LoggedMessageEdit(
|
||||
val timestamp: Long,
|
||||
val messageText: String
|
||||
)
|
||||
|
||||
class LoggedMessage(
|
||||
val messageId: Long,
|
||||
val timestamp: Long,
|
||||
val messageData: ByteArray
|
||||
val messageData: ByteArray,
|
||||
)
|
||||
|
||||
class TrackerLog(
|
||||
val id: Int,
|
||||
val timestamp: Long,
|
||||
val conversationId: String,
|
||||
val conversationTitle: String?,
|
||||
@@ -37,6 +46,7 @@ class LoggerWrapper(
|
||||
private var _database: SQLiteDatabase? = null
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
|
||||
private val gson by lazy { GsonBuilder().create() }
|
||||
|
||||
private val database get() = synchronized(this) {
|
||||
_database?.takeIf { it.isOpen } ?: run {
|
||||
@@ -50,6 +60,14 @@ class LoggerWrapper(
|
||||
"message_id BIGINT",
|
||||
"message_data BLOB"
|
||||
),
|
||||
"chat_edits" to listOf(
|
||||
"id INTEGER PRIMARY KEY",
|
||||
"edit_number INTEGER",
|
||||
"added_timestamp BIGINT",
|
||||
"conversation_id VARCHAR",
|
||||
"message_id BIGINT",
|
||||
"message_text BLOB"
|
||||
),
|
||||
"stories" to listOf(
|
||||
"id INTEGER PRIMARY KEY",
|
||||
"added_timestamp BIGINT",
|
||||
@@ -111,18 +129,66 @@ class LoggerWrapper(
|
||||
}
|
||||
|
||||
override fun addMessage(conversationId: String, messageId: Long, serializedMessage: ByteArray) {
|
||||
val cursor = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
|
||||
val state = cursor.moveToFirst()
|
||||
cursor.close()
|
||||
if (state) return
|
||||
val hasMessage = database.rawQuery("SELECT message_id FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString())).use {
|
||||
it.moveToFirst()
|
||||
it.count > 0
|
||||
}
|
||||
|
||||
if (!hasMessage) {
|
||||
runBlocking {
|
||||
withContext(coroutineScope.coroutineContext) {
|
||||
database.insert("messages", null, ContentValues().apply {
|
||||
put("added_timestamp", System.currentTimeMillis())
|
||||
put("conversation_id", conversationId)
|
||||
put("message_id", messageId)
|
||||
put("message_data", serializedMessage)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle message edits
|
||||
runBlocking {
|
||||
withContext(coroutineScope.coroutineContext) {
|
||||
database.insert("messages", null, ContentValues().apply {
|
||||
put("added_timestamp", System.currentTimeMillis())
|
||||
put("conversation_id", conversationId)
|
||||
put("message_id", messageId)
|
||||
put("message_data", serializedMessage)
|
||||
})
|
||||
runCatching {
|
||||
val messageObject = gson.fromJson(
|
||||
serializedMessage.toString(Charsets.UTF_8),
|
||||
JsonObject::class.java
|
||||
)
|
||||
if (messageObject.getAsJsonObject("mMessageContent")
|
||||
?.getAsJsonPrimitive("mContentType")?.asString != "CHAT"
|
||||
) return@withContext
|
||||
|
||||
val metadata = messageObject.getAsJsonObject("mMetadata")
|
||||
if (metadata.get("mIsEdited")?.asBoolean != true) return@withContext
|
||||
|
||||
val messageTextContent =
|
||||
messageObject.getAsJsonObject("mMessageContent")?.getAsJsonArray("mContent")
|
||||
?.map { it.asByte }?.toByteArray()?.let {
|
||||
ProtoReader(it).getString(2, 1)
|
||||
} ?: return@withContext
|
||||
|
||||
database.rawQuery(
|
||||
"SELECT MAX(edit_number), message_text FROM chat_edits WHERE conversation_id = ? AND message_id = ?",
|
||||
arrayOf(conversationId, messageId.toString())
|
||||
).use {
|
||||
it.moveToFirst()
|
||||
val editNumber = it.getInt(0)
|
||||
val lastEditedMessage = it.getString(1)
|
||||
|
||||
if (lastEditedMessage == messageTextContent) return@withContext
|
||||
|
||||
database.insert("chat_edits", null, ContentValues().apply {
|
||||
put("edit_number", editNumber + 1)
|
||||
put("added_timestamp", System.currentTimeMillis())
|
||||
put("conversation_id", conversationId)
|
||||
put("message_id", messageId)
|
||||
put("message_text", messageTextContent)
|
||||
})
|
||||
}
|
||||
}.onFailure {
|
||||
AbstractLogger.directDebug("Failed to handle message edit: ${it.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,9 +198,11 @@ class LoggerWrapper(
|
||||
maxAge?.let {
|
||||
val maxTime = System.currentTimeMillis() - it
|
||||
database.execSQL("DELETE FROM messages WHERE added_timestamp < ?", arrayOf(maxTime.toString()))
|
||||
database.execSQL("DELETE FROM chat_edits WHERE added_timestamp < ?", arrayOf(maxTime.toString()))
|
||||
database.execSQL("DELETE FROM stories WHERE added_timestamp < ?", arrayOf(maxTime.toString()))
|
||||
} ?: run {
|
||||
database.execSQL("DELETE FROM messages")
|
||||
database.execSQL("DELETE FROM chat_edits")
|
||||
database.execSQL("DELETE FROM stories")
|
||||
}
|
||||
}
|
||||
@@ -157,6 +225,7 @@ class LoggerWrapper(
|
||||
override fun deleteMessage(conversationId: String, messageId: Long) {
|
||||
coroutineScope.launch {
|
||||
database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
|
||||
database.execSQL("DELETE FROM chat_edits WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +276,12 @@ class LoggerWrapper(
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteTrackerLog(id: Int) {
|
||||
coroutineScope.launch {
|
||||
database.execSQL("DELETE FROM tracker_events WHERE id = ?", arrayOf(id.toString()))
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogs(
|
||||
lastTimestamp: Long,
|
||||
filter: ((TrackerLog) -> Boolean)? = null
|
||||
@@ -215,6 +290,7 @@ class LoggerWrapper(
|
||||
val logs = mutableListOf<TrackerLog>()
|
||||
while (it.moveToNext() && logs.size < 50) {
|
||||
val log = TrackerLog(
|
||||
id = it.getIntOrNull("id") ?: continue,
|
||||
timestamp = it.getLongOrNull("timestamp") ?: continue,
|
||||
conversationId = it.getStringOrNull("conversation_id") ?: continue,
|
||||
conversationTitle = it.getStringOrNull("conversation_title"),
|
||||
@@ -278,6 +354,22 @@ class LoggerWrapper(
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessageEdits(conversationId: String, messageId: Long): List<LoggedMessageEdit> {
|
||||
val edits = mutableListOf<LoggedMessageEdit>()
|
||||
database.rawQuery(
|
||||
"SELECT added_timestamp, message_text FROM chat_edits WHERE conversation_id = ? AND message_id = ?",
|
||||
arrayOf(conversationId, messageId.toString())
|
||||
).use {
|
||||
while (it.moveToNext()) {
|
||||
edits.add(LoggedMessageEdit(
|
||||
timestamp = it.getLongOrNull("added_timestamp") ?: continue,
|
||||
messageText = it.getStringOrNull("message_text") ?: continue
|
||||
))
|
||||
}
|
||||
}
|
||||
return edits
|
||||
}
|
||||
|
||||
fun fetchMessages(
|
||||
conversationId: String,
|
||||
fromTimestamp: Long,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.rhunk.snapenhance.common.config
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import me.rhunk.snapenhance.common.bridge.wrapper.LocaleWrapper
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -35,7 +36,7 @@ class ConfigParams(
|
||||
private var _flags: Int? = null,
|
||||
private var _notices: Int? = null,
|
||||
|
||||
var icon: String? = null,
|
||||
var icon: ImageVector? = null,
|
||||
var disabledKey: String? = null,
|
||||
var customTranslationPath: String? = null,
|
||||
var customOptionTranslationPath: String? = null,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package me.rhunk.snapenhance.common.config.impl
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Fingerprint
|
||||
import androidx.compose.material.icons.filled.Memory
|
||||
import me.rhunk.snapenhance.common.config.ConfigContainer
|
||||
import me.rhunk.snapenhance.common.config.FeatureNotice
|
||||
|
||||
class Experimental : ConfigContainer() {
|
||||
class SessionEventsConfig : ConfigContainer(hasGlobalState = true) {
|
||||
val captureDuplexEvents = boolean("capture_duplex_events", true)
|
||||
val allowRunningInBackground = boolean("allow_running_in_background", true)
|
||||
}
|
||||
|
||||
class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) {
|
||||
val showFirstCreatedUsername = boolean("show_first_created_username")
|
||||
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
|
||||
@@ -34,9 +32,8 @@ class Experimental : ConfigContainer() {
|
||||
val lockOnResume = boolean("lock_on_resume", defaultValue = true)
|
||||
}
|
||||
|
||||
val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory"; requireRestart() }
|
||||
val sessionEvents = container("session_events", SessionEventsConfig()) { requireRestart(); nativeHooks() }
|
||||
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
|
||||
val nativeHooks = container("native_hooks", NativeHooks()) { icon = Icons.Default.Memory; requireRestart() }
|
||||
val spoof = container("spoof", Spoof()) { icon = Icons.Default.Fingerprint ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
|
||||
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
|
||||
val newChatActionMenu = boolean("new_chat_action_menu") { requireRestart() }
|
||||
val mediaFilePicker = boolean("media_file_picker") { requireRestart(); addNotices(FeatureNotice.UNSTABLE) }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package me.rhunk.snapenhance.common.config.impl
|
||||
|
||||
import me.rhunk.snapenhance.common.config.ConfigContainer
|
||||
|
||||
class FriendTrackerConfig: ConfigContainer(hasGlobalState = true) {
|
||||
val recordMessagingEvents = boolean("record_messaging_events", false)
|
||||
val allowRunningInBackground = boolean("allow_running_in_background", false)
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
package me.rhunk.snapenhance.common.config.impl
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Rule
|
||||
import androidx.compose.material.icons.automirrored.filled.Send
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import me.rhunk.snapenhance.common.config.ConfigContainer
|
||||
import me.rhunk.snapenhance.common.config.FeatureNotice
|
||||
|
||||
class RootConfig : ConfigContainer() {
|
||||
val downloader = container("downloader", DownloaderConfig()) { icon = "Download"}
|
||||
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"; requireRestart() }
|
||||
val streaksReminder = container("streaks_reminder", StreaksReminderConfig()) { icon = "Alarm" }
|
||||
val experimental = container("experimental", Experimental()) { icon = "Science"; addNotices(
|
||||
val downloader = container("downloader", DownloaderConfig()) { icon = Icons.Default.Download }
|
||||
val userInterface = container("user_interface", UserInterfaceTweaks()) { icon = Icons.Default.RemoveRedEye }
|
||||
val messaging = container("messaging", MessagingTweaks()) { icon = Icons.AutoMirrored.Default.Send }
|
||||
val global = container("global", Global()) { icon = Icons.Default.MiscellaneousServices }
|
||||
val rules = container("rules", Rules()) { icon = Icons.AutoMirrored.Default.Rule }
|
||||
val camera = container("camera", Camera()) { icon = Icons.Default.Camera; requireRestart() }
|
||||
val streaksReminder = container("streaks_reminder", StreaksReminderConfig()) { icon = Icons.Default.Alarm }
|
||||
val experimental = container("experimental", Experimental()) { icon = Icons.Default.Science; addNotices(
|
||||
FeatureNotice.UNSTABLE) }
|
||||
val scripting = container("scripting", Scripting()) { icon = "DataObject" }
|
||||
val scripting = container("scripting", Scripting()) { icon = Icons.Default.DataObject }
|
||||
val friendTracker = container("friend_tracker", FriendTrackerConfig()) { icon = Icons.Default.PersonSearch; nativeHooks() }
|
||||
}
|
||||
@@ -35,6 +35,7 @@ enum class SessionEventType(
|
||||
MESSAGE_DELETED("message_deleted"),
|
||||
MESSAGE_SAVED("message_saved"),
|
||||
MESSAGE_UNSAVED("message_unsaved"),
|
||||
MESSAGE_EDITED("message_edited"),
|
||||
MESSAGE_REACTION_ADD("message_reaction_add"),
|
||||
MESSAGE_REACTION_REMOVE("message_reaction_remove"),
|
||||
SNAP_OPENED("snap_opened"),
|
||||
@@ -44,70 +45,6 @@ enum class SessionEventType(
|
||||
SNAP_SCREEN_RECORD("snap_screen_record"),
|
||||
}
|
||||
|
||||
object TrackerFlags {
|
||||
const val TRACK = 1
|
||||
const val LOG = 2
|
||||
const val NOTIFY = 4
|
||||
const val APP_IS_ACTIVE = 8
|
||||
const val APP_IS_INACTIVE = 16
|
||||
const val IS_IN_CONVERSATION = 32
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
class TrackerEventsResult(
|
||||
private val rules: Map<TrackerRule, List<TrackerRuleEvent>>
|
||||
): Parcelable {
|
||||
fun hasFlags(vararg flags: Int): Boolean {
|
||||
return rules.any { (_, ruleEvents) ->
|
||||
ruleEvents.any { flags.all { flag -> it.flags and flag != 0 } }
|
||||
}
|
||||
}
|
||||
|
||||
fun canTrackOn(conversationId: String?, userId: String?): Boolean {
|
||||
return rules.any t@{ (rule, ruleEvents) ->
|
||||
ruleEvents.any { event ->
|
||||
if (event.flags and TrackerFlags.TRACK == 0) {
|
||||
return@any false
|
||||
}
|
||||
|
||||
// global rule
|
||||
if (rule.conversationId == null && rule.userId == null) {
|
||||
return@any true
|
||||
}
|
||||
|
||||
// user rule
|
||||
if (rule.conversationId == null && rule.userId == userId) {
|
||||
return@any true
|
||||
}
|
||||
|
||||
// conversation rule
|
||||
if (rule.conversationId == conversationId && rule.userId == null) {
|
||||
return@any true
|
||||
}
|
||||
|
||||
// conversation and user rule
|
||||
return@any rule.conversationId == conversationId && rule.userId == userId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class TrackerRule(
|
||||
val id: Int,
|
||||
val flags: Int,
|
||||
val conversationId: String?,
|
||||
val userId: String?
|
||||
): Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class TrackerRuleEvent(
|
||||
val id: Int,
|
||||
val flags: Int,
|
||||
val eventType: String,
|
||||
): Parcelable
|
||||
|
||||
enum class TrackerEventType(
|
||||
val key: String
|
||||
) {
|
||||
@@ -126,6 +63,7 @@ enum class TrackerEventType(
|
||||
MESSAGE_DELETED("message_deleted"),
|
||||
MESSAGE_SAVED("message_saved"),
|
||||
MESSAGE_UNSAVED("message_unsaved"),
|
||||
MESSAGE_EDITED("message_edited"),
|
||||
MESSAGE_REACTION_ADD("message_reaction_add"),
|
||||
MESSAGE_REACTION_REMOVE("message_reaction_remove"),
|
||||
SNAP_OPENED("snap_opened"),
|
||||
@@ -134,3 +72,104 @@ enum class TrackerEventType(
|
||||
SNAP_SCREENSHOT("snap_screenshot"),
|
||||
SNAP_SCREEN_RECORD("snap_screen_record"),
|
||||
}
|
||||
|
||||
|
||||
@Parcelize
|
||||
class TrackerEventsResult(
|
||||
val rules: Map<ScopedTrackerRule, List<TrackerRuleEvent>>,
|
||||
): Parcelable {
|
||||
fun getActions(): Map<TrackerRuleAction, TrackerRuleActionParams> {
|
||||
return rules.flatMap {
|
||||
it.value
|
||||
}.fold(mutableMapOf()) { acc, ruleEvent ->
|
||||
ruleEvent.actions.forEach { action ->
|
||||
acc[action] = acc[action]?.merge(ruleEvent.params) ?: ruleEvent.params
|
||||
}
|
||||
acc
|
||||
}
|
||||
}
|
||||
|
||||
fun canTrackOn(conversationId: String?, userId: String?): Boolean {
|
||||
return rules.any { (scopedRule, events) ->
|
||||
if (!events.any { it.enabled }) return@any false
|
||||
val scopes = scopedRule.scopes
|
||||
|
||||
when (scopes[userId]) {
|
||||
TrackerScopeType.WHITELIST -> return@any true
|
||||
TrackerScopeType.BLACKLIST -> return@any false
|
||||
else -> {}
|
||||
}
|
||||
|
||||
when (scopes[conversationId]) {
|
||||
TrackerScopeType.WHITELIST -> return@any true
|
||||
TrackerScopeType.BLACKLIST -> return@any false
|
||||
else -> {}
|
||||
}
|
||||
|
||||
return@any scopes.isEmpty() || scopes.any { it.value == TrackerScopeType.BLACKLIST }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class TrackerRuleAction(
|
||||
val key: String
|
||||
) {
|
||||
LOG("log"),
|
||||
IN_APP_NOTIFICATION("in_app_notification"),
|
||||
PUSH_NOTIFICATION("push_notification"),
|
||||
CUSTOM("custom");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): TrackerRuleAction? {
|
||||
return entries.find { it.key == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class TrackerRuleActionParams(
|
||||
var onlyInsideConversation: Boolean = false,
|
||||
var onlyOutsideConversation: Boolean = false,
|
||||
var onlyWhenAppActive: Boolean = false,
|
||||
var onlyWhenAppInactive: Boolean = false,
|
||||
var noPushNotificationWhenAppActive: Boolean = false,
|
||||
): Parcelable {
|
||||
fun merge(other: TrackerRuleActionParams): TrackerRuleActionParams {
|
||||
return TrackerRuleActionParams(
|
||||
onlyInsideConversation = onlyInsideConversation || other.onlyInsideConversation,
|
||||
onlyOutsideConversation = onlyOutsideConversation || other.onlyOutsideConversation,
|
||||
onlyWhenAppActive = onlyWhenAppActive || other.onlyWhenAppActive,
|
||||
onlyWhenAppInactive = onlyWhenAppInactive || other.onlyWhenAppInactive,
|
||||
noPushNotificationWhenAppActive = noPushNotificationWhenAppActive || other.noPushNotificationWhenAppActive,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class TrackerRule(
|
||||
val id: Int,
|
||||
val enabled: Boolean,
|
||||
val name: String,
|
||||
): Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class ScopedTrackerRule(
|
||||
val rule: TrackerRule,
|
||||
val scopes: Map<String, TrackerScopeType>
|
||||
): Parcelable
|
||||
|
||||
enum class TrackerScopeType(
|
||||
val key: String
|
||||
) {
|
||||
WHITELIST("whitelist"),
|
||||
BLACKLIST("blacklist");
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class TrackerRuleEvent(
|
||||
val id: Int,
|
||||
val enabled: Boolean,
|
||||
val eventType: String,
|
||||
val params: TrackerRuleActionParams,
|
||||
val actions: List<TrackerRuleAction>
|
||||
): Parcelable
|
||||
|
||||
@@ -4,8 +4,8 @@ import android.os.Handler
|
||||
import android.widget.Toast
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext
|
||||
import me.rhunk.snapenhance.common.scripting.impl.Networking
|
||||
import me.rhunk.snapenhance.common.scripting.impl.JavaInterfaces
|
||||
import me.rhunk.snapenhance.common.scripting.impl.Networking
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.contextScope
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.putFunction
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.scriptable
|
||||
@@ -18,13 +18,14 @@ import org.mozilla.javascript.NativeJavaObject
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import org.mozilla.javascript.Undefined
|
||||
import org.mozilla.javascript.Wrapper
|
||||
import java.io.Reader
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class JSModule(
|
||||
val scriptRuntime: ScriptRuntime,
|
||||
private val scriptRuntime: ScriptRuntime,
|
||||
val moduleInfo: ModuleInfo,
|
||||
val content: String,
|
||||
private val reader: Reader,
|
||||
) {
|
||||
private val moduleBindings = mutableMapOf<String, AbstractBinding>()
|
||||
private lateinit var moduleObject: ScriptableObject
|
||||
@@ -53,6 +54,18 @@ class JSModule(
|
||||
})
|
||||
})
|
||||
|
||||
scriptRuntime.logger.apply {
|
||||
moduleObject.putConst("console", moduleObject, scriptableObject {
|
||||
putFunction("log") { info(argsToString(it)) }
|
||||
putFunction("warn") { warn(argsToString(it)) }
|
||||
putFunction("error") { error(argsToString(it)) }
|
||||
putFunction("debug") { debug(argsToString(it)) }
|
||||
putFunction("info") { info(argsToString(it)) }
|
||||
putFunction("trace") { verbose(argsToString(it)) }
|
||||
putFunction("verbose") { verbose(argsToString(it)) }
|
||||
})
|
||||
}
|
||||
|
||||
registerBindings(
|
||||
JavaInterfaces(),
|
||||
InterfaceManager(),
|
||||
@@ -186,7 +199,7 @@ class JSModule(
|
||||
}
|
||||
|
||||
contextScope(shouldOptimize = true) {
|
||||
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
|
||||
evaluateReader(moduleObject, reader, moduleInfo.name, 1, null)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +246,10 @@ class JSModule(
|
||||
private fun argsToString(args: Array<out Any?>?): String {
|
||||
return args?.joinToString(" ") {
|
||||
when (it) {
|
||||
is Wrapper -> it.unwrap().toString()
|
||||
is Wrapper -> it.unwrap().let { value ->
|
||||
if (value is Throwable) value.message + "\n" + value.stackTraceToString()
|
||||
else value.toString()
|
||||
}
|
||||
else -> it.toString()
|
||||
}
|
||||
} ?: "null"
|
||||
|
||||
@@ -3,11 +3,11 @@ package me.rhunk.snapenhance.common.scripting
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||
import me.rhunk.snapenhance.common.BuildConfig
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
|
||||
open class ScriptRuntime(
|
||||
@@ -35,7 +35,7 @@ open class ScriptRuntime(
|
||||
return modules.values.find { it.moduleInfo.name == name }
|
||||
}
|
||||
|
||||
private fun readModuleInfo(reader: BufferedReader): ModuleInfo {
|
||||
fun readModuleInfo(reader: BufferedReader): ModuleInfo {
|
||||
val header = reader.readLine()
|
||||
if (!header.startsWith("// ==SE_module==")) {
|
||||
throw Exception("Invalid module header")
|
||||
@@ -74,6 +74,10 @@ open class ScriptRuntime(
|
||||
return readModuleInfo(inputStream.bufferedReader())
|
||||
}
|
||||
|
||||
fun removeModule(scriptPath: String) {
|
||||
modules.remove(scriptPath)
|
||||
}
|
||||
|
||||
fun unload(scriptPath: String) {
|
||||
val module = modules[scriptPath] ?: return
|
||||
logger.info("Unloading module $scriptPath")
|
||||
@@ -81,27 +85,30 @@ open class ScriptRuntime(
|
||||
modules.remove(scriptPath)
|
||||
}
|
||||
|
||||
fun load(scriptPath: String, pfd: ParcelFileDescriptor) {
|
||||
load(scriptPath, ParcelFileDescriptor.AutoCloseInputStream(pfd).use {
|
||||
it.readBytes().toString(Charsets.UTF_8)
|
||||
})
|
||||
fun load(scriptPath: String, pfd: ParcelFileDescriptor): JSModule {
|
||||
return ParcelFileDescriptor.AutoCloseInputStream(pfd).use {
|
||||
load(scriptPath, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun load(scriptPath: String, content: String): JSModule? {
|
||||
fun load(scriptPath: String, content: InputStream): JSModule {
|
||||
logger.info("Loading module $scriptPath")
|
||||
return runCatching {
|
||||
JSModule(
|
||||
scriptRuntime = this,
|
||||
moduleInfo = readModuleInfo(ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)).bufferedReader()),
|
||||
content = content,
|
||||
).apply {
|
||||
load {
|
||||
buildModuleObject(this, this@apply)
|
||||
}
|
||||
modules[scriptPath] = this
|
||||
val bufferedReader = content.bufferedReader()
|
||||
val moduleInfo = readModuleInfo(bufferedReader)
|
||||
|
||||
if (moduleInfo.minSEVersion != null && moduleInfo.minSEVersion > BuildConfig.VERSION_CODE) {
|
||||
throw Exception("Module requires a newer version of SnapEnhance (min version: ${moduleInfo.minSEVersion})")
|
||||
}
|
||||
|
||||
return JSModule(
|
||||
scriptRuntime = this,
|
||||
moduleInfo = moduleInfo,
|
||||
reader = bufferedReader,
|
||||
).apply {
|
||||
load {
|
||||
buildModuleObject(this, this@apply)
|
||||
}
|
||||
}.onFailure {
|
||||
logger.error("Failed to load module $scriptPath", it)
|
||||
}.getOrNull()
|
||||
modules[scriptPath] = this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package me.rhunk.snapenhance.common.ui
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class AsyncUpdateDispatcher(
|
||||
val updateOnFirstComposition: Boolean = true
|
||||
) {
|
||||
private val callbacks = CopyOnWriteArrayList<suspend () -> Unit>()
|
||||
|
||||
suspend fun dispatch() {
|
||||
callbacks.forEach { it() }
|
||||
}
|
||||
|
||||
fun addCallback(callback: suspend () -> Unit) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
fun removeCallback(callback: suspend () -> Unit) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberAsyncUpdateDispatcher(): AsyncUpdateDispatcher {
|
||||
return remember { AsyncUpdateDispatcher() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> rememberCommonState(
|
||||
initialState: () -> T,
|
||||
setter: suspend T.() -> Unit,
|
||||
updateDispatcher: AsyncUpdateDispatcher? = null,
|
||||
keys: Array<*> = emptyArray<Any>(),
|
||||
): T {
|
||||
return remember { initialState() }.apply {
|
||||
var asyncSetCallback by remember { mutableStateOf(suspend {}) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
asyncSetCallback = { setter(this@apply) }
|
||||
updateDispatcher?.addCallback(asyncSetCallback)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { updateDispatcher?.removeCallback(asyncSetCallback) }
|
||||
}
|
||||
|
||||
if (updateDispatcher?.updateOnFirstComposition != false) {
|
||||
LaunchedEffect(*keys) {
|
||||
setter(this@apply)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> rememberAsyncMutableState(
|
||||
defaultValue: T,
|
||||
updateDispatcher: AsyncUpdateDispatcher? = null,
|
||||
keys: Array<*> = emptyArray<Any>(),
|
||||
getter: () -> T,
|
||||
): MutableState<T> {
|
||||
return rememberCommonState(
|
||||
initialState = { mutableStateOf(defaultValue) },
|
||||
setter = {
|
||||
withContext(Dispatchers.Main) {
|
||||
value = withContext(Dispatchers.IO) {
|
||||
getter()
|
||||
}
|
||||
}
|
||||
},
|
||||
updateDispatcher = updateDispatcher,
|
||||
keys = keys,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> rememberAsyncMutableStateList(
|
||||
defaultValue: List<T>,
|
||||
updateDispatcher: AsyncUpdateDispatcher? = null,
|
||||
keys: Array<*> = emptyArray<Any>(),
|
||||
getter: () -> List<T>,
|
||||
): SnapshotStateList<T> {
|
||||
return rememberCommonState(
|
||||
initialState = { mutableStateListOf<T>().apply {
|
||||
addAll(defaultValue)
|
||||
}},
|
||||
setter = {
|
||||
withContext(Dispatchers.Main) {
|
||||
clear()
|
||||
addAll(withContext(Dispatchers.IO) {
|
||||
getter()
|
||||
})
|
||||
}
|
||||
},
|
||||
updateDispatcher = updateDispatcher,
|
||||
keys = keys,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ object BitmojiSelfie {
|
||||
return when (type) {
|
||||
BitmojiSelfieType.STANDARD -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?transparent=1"
|
||||
BitmojiSelfieType.THREE_D -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?trim=circle"
|
||||
BitmojiSelfieType.NEW_THREE_D -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?trim=circle&ua=1"
|
||||
BitmojiSelfieType.NEW_THREE_D -> "${type.prefixUrl}$selfieId-$avatarId-v1.webp?trim=circle&ua=2"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user