mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 05:07:46 +02:00
feat(experimental): story logger
This commit is contained in:
@ -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);
|
||||
}
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -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()
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user