feat(experimental): story logger

This commit is contained in:
rhunk
2023-12-17 02:50:01 +01:00
parent 195dd278d8
commit 614d629f07
17 changed files with 523 additions and 82 deletions

View File

@ -21,4 +21,9 @@ interface MessageLoggerInterface {
* Delete a message from the message logger database
*/
void deleteMessage(String conversationId, long id);
/**
* Add a story to the message logger database if it is not already there
*/
boolean addStory(String userId, String url, long postedAt, long createdAt, in byte[] key, in byte[] iv);
}

View File

@ -597,6 +597,10 @@
"name": "Convert Message Locally",
"description": "Converts snaps to chat external media locally. This appears in chat download context menu"
},
"story_logger": {
"name": "Story Logger",
"description": "Provides a history of friends stories"
},
"app_passcode": {
"name": "App Passcode",
"description": "Sets a passcode to lock the app"

View File

@ -4,7 +4,11 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import kotlinx.coroutines.*
import me.rhunk.snapenhance.bridge.MessageLoggerInterface
import me.rhunk.snapenhance.common.data.StoryData
import me.rhunk.snapenhance.common.util.SQLiteDatabaseHelper
import me.rhunk.snapenhance.common.util.ktx.getBlobOrNull
import me.rhunk.snapenhance.common.util.ktx.getLongOrNull
import me.rhunk.snapenhance.common.util.ktx.getStringOrNull
import java.io.File
import java.util.UUID
@ -25,6 +29,15 @@ class MessageLoggerWrapper(
"conversation_id VARCHAR",
"message_id BIGINT",
"message_data BLOB"
),
"stories" to listOf(
"id INTEGER PRIMARY KEY",
"user_id VARCHAR",
"posted_timestamp BIGINT",
"created_timestamp BIGINT",
"url VARCHAR",
"encryption_key BLOB",
"encryption_iv BLOB"
)
))
_database = openedDatabase
@ -89,9 +102,10 @@ class MessageLoggerWrapper(
return true
}
fun clearMessages() {
fun clearAll() {
coroutineScope.launch {
database.execSQL("DELETE FROM messages")
database.execSQL("DELETE FROM stories")
}
}
@ -103,9 +117,54 @@ class MessageLoggerWrapper(
return count
}
fun getStoredStoriesCount(): Int {
val cursor = database.rawQuery("SELECT COUNT(*) FROM stories", null)
cursor.moveToFirst()
val count = cursor.getInt(0)
cursor.close()
return count
}
override fun deleteMessage(conversationId: String, messageId: Long) {
coroutineScope.launch {
database.execSQL("DELETE FROM messages WHERE conversation_id = ? AND message_id = ?", arrayOf(conversationId, messageId.toString()))
}
}
override fun addStory(userId: String, url: String, postedAt: Long, createdAt: Long, key: ByteArray?, iv: ByteArray?): Boolean {
if (database.rawQuery("SELECT id FROM stories WHERE user_id = ? AND url = ?", arrayOf(userId, url)).use {
it.moveToFirst()
}) {
return false
}
runBlocking {
withContext(coroutineScope.coroutineContext) {
database.insert("stories", null, ContentValues().apply {
put("user_id", userId)
put("url", url)
put("posted_timestamp", postedAt)
put("created_timestamp", createdAt)
put("encryption_key", key)
put("encryption_iv", iv)
})
}
}
return true
}
fun getStories(userId: String, from: Long, limit: Int = Int.MAX_VALUE): Map<Long, StoryData> {
val stories = sortedMapOf<Long, StoryData>()
database.rawQuery("SELECT * FROM stories WHERE user_id = ? AND posted_timestamp < ? ORDER BY posted_timestamp DESC LIMIT $limit", arrayOf(userId, from.toString())).use {
while (it.moveToNext()) {
stories[it.getLongOrNull("posted_timestamp") ?: continue] = StoryData(
url = it.getStringOrNull("url") ?: continue,
postedAt = it.getLongOrNull("posted_timestamp") ?: continue,
createdAt = it.getLongOrNull("created_timestamp") ?: continue,
key = it.getBlobOrNull("encryption_key"),
iv = it.getBlobOrNull("encryption_iv")
)
}
}
return stories
}
}

View File

@ -11,6 +11,7 @@ class Experimental : ConfigContainer() {
val nativeHooks = container("native_hooks", NativeHooks()) { icon = "Memory"; requireRestart() }
val spoof = container("spoof", Spoof()) { icon = "Fingerprint" ; addNotices(FeatureNotice.BAN_RISK); requireRestart() }
val convertMessageLocally = boolean("convert_message_locally") { requireRestart() }
val storyLogger = boolean("story_logger") { requireRestart(); addNotices(FeatureNotice.UNSTABLE); }
val appPasscode = string("app_passcode")
val appLockOnResume = boolean("app_lock_on_resume")
val infiniteStoryBoost = boolean("infinite_story_boost")

View File

@ -71,3 +71,12 @@ data class MessagingFriendInfo(
val bitmojiId: String?,
val selfieId: String?
) : SerializableDataObject()
class StoryData(
val url: String,
val postedAt: Long,
val createdAt: Long,
val key: ByteArray?,
val iv: ByteArray?
) : SerializableDataObject()

View File

@ -1,5 +1,9 @@
package me.rhunk.snapenhance.common.data.download
import me.rhunk.snapenhance.common.config.impl.RootConfig
import java.text.SimpleDateFormat
import java.util.Locale
data class DashOptions(val offsetTime: Long, val duration: Long?)
data class InputMedia(
@ -25,4 +29,55 @@ class DownloadRequest(
val shouldMergeOverlay: Boolean
get() = flags and Flags.MERGE_OVERLAY != 0
}
fun String.sanitizeForPath(): String {
return this.replace(" ", "_")
.replace(Regex("\\p{Cntrl}"), "")
}
fun createNewFilePath(
config: RootConfig,
hexHash: String,
downloadSource: MediaDownloadSource,
mediaAuthor: String,
creationTimestamp: Long?
): String {
val pathFormat by config.downloader.pathFormat
val sanitizedMediaAuthor = mediaAuthor.sanitizeForPath().ifEmpty { hexHash }
val currentDateTime = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.ENGLISH).format(creationTimestamp ?: System.currentTimeMillis())
val finalPath = StringBuilder()
fun appendFileName(string: String) {
if (finalPath.isEmpty() || finalPath.endsWith("/")) {
finalPath.append(string)
} else {
finalPath.append("_").append(string)
}
}
if (pathFormat.contains("create_author_folder")) {
finalPath.append(sanitizedMediaAuthor).append("/")
}
if (pathFormat.contains("create_source_folder")) {
finalPath.append(downloadSource.pathName).append("/")
}
if (pathFormat.contains("append_hash")) {
appendFileName(hexHash)
}
if (pathFormat.contains("append_source")) {
appendFileName(downloadSource.pathName)
}
if (pathFormat.contains("append_username")) {
appendFileName(sanitizedMediaAuthor)
}
if (pathFormat.contains("append_date_time")) {
appendFileName(currentDateTime)
}
if (finalPath.isEmpty()) finalPath.append(hexHash)
return finalPath.toString()
}

View File

@ -12,7 +12,8 @@ enum class MediaDownloadSource(
STORY("story", "Story", "story"),
PUBLIC_STORY("public_story", "Public Story", "public_story"),
SPOTLIGHT("spotlight", "Spotlight", "spotlight"),
PROFILE_PICTURE("profile_picture", "Profile Picture", "profile_picture");
PROFILE_PICTURE("profile_picture", "Profile Picture", "profile_picture"),
STORY_LOGGER("story_logger", "Story Logger", "story_logger");
fun matches(source: String?): Boolean {
if (source == null) return false

View File

@ -22,7 +22,7 @@ object MediaDownloaderHelper {
inputStream: InputStream,
callback: (SplitMediaAssetType, InputStream) -> Unit
) {
val bufferedInputStream = BufferedInputStream(inputStream)
val bufferedInputStream = inputStream.buffered()
val fileType = getFileType(bufferedInputStream)
if (fileType != FileType.ZIP) {
@ -30,16 +30,16 @@ object MediaDownloaderHelper {
return
}
val zipInputStream = ZipInputStream(bufferedInputStream)
var entry: ZipEntry? = zipInputStream.nextEntry
while (entry != null) {
if (entry.name.startsWith("overlay")) {
callback(SplitMediaAssetType.OVERLAY, zipInputStream)
} else if (entry.name.startsWith("media")) {
callback(SplitMediaAssetType.ORIGINAL, zipInputStream)
ZipInputStream(bufferedInputStream).use { zipInputStream ->
var entry: ZipEntry? = zipInputStream.nextEntry
while (entry != null) {
if (entry.name.startsWith("overlay")) {
callback(SplitMediaAssetType.OVERLAY, zipInputStream)
} else if (entry.name.startsWith("media")) {
callback(SplitMediaAssetType.ORIGINAL, zipInputStream)
}
entry = zipInputStream.nextEntry
}
entry = zipInputStream.nextEntry
}
}
}