commit 95eb4e89f634707ca668740a60940d2898ade9ab
parent 343159ee5b934da8865e2c0db18e2693f5c824ad
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri, 19 Apr 2024 23:05:28 +0200

feat: friend mutation observer
- bitmoji, birthday changes

Diffstat:
Mcommon/src/main/assets/lang/en_US.json | 25+++++++++++++++++++------
Mcommon/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt | 9++++++++-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 106 insertions(+), 28 deletions(-)

diff --git a/common/src/main/assets/lang/en_US.json b/common/src/main/assets/lang/en_US.json @@ -538,9 +538,9 @@ "name": "Instant Delete", "description": "Removes the confirmation dialog when deleting messages" }, - "relationship_notifier": { - "name": "Relationship Notifier", - "description": "Notifies you when someone removes you as a friend" + "friend_mutation_notifier": { + "name": "Friend Mutation Notifier", + "description": "Notifies you when something changes in a friend's profile" }, "better_notifications": { "name": "Better Notifications", @@ -1114,6 +1114,14 @@ "auto_mark_as_read": { "conversation_read": "Mark conversation as read when sending a message", "snap_reply": "Mark snaps as read when replying to them" + }, + "friend_mutation_observer": { + "remove_friend": "Notify when someone removes you as a friend", + "birthday_changes": "Notify when someone changes their birthday", + "bitmoji_selfie_changes": "Notify when someone changes their Bitmoji selfie", + "bitmoji_avatar_changes": "Notify when someone changes their Bitmoji avatar", + "bitmoji_background_changes": "Notify when someone changes their Bitmoji background", + "bitmoji_scene_changes": "Notify when someone changes their Bitmoji scene" } } }, @@ -1382,9 +1390,14 @@ "friend_mutation_observer": { "notification_channel_name": "Friend Mutation Observer", - "removed_friend_notification_title": "Friend Removed", - "removed_friend_notification_content": "{username} has removed you as a friend", - "removed_friend_notification_content_with_display_name": "{displayName} ({username}) has removed you as a friend" + "friend_removed": "{username} has removed you as a friend", + "birthday_removed": "{username} has removed their birthday ({birthday})", + "birthday_added": "{username} has added their birthday ({birthday})", + "birthday_changed": "{username} has changed their birthday from {oldBirthday} to {newBirthday}", + "bitmoji_selfie_changed": "{username} has changed their Bitmoji selfie", + "bitmoji_avatar_changed": "{username} has changed their Bitmoji avatar", + "bitmoji_background_changed": "{username} has changed their Bitmoji background", + "bitmoji_scene_changed": "{username} has changed their Bitmoji scene" }, "suspend_location_updates": { diff --git a/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt b/common/src/main/kotlin/me/rhunk/snapenhance/common/config/impl/MessagingTweaks.kt @@ -73,7 +73,14 @@ class MessagingTweaks : ConfigContainer() { nativeHooks() } val instantDelete = boolean("instant_delete") { requireRestart() } - val relationshipNotifier = boolean("relationship_notifier") { requireRestart() } + val friendMutationObserver = multiple("friend_mutation_observer", + "remove_friend", + "birthday_changes", + "bitmoji_selfie_changes", + "bitmoji_avatar_changes", + "bitmoji_background_changes", + "bitmoji_scene_changes", + ) { requireRestart() } val betterNotifications = multiple("better_notifications", "chat_preview", "media_preview", diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/FriendMutationObserver.kt @@ -7,11 +7,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.WarningAmber import com.google.gson.JsonObject import me.rhunk.snapenhance.common.data.FriendLinkType +import me.rhunk.snapenhance.common.database.impl.FriendInfo import me.rhunk.snapenhance.core.event.events.impl.NetworkApiRequestEvent import me.rhunk.snapenhance.core.features.Feature import me.rhunk.snapenhance.core.features.FeatureLoadParams import me.rhunk.snapenhance.core.util.EvictingMap import java.io.InputStreamReader +import java.util.Calendar class FriendMutationObserver: Feature("FriendMutationObserver", loadParams = FeatureLoadParams.INIT_SYNC) { private val translation by lazy { context.translation.getCategory("friend_mutation_observer") } @@ -34,29 +36,45 @@ class FriendMutationObserver: Feature("FriendMutationObserver", loadParams = Fea return addSourceCache[userId] } - private fun sendFriendRemoveNotification(displayName: String?, username: String) { - val contentText = (if (displayName != null) - translation.format("removed_friend_notification_content_with_display_name", "displayName" to displayName, "username" to username) - else translation.format("removed_friend_notification_content", "username" to username)) - + private fun sendWarnNotification( + contentText: String + ) { notificationManager.notify(System.nanoTime().toInt(), Notification.Builder(context.androidContext, channelId) - .setSmallIcon(android.R.drawable.ic_dialog_alert) - .setContentTitle(translation["removed_friend_notification_title"]) - .setContentText(contentText) - .setShowWhen(true) - .setWhen(System.currentTimeMillis()) - .build() + .setSmallIcon(android.R.drawable.ic_dialog_alert) + .setContentTitle(translation["notification_channel_name"]) + .setContentText(contentText) + .setShowWhen(true) + .setWhen(System.currentTimeMillis()) + .build() ) context.inAppOverlay.showStatusToast( Icons.Default.WarningAmber, contentText, - durationMs = 6000 + durationMs = 7000 ) } + private fun formatUsername(friendInfo: FriendInfo): String { + return friendInfo.displayName?.takeIf { it.isNotBlank() }?.let { + "$it (${friendInfo.mutableUsername})" + } ?: friendInfo.mutableUsername ?: "" + } + + private fun prettyPrintBirthday(month: Int, day: Int): String { + val calendar = Calendar.getInstance() + calendar[Calendar.MONTH] = month + return calendar.getDisplayName( + Calendar.MONTH, + Calendar.LONG, + context.translation.loadedLocale + )?.toString() + " " + day + } + override fun init() { + val config by context.config.messaging.friendMutationObserver + context.event.subscribe(NetworkApiRequestEvent::class) { event -> if (!event.url.contains("ami/friends")) return@subscribe event.onSuccess { buffer -> @@ -74,20 +92,60 @@ class FriendMutationObserver: Feature("FriendMutationObserver", loadParams = Fea } } - if (context.config.messaging.relationshipNotifier.get()) { - jsonObject.getAsJsonArray("friends").map { it.asJsonObject }.forEach { friend -> + if (config.isEmpty()) return@runCatching + + jsonObject.getAsJsonArray("friends").map { it.asJsonObject }.forEach { friend -> + runCatching { val userId = friend.get("user_id")?.asString if (userId == context.database.myUserId) return@forEach - val direction = friend.get("direction")?.asString - if (direction != "OUTGOING") return@forEach - val databaseFriend = context.database.getFriendInfo(userId ?: return@forEach) ?: return@forEach - val mutableUsername = friend.get("mutable_username").asString - val databaseLinkType = FriendLinkType.fromValue(databaseFriend.friendLinkType) + if (FriendLinkType.fromValue(databaseFriend.friendLinkType) != FriendLinkType.MUTUAL) return@forEach + + if (config.contains("remove_friend") && friend.get("direction")?.asString == "OUTGOING" && !friend.has("fidelius_info")) { + sendWarnNotification(translation.format("friend_removed", "username" to formatUsername(databaseFriend))) + return@forEach + } + + if (config.contains("birthday_changes") && + databaseFriend.birthday.takeIf { it != 0L }?.let { + ((it shr 32).toInt()).toString().padStart(2, '0') + "-" + (it.toInt()).toString().padStart(2, '0') + } != friend.get("birthday")?.asString + ) { + val oldBirthday = databaseFriend.birthday.takeIf { it != 0L }?.let { + prettyPrintBirthday((it shr 32).toInt(), it.toInt()) + } + + if (!friend.has("birthday")) { + sendWarnNotification(translation.format("birthday_removed", "username" to formatUsername(databaseFriend), "birthday" to oldBirthday.orEmpty())) + } else { + val newBirthday = friend.get("birthday")?.asString?.split("-")?.let { + prettyPrintBirthday(it[0].toInt(), it[1].toInt()) + } + if (oldBirthday == null) { + sendWarnNotification(translation.format("birthday_added", "username" to formatUsername(databaseFriend), "birthday" to newBirthday.orEmpty())) + } else { + sendWarnNotification(translation.format("birthday_changed", "username" to formatUsername(databaseFriend), "oldBirthday" to oldBirthday, "newBirthday" to newBirthday.orEmpty())) + } + } + } + + if (config.contains("bitmoji_avatar_changes") && databaseFriend.bitmojiSelfieId != friend.get("bitmoji_avatar_id")?.asString) { + sendWarnNotification(translation.format("bitmoji_avatar_changed", "username" to formatUsername(databaseFriend))) + } + + if (config.contains("bitmoji_selfie_changes") && databaseFriend.bitmojiAvatarId != friend.get("bitmoji_selfie_id")?.asString) { + sendWarnNotification(translation.format("bitmoji_selfie_changed", "username" to formatUsername(databaseFriend))) + } + + if (config.contains("bitmoji_background_changes") && databaseFriend.bitmojiBackgroundId != friend.get("bitmoji_background_id")?.asString) { + sendWarnNotification(translation.format("bitmoji_background_changed", "username" to formatUsername(databaseFriend))) + } - if (databaseLinkType == FriendLinkType.MUTUAL && !friend.has("fidelius_info")) { - sendFriendRemoveNotification(databaseFriend.displayName, mutableUsername) + if (config.contains("bitmoji_scene_changes") && databaseFriend.bitmojiSceneId != friend.get("bitmoji_scene_id")?.asString) { + sendWarnNotification(translation.format("bitmoji_scene_changed", "username" to formatUsername(databaseFriend))) } + }.onFailure { + context.log.error("Failed to process friend", it) } } }.onFailure {