package com.mattermost.helpers import android.content.Context import android.net.Uri import android.text.TextUtils import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.NoSuchKeyException import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.nozbe.watermelondb.Database import com.nozbe.watermelondb.mapCursor import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.lang.Exception import java.util.* class DatabaseHelper { var defaultDatabase: Database? = null private set val onlyServerUrl: String? get() { val query = "SELECT url FROM Servers WHERE last_active_at != 0 AND identifier != ''" val cursor = defaultDatabase!!.rawQuery(query) if (cursor.count == 1) { cursor.moveToFirst() val url = cursor.getString(0) cursor.close() return url } return null } fun init(context: Context) { if (defaultDatabase == null) { setDefaultDatabase(context) } } fun getServerUrlForIdentifier(identifier: String): String? { val args: Array = arrayOf(identifier) val query = "SELECT url FROM Servers WHERE identifier=?" val cursor = defaultDatabase!!.rawQuery(query, args) if (cursor.count == 1) { cursor.moveToFirst() val url = cursor.getString(0) cursor.close() return url } return null } fun find(db: Database, tableName: String, id: String?): ReadableMap? { val args: Array = arrayOf(id) try { db.rawQuery("select * from $tableName where id == ? limit 1", args).use { cursor -> if (cursor.count <= 0) { return null } val resultMap = Arguments.createMap() cursor.moveToFirst() resultMap.mapCursor(cursor) return resultMap } } catch (e: Exception) { return null } } fun getDatabaseForServer(context: Context?, serverUrl: String): Database? { val args: Array = arrayOf(serverUrl) val query = "SELECT db_path FROM Servers WHERE url=?" val cursor = defaultDatabase!!.rawQuery(query, args) if (cursor.count == 1) { cursor.moveToFirst() val databasePath = cursor.getString(0) cursor.close() return Database(databasePath, context!!) } return null } fun queryIds(db: Database, tableName: String, ids: Array): List { val list: MutableList = ArrayList() val args = TextUtils.join(",", Arrays.stream(ids).map { value: String? -> "?" }.toArray()) try { db.rawQuery("select distinct id from $tableName where id IN ($args)", ids as Array).use { cursor -> if (cursor.count > 0) { while (cursor.moveToNext()) { list.add(cursor.getString(cursor.getColumnIndex("id"))) } } } } catch (e: Exception) { e.printStackTrace() } return list } fun queryByColumn(db: Database, tableName: String, columnName: String, values: Array): List { val list: MutableList = ArrayList() val args = TextUtils.join(",", Arrays.stream(values).map { value: Any? -> "?" }.toArray()) try { db.rawQuery("select distinct $columnName from $tableName where $columnName IN ($args)", values).use { cursor -> if (cursor.count > 0) { while (cursor.moveToNext()) { list.add(cursor.getString(cursor.getColumnIndex(columnName))) } } } } catch (e: Exception) { e.printStackTrace() } return list } fun queryCurrentUserId(db: Database): String? { val result = find(db, "System", "currentUserId")!! return result.getString("value") } fun queryPostSinceForChannel(db: Database?, channelId: String): Double? { if (db != null) { val postsInChannelQuery = "SELECT earliest, latest FROM PostsInChannel WHERE channel_id=? ORDER BY latest DESC LIMIT 1" val cursor1 = db.rawQuery(postsInChannelQuery, arrayOf(channelId)) if (cursor1.count == 1) { cursor1.moveToFirst() val earliest = cursor1.getDouble(0) val latest = cursor1.getDouble(1) cursor1.close() val postQuery = "SELECT create_at FROM POST WHERE channel_id= ? AND delete_at=0 AND create_at BETWEEN ? AND ? ORDER BY create_at DESC" val cursor2 = db.rawQuery(postQuery, arrayOf(channelId, earliest, latest)) if (cursor2.count >= 60) { cursor2.moveToFirst() val createAt = cursor2.getDouble(0) cursor2.close() return createAt } } } return null } fun handlePosts(db: Database, postsData: ReadableMap?, channelId: String) { // Posts, PostInChannel, PostInThread, Reactions, Files, CustomEmojis, Users if (postsData != null) { val ordered = postsData.getArray("order")?.toArrayList() val posts = ReadableMapUtils.toJSONObject(postsData.getMap("posts")).toMap() val previousPostId = postsData.getString("prev_post_id") val postsInThread = hashMapOf>() var earliest = 0.0 var latest = 0.0 if (ordered != null && posts.isNotEmpty()) { val firstId = ordered.first() val lastId = ordered.last() var prevPostId = "" val sortedPosts = posts.toList().sortedBy { (_, value) -> ((value as Map<*, *>).get("create_at") as Double) } sortedPosts.forEachIndexed { index, it -> val key = it.first if (it.second != null) { val post = (it.second as MutableMap) if (index == 0) { post.putIfAbsent("prev_post_id", previousPostId) } else if (!prevPostId.isNullOrEmpty()) { post.putIfAbsent("prev_post_id", prevPostId) } if (lastId == key) { earliest = post.get("create_at") as Double } if (firstId == key) { latest = post.get("create_at") as Double } val jsonPost = JSONObject(post) val rootId = post.get("root_id") as? String if (!rootId.isNullOrEmpty()) { var thread = postsInThread.get(rootId)?.toMutableList() if (thread == null) { thread = mutableListOf() } thread.add(jsonPost) postsInThread.put(rootId, thread.toList()) } if (find(db, "Post", key) == null) { insertPost(db, jsonPost) } else { updatePost(db, jsonPost) } if (ordered.contains(key)) { prevPostId = key } } } } handlePostsInChannel(db, channelId, earliest, latest) handlePostsInThread(db, postsInThread) } } fun handleUsers(db: Database, users: ReadableArray) { for (i in 0 until users.size()) { val user = users.getMap(i) val roles = user.getString("roles") ?: "" val isBot = try { user.getBoolean("is_bot") } catch (e: NoSuchKeyException) { false } db.execute( "insert into User (id, auth_service, update_at, delete_at, email, first_name, is_bot, is_guest, " + "last_name, last_picture_update, locale, nickname, position, roles, status, username, notify_props, " + "props, timezone, _status) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')", arrayOf( user.getString("id"), user.getString("auth_service"), user.getDouble("update_at"), user.getDouble("delete_at"), user.getString("email"), user.getString("first_name"), isBot, roles.contains("system_guest"), user.getString("last_name"), user.getDouble("last_picture_update"), user.getString("locale"), user.getString("nickname"), user.getString("position"), roles, "", user.getString("username"), "{}", ReadableMapUtils.toJSONObject(user.getMap("props") ?: Arguments.createMap()).toString(), ReadableMapUtils.toJSONObject(user.getMap("timezone") ?: Arguments.createMap()).toString(), ) ) } } private fun setDefaultDatabase(context: Context) { val databaseName = "app.db" val databasePath = Uri.fromFile(context.filesDir).toString() + "/" + databaseName defaultDatabase = Database(databasePath, context) } private fun insertPost(db: Database, post: JSONObject) { var metadata: JSONObject? = null var reactions: JSONArray? = null var customEmojis: JSONArray? = null var files: JSONArray? = null try { metadata = post.getJSONObject("metadata") reactions = metadata.remove("reactions") as JSONArray? customEmojis = metadata.remove("emojis") as JSONArray? files = metadata.remove("files") as JSONArray? } catch (e: Exception) { // no metadata found metadata = JSONObject() } db.execute( "insert into Post " + "(id, channel_id, create_at, delete_at, update_at, edit_at, is_pinned, message, metadata, original_id, pending_post_id, " + "previous_post_id, root_id, type, user_id, props, _status)" + " values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'created')", arrayOf( post.getString("id"), post.getString("channel_id"), post.getDouble("create_at"), post.getDouble("delete_at"), post.getDouble("update_at"), post.getDouble("edit_at"), post.getBoolean("is_pinned"), post.getString("message"), metadata.toString(), post.getString("original_id"), post.getString("pending_post_id"), post.getString("prev_post_id"), post.getString("root_id"), post.getString("type"), post.getString("user_id"), post.getJSONObject("props").toString() ) ) if (reactions != null && reactions.length() > 0) { insertReactions(db, reactions) } if (customEmojis != null && customEmojis.length() > 0) { insertCustomEmojis(db, customEmojis) } if (files != null && files.length() > 0) { insertFiles(db, files) } } private fun updatePost(db: Database, post: JSONObject) { var metadata: JSONObject? = null var reactions: JSONArray? = null var customEmojis: JSONArray? = null try { metadata = post.getJSONObject("metadata") reactions = metadata.remove("reactions") as JSONArray? customEmojis = metadata.remove("emojis") as JSONArray? metadata.remove("files") } catch (e: Exception) { // no metadata found metadata = JSONObject() } db.execute( "update Post SET channel_id = ?, create_at = ?, delete_at = ?, update_at =?, edit_at =?, " + "is_pinned = ?, message = ?, metadata = ?, original_id = ?, pending_post_id = ?, previous_post_id = ?, " + "root_id = ?, type = ?, user_id = ?, props = ?, _status = 'updated' " + "where id = ?", arrayOf( post.getString("channel_id"), post.getDouble("create_at"), post.getDouble("delete_at"), post.getDouble("update_at"), post.getDouble("edit_at"), post.getBoolean("is_pinned"), post.getString("message"), metadata.toString(), post.getString("original_id"), post.getString("pending_post_id"), post.getString("prev_post_id"), post.getString("root_id"), post.getString("type"), post.getString("user_id"), post.getJSONObject("props").toString(), post.getString("id"), ) ) if (reactions != null && reactions.length() > 0) { db.execute("delete from Reaction where post_id = ?", arrayOf(post.getString("id"))) insertReactions(db, reactions) } if (customEmojis != null && customEmojis.length() > 0) { insertCustomEmojis(db, customEmojis) } } private fun insertCustomEmojis(db: Database, customEmojis: JSONArray) { for (i in 0 until customEmojis.length()) { val emoji = customEmojis.getJSONObject(i) if(find(db, "CustomEmoji", emoji.getString("id")) == null) { db.execute( "insert into CustomEmoji (id, name, _status) values (?, ?, 'created')", arrayOf( emoji.getString("id"), emoji.getString("name"), ) ) } } } private fun insertFiles(db: Database, files: JSONArray) { for (i in 0 until files.length()) { val file = files.getJSONObject(i) val miniPreview = try { file.getString("mini_preview") } catch (e: JSONException) { "" }; val height = try { file.getInt("height") } catch (e: JSONException) { 0 }; val width = try { file.getInt("width") } catch (e: JSONException) { 0 }; db.execute( "insert into File (id, extension, height, image_thumbnail, local_path, mime_type, name, post_id, size, width, _status) " + "values (?, ?, ?, ?, '', ?, ?, ?, ?, ?, 'created')", arrayOf( file.getString("id"), file.getString("extension"), height, miniPreview, file.getString("mime_type"), file.getString("name"), file.getString("post_id"), file.getDouble("size"), width ) ) } } private fun insertReactions(db: Database, reactions: JSONArray) { for (i in 0 until reactions.length()) { val reaction = reactions.getJSONObject(i) val id = RandomId.generate() db.execute( "insert into Reaction (id, create_at, emoji_name, post_id, user_id, _status) " + "values (?, ?, ?, ?, ?, 'created')", arrayOf( id, reaction.getDouble("create_at"), reaction.getString("emoji_name"), reaction.getString("post_id"), reaction.getString("user_id") ) ) } } private fun handlePostsInChannel(db: Database, channelId: String, earliest: Double, latest: Double) { db.rawQuery("select id, channel_id, earliest, latest from PostsInChannel where channel_id = ?", arrayOf(channelId)).use { cursor -> if (cursor.count == 0) { // create new post in channel insertPostInChannel(db, channelId, earliest, latest) return } val resultArray = Arguments.createArray() while (cursor.moveToNext()) { val cursorMap = Arguments.createMap() cursorMap.mapCursor(cursor) resultArray.pushMap(cursorMap) } val chunk = findPostInChannel(resultArray, earliest, latest) if (chunk != null) { db.execute( "update PostsInChannel set earliest = ?, latest = ?, _status = 'updated' where id = ?", arrayOf( minOf(earliest, chunk.getDouble("earliest")), maxOf(latest, chunk.getDouble("latest")), chunk.getString("id") ) ) return } val newChunk = insertPostInChannel(db, channelId, earliest, latest) mergePostsInChannel(db, resultArray, newChunk) } } private fun findPostInChannel(chunks: ReadableArray, earliest: Double, latest: Double): ReadableMap? { for (i in 0 until chunks.size()) { val chunk = chunks.getMap(i) if (earliest >= chunk.getDouble("earliest") || latest <= chunk.getDouble("latest")) { return chunk; } } return null; } private fun insertPostInChannel(db: Database, channelId: String, earliest: Double, latest: Double): ReadableMap { val id = RandomId.generate() db.execute("insert into PostsInChannel (id, channel_id, earliest, latest, _status) values (?, ?, ?, ?, 'created')", arrayOf(id, channelId, earliest, latest)) val map = Arguments.createMap() map.putString("id", id) map.putString("channel_id", channelId) map.putDouble("earliest", earliest) map.putDouble("latest", latest) return map } private fun mergePostsInChannel(db: Database, existingChunks: ReadableArray, newChunk: ReadableMap) { for (i in 0 until existingChunks.size()) { val chunk = existingChunks.getMap(i) if (newChunk.getDouble("earliest") <= chunk.getDouble("earliest") && newChunk.getDouble("latest") >= chunk.getDouble("latest")) { db.execute("delete from PostsInChannel where id = ?", arrayOf(chunk.getString("id"))) break } } } private fun handlePostsInThread(db: Database, postsInThread: Map>) { postsInThread.forEach { (key, list) -> val sorted = list.sortedBy { it.getDouble("create_at") } val earliest = sorted.first().getDouble("create_at") val latest = sorted.last().getDouble("create_at") db.rawQuery("select * from PostsInThread where root_id = ? order by latest desc", arrayOf(key)).use { cursor -> if (cursor.count > 0) { cursor.moveToFirst() val cursorMap = Arguments.createMap() cursorMap.mapCursor(cursor) db.execute( "update PostsInThread set earliest = ?, latest = ?, _status = 'updated' where id = ?", arrayOf( minOf(earliest, cursorMap.getDouble("earliest")), maxOf(latest, cursorMap.getDouble("latest")), key ) ) return } val id = RandomId.generate() db.execute( "insert into PostsInThread (id, root_id, earliest, latest, _status) " + "values (?, ?, ?, ?, 'created')", arrayOf(id, key, earliest, latest) ) } } } private fun JSONObject.toMap(): Map = keys().asSequence().associateWith { when (val value = this[it]) { is JSONArray -> { val map = (0 until value.length()).associate { Pair(it.toString(), value[it]) } JSONObject(map).toMap().values.toList() } is JSONObject -> value.toMap() JSONObject.NULL -> null else -> value } } companion object { var instance: DatabaseHelper? = null get() { if (field == null) { field = DatabaseHelper() } return field } private set } }