From f2e49e93fbd3df5b1a9e70601b33c38b9256445f Mon Sep 17 00:00:00 2001 From: rhunk <101876869+rhunk@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:06:56 +0200 Subject: [PATCH] feat(social): auto sync scope --- .../rhunk/snapenhance/bridge/BridgeService.kt | 51 +++++--- .../sections/social/AddFriendDialog.kt | 5 +- .../snapenhance/bridge/BridgeInterface.aidl | 5 + .../me/rhunk/snapenhance/EventDispatcher.kt | 3 +- .../snapenhance/core/bridge/BridgeClient.kt | 20 +--- .../core/bridge/types/BridgeFileType.kt | 3 +- .../impl/SendMessageWithContentEvent.kt | 17 ++- .../core/messaging/MessagingCoreObjects.kt | 6 +- .../snapenhance/features/impl/ScopeSync.kt | 42 +++++++ .../features/impl/tweaks/SnapchatPlus.kt | 2 +- .../me/rhunk/snapenhance/hook/HookAdapter.kt | 6 +- .../manager/impl/FeatureManager.kt | 110 ++++++++---------- 12 files changed, 165 insertions(+), 105 deletions(-) create mode 100644 core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ScopeSync.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt index a18d7574..7cabff84 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -13,6 +13,7 @@ import me.rhunk.snapenhance.core.database.objects.FriendInfo import me.rhunk.snapenhance.core.logger.LogLevel import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.core.messaging.SocialScope import me.rhunk.snapenhance.core.util.SerializableDataObject import me.rhunk.snapenhance.download.DownloadProcessor import kotlin.system.measureTimeMillis @@ -33,25 +34,36 @@ class BridgeService : Service() { return BridgeBinder() } - fun triggerFriendSync(friendId: String) { - val syncedFriend = syncCallback.syncFriend(friendId) - if (syncedFriend == null) { - remoteSideContext.log.error("Failed to sync friend $friendId") - return + fun triggerScopeSync(scope: SocialScope, id: String, updateOnly: Boolean = false) { + val modDatabase = remoteSideContext.modDatabase + val syncedObject = when (scope) { + SocialScope.FRIEND -> { + if (updateOnly && modDatabase.getFriendInfo(id) == null) return + syncCallback.syncFriend(id) + } + SocialScope.GROUP -> { + if (updateOnly && modDatabase.getGroupInfo(id) == null) return + syncCallback.syncGroup(id) + } + else -> null } - SerializableDataObject.fromJson(syncedFriend).let { - remoteSideContext.modDatabase.syncFriend(it) - } - } - fun triggerGroupSync(groupId: String) { - val syncedGroup = syncCallback.syncGroup(groupId) - if (syncedGroup == null) { - remoteSideContext.log.error("Failed to sync group $groupId") + if (syncedObject == null) { + remoteSideContext.log.error("Failed to sync $scope $id") return } - SerializableDataObject.fromJson(syncedGroup).let { - remoteSideContext.modDatabase.syncGroupInfo(it) + + when (scope) { + SocialScope.FRIEND -> { + SerializableDataObject.fromJson(syncedObject).let { + modDatabase.syncFriend(it) + } + } + SocialScope.GROUP -> { + SerializableDataObject.fromJson(syncedObject).let { + modDatabase.syncGroupInfo(it) + } + } } } @@ -141,14 +153,14 @@ class BridgeService : Service() { measureTimeMillis { remoteSideContext.modDatabase.getFriends().map { it.userId } .forEach { friendId -> runCatching { - triggerFriendSync(friendId) + triggerScopeSync(SocialScope.FRIEND, friendId, true) }.onFailure { remoteSideContext.log.error("Failed to sync friend $friendId", it) } } remoteSideContext.modDatabase.getGroups().map { it.conversationId }.forEach { groupId -> runCatching { - triggerGroupSync(groupId) + triggerScopeSync(SocialScope.GROUP, groupId, true) }.onFailure { remoteSideContext.log.error("Failed to sync group $groupId", it) } @@ -158,6 +170,11 @@ class BridgeService : Service() { } } + override fun triggerSync(scope: String, id: String) { + remoteSideContext.log.verbose("trigger sync for $scope $id") + triggerScopeSync(SocialScope.getByName(scope), id, true) + } + override fun passGroupsAndFriends( groups: List, friends: List diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt index b3fc94ea..09ec563e 100644 --- a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt +++ b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/social/AddFriendDialog.kt @@ -47,6 +47,7 @@ import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo +import me.rhunk.snapenhance.core.messaging.SocialScope import me.rhunk.snapenhance.core.util.snap.SnapWidgetBroadcastReceiverHelper class AddFriendDialog( @@ -239,7 +240,7 @@ class AddFriendDialog( getCurrentState = { context.modDatabase.getGroupInfo(group.conversationId) != null } ) { state -> if (state) { - context.bridgeService.triggerGroupSync(group.conversationId) + context.bridgeService.triggerScopeSync(SocialScope.GROUP, group.conversationId) } else { context.modDatabase.deleteGroup(group.conversationId) } @@ -266,7 +267,7 @@ class AddFriendDialog( getCurrentState = { context.modDatabase.getFriendInfo(friend.userId) != null } ) { state -> if (state) { - context.bridgeService.triggerFriendSync(friend.userId) + context.bridgeService.triggerScopeSync(SocialScope.FRIEND, friend.userId) } else { context.modDatabase.deleteFriend(friend.userId) } diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl index 0b7d330f..68d8c465 100644 --- a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl +++ b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -80,6 +80,11 @@ interface BridgeInterface { */ oneway void sync(SyncCallback callback); + /** + * Trigger sync for an id + */ + void triggerSync(String scope, String id); + /** * Pass all groups and friends to be able to add them to the database * @param groups list of groups (MessagingGroupInfo as json string) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt index 1a918880..f91d8b42 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/EventDispatcher.kt @@ -26,7 +26,8 @@ class EventDispatcher( context.classCache.conversationManager.hook("sendMessageWithContent", HookStage.BEFORE) { param -> context.event.post(SendMessageWithContentEvent( destinations = MessageDestinations(param.arg(0)), - messageContent = MessageContent(param.arg(1)) + messageContent = MessageContent(param.arg(1)), + callback = param.arg(2) ).apply { adapter = param })?.also { if (it.canceled) { param.setResult(null) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt index 0477076f..b59c669c 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -14,10 +14,12 @@ import me.rhunk.snapenhance.ModContext import me.rhunk.snapenhance.bridge.BridgeInterface import me.rhunk.snapenhance.bridge.DownloadCallback import me.rhunk.snapenhance.bridge.SyncCallback +import me.rhunk.snapenhance.bridge.scripting.IScripting import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.core.bridge.types.BridgeFileType import me.rhunk.snapenhance.core.bridge.types.FileActionType import me.rhunk.snapenhance.core.messaging.MessagingRuleType +import me.rhunk.snapenhance.core.messaging.SocialScope import me.rhunk.snapenhance.data.LocalePair import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors @@ -118,24 +120,12 @@ class BridgeClient( fun getApplicationApkPath() = service.getApplicationApkPath() - fun getAutoUpdaterTime(): Long { - createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { - return if (isEmpty()) { - 0 - } else { - String(this).toLong() - } - } - } - - fun setAutoUpdaterTime(time: Long) { - writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) - } - fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) fun sync(callback: SyncCallback) = service.sync(callback) + fun triggerSync(scope: SocialScope, id: String) = service.triggerSync(scope.key, id) + fun passGroupsAndFriends(groups: List, friends: List) = service.passGroupsAndFriends(groups, friends) fun getRules(targetUuid: String): List { @@ -149,5 +139,5 @@ class BridgeClient( fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) = service.setRule(targetUuid, type.key, state) - fun getScriptingInterface() = service.getScriptingInterface() + fun getScriptingInterface(): IScripting = service.getScriptingInterface() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt index fc120de5..f7f88676 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/types/BridgeFileType.kt @@ -8,8 +8,7 @@ enum class BridgeFileType(val value: Int, val fileName: String, val displayName: CONFIG(0, "config.json", "Config"), MAPPINGS(1, "mappings.json", "Mappings"), MESSAGE_LOGGER_DATABASE(2, "message_logger.db", "Message Logger",true), - AUTO_UPDATER_TIMESTAMP(3, "auto_updater_timestamp.txt", "Auto Updater Timestamp"), - PINNED_CONVERSATIONS(4, "pinned_conversations.txt", "Pinned Conversations"); + PINNED_CONVERSATIONS(3, "pinned_conversations.txt", "Pinned Conversations"); fun resolve(context: Context): File = if (isDatabase) { context.getDatabasePath(fileName) diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SendMessageWithContentEvent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SendMessageWithContentEvent.kt index f94c13e3..3124fb71 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SendMessageWithContentEvent.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/eventbus/events/impl/SendMessageWithContentEvent.kt @@ -3,8 +3,21 @@ package me.rhunk.snapenhance.core.eventbus.events.impl import me.rhunk.snapenhance.core.eventbus.events.AbstractHookEvent import me.rhunk.snapenhance.data.wrapper.impl.MessageContent import me.rhunk.snapenhance.data.wrapper.impl.MessageDestinations +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker class SendMessageWithContentEvent( val destinations: MessageDestinations, - val messageContent: MessageContent -) : AbstractHookEvent() \ No newline at end of file + val messageContent: MessageContent, + private val callback: Any +) : AbstractHookEvent() { + + fun addCallbackResult(methodName: String, block: (args: Array) -> Unit) { + Hooker.ephemeralHookObjectMethod( + callback::class.java, + callback, + methodName, + HookStage.BEFORE + ) { block(it.args()) } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt index b0a544f2..49649a89 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/core/messaging/MessagingCoreObjects.kt @@ -19,7 +19,11 @@ enum class SocialScope( val tabRoute: String, ) { FRIEND("friend", "friend_info/{id}"), - GROUP("group", "group_info/{id}"), + GROUP("group", "group_info/{id}"); + + companion object { + fun getByName(name: String) = values().first { it.key == name } + } } enum class MessagingRuleType( diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ScopeSync.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ScopeSync.kt new file mode 100644 index 00000000..798d2700 --- /dev/null +++ b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ScopeSync.kt @@ -0,0 +1,42 @@ +package me.rhunk.snapenhance.features.impl + +import kotlinx.coroutines.* +import me.rhunk.snapenhance.core.eventbus.events.impl.SendMessageWithContentEvent +import me.rhunk.snapenhance.core.messaging.SocialScope +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams + +class ScopeSync : Feature("Scope Sync", loadParams = FeatureLoadParams.INIT_SYNC) { + companion object { + private const val DELAY_BEFORE_SYNC = 2000L + } + + private val updateJobs = mutableMapOf() + private val coroutineScope = CoroutineScope(Dispatchers.Main) + + private fun sync(conversationId: String) { + context.database.getDMOtherParticipant(conversationId)?.also { participant -> + context.bridgeClient.triggerSync(SocialScope.FRIEND, participant) + } ?: run { + context.bridgeClient.triggerSync(SocialScope.GROUP, conversationId) + } + } + + override fun init() { + context.event.subscribe(SendMessageWithContentEvent::class) { event -> + if (event.messageContent.contentType != ContentType.SNAP) return@subscribe + + event.addCallbackResult("onSuccess") { + event.destinations.conversations.map { it.toString() }.forEach { conversationId -> + updateJobs[conversationId]?.also { it.cancel() } + + updateJobs[conversationId] = (coroutineScope.launch { + delay(DELAY_BEFORE_SYNC) + sync(conversationId) + }) + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt index 03bbf8bf..a19db903 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt @@ -33,7 +33,7 @@ class SnapchatPlus: Feature("SnapchatPlus", loadParams = FeatureLoadParams.INIT_ it.parameterTypes[0].name != "java.lang.Boolean" }.hook(HookStage.BEFORE) { param -> val instance = param.thisObject() - val firstArg = param.args()[0] + val firstArg = param.arg(0) instance::class.java.declaredFields.filter { it.type == firstArg::class.java }.forEach { it.isAccessible = true diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt index db6d1f65..c75851a7 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt @@ -26,7 +26,7 @@ class HookAdapter( } fun argNullable(index: Int): T? { - return methodHookParam.args[index] as T? + return methodHookParam.args.getOrNull(index) as T? } fun setArg(index: Int, value: Any?) { @@ -34,7 +34,7 @@ class HookAdapter( methodHookParam.args[index] = value } - fun args(): Array { + fun args(): Array { return methodHookParam.args } @@ -66,7 +66,7 @@ class HookAdapter( invokeOriginalSafe(args(), errorCallback) } - fun invokeOriginalSafe(args: Array, errorCallback: Consumer) { + fun invokeOriginalSafe(args: Array, errorCallback: Consumer) { runCatching { setResult(XposedBridge.invokeOriginalMethod(method(), thisObject(), args)) }.onFailure { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt index ed69fb89..c6c91b87 100644 --- a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt +++ b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt @@ -6,34 +6,17 @@ import me.rhunk.snapenhance.features.Feature import me.rhunk.snapenhance.features.FeatureLoadParams import me.rhunk.snapenhance.features.impl.ConfigurationOverride import me.rhunk.snapenhance.features.impl.Messaging +import me.rhunk.snapenhance.features.impl.ScopeSync import me.rhunk.snapenhance.features.impl.downloader.MediaDownloader import me.rhunk.snapenhance.features.impl.downloader.ProfilePictureDownloader -import me.rhunk.snapenhance.features.impl.experiments.AddFriendSourceSpoof -import me.rhunk.snapenhance.features.impl.experiments.AmoledDarkMode -import me.rhunk.snapenhance.features.impl.experiments.AppPasscode -import me.rhunk.snapenhance.features.impl.experiments.DeviceSpooferHook -import me.rhunk.snapenhance.features.impl.experiments.InfiniteStoryBoost -import me.rhunk.snapenhance.features.impl.experiments.MeoPasscodeBypass -import me.rhunk.snapenhance.features.impl.experiments.NoFriendScoreDelay -import me.rhunk.snapenhance.features.impl.experiments.UnlimitedMultiSnap +import me.rhunk.snapenhance.features.impl.experiments.* import me.rhunk.snapenhance.features.impl.privacy.DisableMetrics import me.rhunk.snapenhance.features.impl.privacy.PreventMessageSending import me.rhunk.snapenhance.features.impl.spying.AnonymousStoryViewing import me.rhunk.snapenhance.features.impl.spying.MessageLogger import me.rhunk.snapenhance.features.impl.spying.PreventReadReceipts import me.rhunk.snapenhance.features.impl.spying.StealthMode -import me.rhunk.snapenhance.features.impl.tweaks.AutoSave -import me.rhunk.snapenhance.features.impl.tweaks.CameraTweaks -import me.rhunk.snapenhance.features.impl.tweaks.DisableReplayInFF -import me.rhunk.snapenhance.features.impl.tweaks.DisableVideoLengthRestriction -import me.rhunk.snapenhance.features.impl.tweaks.GooglePlayServicesDialogs -import me.rhunk.snapenhance.features.impl.tweaks.LocationSpoofer -import me.rhunk.snapenhance.features.impl.tweaks.MediaQualityLevelOverride -import me.rhunk.snapenhance.features.impl.tweaks.Notifications -import me.rhunk.snapenhance.features.impl.tweaks.OldBitmojiSelfie -import me.rhunk.snapenhance.features.impl.tweaks.SendOverride -import me.rhunk.snapenhance.features.impl.tweaks.SnapchatPlus -import me.rhunk.snapenhance.features.impl.tweaks.UnlimitedSnapViewTime +import me.rhunk.snapenhance.features.impl.tweaks.* import me.rhunk.snapenhance.features.impl.ui.ClientBootstrapOverride import me.rhunk.snapenhance.features.impl.ui.PinConversations import me.rhunk.snapenhance.features.impl.ui.UITweaks @@ -46,14 +29,16 @@ class FeatureManager(private val context: ModContext) : Manager { private val asyncLoadExecutorService = Executors.newFixedThreadPool(5) private val features = mutableListOf() - private fun register(featureClass: KClass) { - runCatching { - with(featureClass.java.newInstance()) { - context = this@FeatureManager.context - features.add(this) + private fun register(vararg featureClasses: KClass) { + featureClasses.forEach { clazz -> + runCatching { + clazz.constructors.first().call().also { feature -> + feature.context = context + features.add(feature) + } + }.onFailure { + Logger.xposedLog("Failed to register feature ${clazz.simpleName}", it) } - }.onFailure { - Logger.xposedLog("Failed to register feature ${featureClass.simpleName}", it) } } @@ -63,40 +48,43 @@ class FeatureManager(private val context: ModContext) : Manager { } override fun init() { - register(Messaging::class) - register(MediaDownloader::class) - register(StealthMode::class) - register(MenuViewInjector::class) - register(PreventReadReceipts::class) - register(AnonymousStoryViewing::class) - register(MessageLogger::class) - register(SnapchatPlus::class) - register(DisableMetrics::class) - register(PreventMessageSending::class) - register(Notifications::class) - register(AutoSave::class) - register(UITweaks::class) - register(ConfigurationOverride::class) - register(SendOverride::class) - register(UnlimitedSnapViewTime::class) - register(DisableVideoLengthRestriction::class) - register(MediaQualityLevelOverride::class) - register(MeoPasscodeBypass::class) - register(AppPasscode::class) - register(LocationSpoofer::class) - register(CameraTweaks::class) - register(InfiniteStoryBoost::class) - register(AmoledDarkMode::class) - register(PinConversations::class) - register(UnlimitedMultiSnap::class) - register(DeviceSpooferHook::class) - register(ClientBootstrapOverride::class) - register(GooglePlayServicesDialogs::class) - register(NoFriendScoreDelay::class) - register(ProfilePictureDownloader::class) - register(AddFriendSourceSpoof::class) - register(DisableReplayInFF::class) - register(OldBitmojiSelfie::class) + register( + ScopeSync::class, + Messaging::class, + MediaDownloader::class, + StealthMode::class, + MenuViewInjector::class, + PreventReadReceipts::class, + AnonymousStoryViewing::class, + MessageLogger::class, + SnapchatPlus::class, + DisableMetrics::class, + PreventMessageSending::class, + Notifications::class, + AutoSave::class, + UITweaks::class, + ConfigurationOverride::class, + SendOverride::class, + UnlimitedSnapViewTime::class, + DisableVideoLengthRestriction::class, + MediaQualityLevelOverride::class, + MeoPasscodeBypass::class, + AppPasscode::class, + LocationSpoofer::class, + CameraTweaks::class, + InfiniteStoryBoost::class, + AmoledDarkMode::class, + PinConversations::class, + UnlimitedMultiSnap::class, + DeviceSpooferHook::class, + ClientBootstrapOverride::class, + GooglePlayServicesDialogs::class, + NoFriendScoreDelay::class, + ProfilePictureDownloader::class, + AddFriendSourceSpoof::class, + DisableReplayInFF::class, + OldBitmojiSelfie::class, + ) initializeFeatures() }