telegram_backup/src/main/kotlin/de/fabianonline/telegram_backup/Database.kt

703 lines
23 KiB
Kotlin

/* Telegram_Backup
* Copyright (C) 2016 Fabian Schlenz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. */
package de.fabianonline.telegram_backup
import com.github.badoualy.telegram.tl.api.*
import com.github.badoualy.telegram.tl.core.TLVector
import com.github.badoualy.telegram.api.TelegramClient
import org.slf4j.LoggerFactory
import org.slf4j.Logger
import java.sql.Connection
import java.sql.DriverManager
import java.sql.Statement
import java.sql.SQLException
import java.sql.ResultSet
import java.sql.ResultSetMetaData
import java.sql.PreparedStatement
import java.sql.Types
import java.sql.Time
import java.io.File
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.LinkedList
import java.util.LinkedHashMap
import java.util.HashMap
import java.util.Date
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.nio.file.FileAlreadyExistsException
import java.text.SimpleDateFormat
import de.fabianonline.telegram_backup.mediafilemanager.AbstractMediaFileManager
import de.fabianonline.telegram_backup.mediafilemanager.FileManagerFactory
class Database constructor(val file_base: String, val user_manager: UserManager) {
val conn: Connection
val stmt: Statement
val logger = LoggerFactory.getLogger(Database::class.java)
init {
println("Opening database...")
try {
Class.forName("org.sqlite.JDBC")
} catch (e: ClassNotFoundException) {
throw RuntimeException("Could not load jdbc-sqlite class.")
}
val path = "jdbc:sqlite:${file_base}${Config.FILE_NAME_DB}"
try {
conn = DriverManager.getConnection(path)!!
stmt = conn.createStatement()
} catch (e: SQLException) {
throw RuntimeException("Could not connect to SQLITE database.")
}
// Run updates
val updates = DatabaseUpdates(conn, this)
updates.doUpdates()
println("Database is ready.")
}
fun getTopMessageID(): Int = queryInt("SELECT MAX(message_id) FROM messages WHERE source_type IN ('group', 'dialog')")
fun getMessageCount(): Int = queryInt("SELECT COUNT(*) FROM messages")
fun getChatCount(): Int = queryInt("SELECT COUNT(*) FROM chats")
fun getUserCount(): Int = queryInt("SELECT COUNT(*) FROM users")
fun getMissingIDs(): LinkedList<Int> {
try {
val missing = LinkedList<Int>()
val max = getTopMessageID()
val rs = stmt.executeQuery("SELECT message_id FROM messages WHERE source_type IN ('group', 'dialog') ORDER BY id")
rs.next()
var id = rs.getInt(1)
for (i in 1..max) {
if (i == id) {
rs.next()
if (rs.isClosed()) {
id = Integer.MAX_VALUE
} else {
id = rs.getInt(1)
}
} else if (i < id) {
missing.add(i)
}
}
rs.close()
return missing
} catch (e: SQLException) {
e.printStackTrace()
throw RuntimeException("Could not get list of ids.")
}
}
fun getMessagesWithMedia(): LinkedList<TLMessage?> {
try {
val list = LinkedList<TLMessage?>()
val rs = stmt.executeQuery("SELECT data FROM messages WHERE has_media=1")
while (rs.next()) {
list.add(bytesToTLMessage(rs.getBytes(1)))
}
rs.close()
return list
} catch (e: Exception) {
e.printStackTrace()
throw RuntimeException("Exception occured. See above.")
}
}
fun getMessagesFromUserCount() = queryInt("SELECT COUNT(*) FROM messages WHERE sender_id=" + user_manager.id)
fun getMessageTypesWithCount(): HashMap<String, Int> = getMessageTypesWithCount(GlobalChat())
fun getMessageMediaTypesWithCount(): HashMap<String, Int> = getMessageMediaTypesWithCount(GlobalChat())
fun getMessageSourceTypeWithCount(): HashMap<String, Int> {
val map = HashMap<String, Int>()
try {
val rs = stmt.executeQuery("SELECT COUNT(id), source_type FROM messages GROUP BY source_type ORDER BY source_type")
while (rs.next()) {
val source_type = rs.getString(2) ?: "null"
map.put("count.messages.source_type.${source_type}", rs.getInt(1))
}
rs.close()
return map
} catch (e:Exception) {
throw RuntimeException(e)
}
}
fun getMessageApiLayerWithCount(): HashMap<String, Int> {
val map = HashMap<String, Int>()
try {
val rs = stmt.executeQuery("SELECT COUNT(id), api_layer FROM messages GROUP BY api_layer ORDER BY api_layer")
while (rs.next()) {
var layer = rs.getInt(2)
map.put("count.messages.api_layer.$layer", rs.getInt(1))
}
rs.close()
return map
} catch (e: Exception) {
throw RuntimeException(e)
}
}
fun getMessageAuthorsWithCount(): HashMap<String, Any> = getMessageAuthorsWithCount(GlobalChat())
fun getMessageTimesMatrix(): Array<IntArray> = getMessageTimesMatrix(GlobalChat())
fun getEncoding(): String = queryString("PRAGMA encoding")
fun getListOfChatsForExport(): LinkedList<Chat> {
val list = LinkedList<Chat>()
val rs = stmt.executeQuery("SELECT chats.id, chats.name, COUNT(messages.id) as c " +
"FROM chats, messages WHERE messages.source_type IN('group', 'supergroup', 'channel') AND messages.source_id=chats.id " +
"GROUP BY chats.id ORDER BY c DESC")
while (rs.next()) {
list.add(Chat(rs.getInt(1), rs.getString(2), rs.getInt(3)))
}
rs.close()
return list
}
fun getListOfDialogsForExport(): LinkedList<Dialog> {
val list = LinkedList<Dialog>()
val rs = stmt.executeQuery(
"SELECT users.id, first_name, last_name, username, COUNT(messages.id) as c " +
"FROM users, messages WHERE messages.source_type='dialog' AND messages.source_id=users.id " +
"GROUP BY users.id ORDER BY c DESC")
while (rs.next()) {
list.add(Dialog(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getInt(5)))
}
rs.close()
return list
}
fun backupDatabase(currentVersion: Int) {
val filename = String.format(Config.FILE_NAME_DB_BACKUP, currentVersion)
System.out.println(" Creating a backup of your database as " + filename)
try {
val src = file_base + Config.FILE_NAME_DB
val dst = file_base + filename
logger.debug("Copying {} to {}", src.anonymize(), dst.anonymize())
Files.copy(
File(src).toPath(),
File(dst).toPath())
} catch (e: FileAlreadyExistsException) {
logger.warn("Backup already exists:", e)
} catch (e: IOException) {
e.printStackTrace()
throw RuntimeException("Could not create backup.")
}
}
fun getTopMessageIDForChannel(id: Int): Int = queryInt("SELECT MAX(message_id) FROM messages WHERE source_id=$id AND source_type IN('channel', 'supergroup')")
fun logRun(start_id: Int, end_id: Int, count: Int) {
val ps = conn.prepareStatement("INSERT INTO runs " +
"(time, start_id, end_id, count_missing) " +
"VALUES " +
"(DateTime('now'), ?, ?, ? )")
ps.setInt(1, start_id)
ps.setInt(2, end_id)
ps.setInt(3, count)
ps.execute()
ps.close()
}
fun queryInt(query: String): Int {
val rs = stmt.executeQuery(query)
rs.next()
val result = rs.getInt(1)
rs.close()
return result
}
fun queryString(query: String): String {
val rs = stmt.executeQuery(query)
rs.next()
val result = rs.getString(1)
rs.close()
return result
}
fun queryStringMap(query: String): Map<String, String> {
val map = mutableMapOf<String, String>()
val rs = stmt.executeQuery(query)
while(rs.next()) {
map.add(rs.getString(1), rs.getString(2))
}
rs.close()
return map
}
@Synchronized
fun saveMessages(all: TLVector<TLAbsMessage>, api_layer: Int, source_type: MessageSource = MessageSource.NORMAL) {
//"(id, dialog_id, from_id, from_type, text, time, has_media, data, sticker, type) " +
//"VALUES " +
//"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
val columns = "(message_id, message_type, source_type, source_id, sender_id, fwd_from_id, text, time, has_media, media_type, media_file, media_size, data, api_layer) " +
"VALUES " +
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
//1 2 3 4 5 6 7 8 9 10 11 12 13 14
val ps = conn.prepareStatement("INSERT OR REPLACE INTO messages " + columns)
val ps_insert_or_ignore = conn.prepareStatement("INSERT OR IGNORE INTO messages " + columns)
for (msg in all) {
if (msg is TLMessage) {
ps.setInt(1, msg.getId())
ps.setString(2, "message")
val peer = msg.getToId()
if (peer is TLPeerChat) {
ps.setString(3, "group")
ps.setInt(4, peer.getChatId())
} else if (peer is TLPeerUser) {
var id = peer.getUserId()
if (id == user_manager.id) {
id = msg.getFromId()
}
ps.setString(3, "dialog")
ps.setInt(4, id)
} else if (peer is TLPeerChannel) {
if (source_type == MessageSource.CHANNEL) {
ps.setString(3, "channel")
} else if (source_type == MessageSource.SUPERGROUP) {
ps.setString(3, "supergroup")
} else {
throw RuntimeException("Got a TLPeerChannel, but were expecting $source_type")
}
ps.setInt(4, peer.getChannelId())
} else {
throw RuntimeException("Unexpected Peer type: " + peer.javaClass)
}
if (peer is TLPeerChannel && msg.getFromId() == null) {
ps.setNull(5, Types.INTEGER)
} else {
ps.setInt(5, msg.getFromId())
}
if (msg.getFwdFrom() != null && msg.getFwdFrom().getFromId() != null) {
ps.setInt(6, msg.getFwdFrom().getFromId())
} else {
ps.setNull(6, Types.INTEGER)
}
var text = msg.getMessage()
if ((text == null || text.equals("")) && msg.getMedia() != null) {
val media = msg.getMedia()
if (media is TLMessageMediaDocument) {
text = media.getCaption()
} else if (media is TLMessageMediaPhoto) {
text = media.getCaption()
}
}
ps.setString(7, text)
ps.setString(8, "" + msg.getDate())
val f = FileManagerFactory.getFileManager(msg, user_manager)
if (f == null) {
ps.setNull(9, Types.BOOLEAN)
ps.setNull(10, Types.VARCHAR)
ps.setNull(11, Types.VARCHAR)
ps.setNull(12, Types.INTEGER)
} else {
ps.setBoolean(9, true)
ps.setString(10, f.name)
ps.setString(11, f.targetFilename)
ps.setInt(12, f.size)
}
val stream = ByteArrayOutputStream()
msg.serializeBody(stream)
ps.setBytes(13, stream.toByteArray())
ps.setInt(14, api_layer)
ps.addBatch()
} else if (msg is TLMessageService) {
ps_insert_or_ignore.setInt(1, msg.getId())
ps_insert_or_ignore.setString(2, "service_message")
val peer = msg.getToId()
if (peer is TLPeerChat) {
ps.setString(3, "group")
ps.setInt(4, peer.getChatId())
} else if (peer is TLPeerUser) {
var id = peer.getUserId()
if (id == user_manager.id) {
id = msg.getFromId()
}
ps.setString(3, "dialog")
ps.setInt(4, id)
} else if (peer is TLPeerChannel) {
// Messages in channels don't have a sender.
if (msg.getFromId() == null) {
ps.setString(3, "channel")
} else {
ps.setString(3, "supergroup")
}
ps.setInt(4, peer.getChannelId())
} else {
throw RuntimeException("Unexpected Peer type: " + peer.javaClass)
}
ps_insert_or_ignore.setNull(5, Types.INTEGER)
ps_insert_or_ignore.setNull(6, Types.INTEGER)
ps_insert_or_ignore.setNull(7, Types.VARCHAR)
ps_insert_or_ignore.setNull(8, Types.INTEGER)
ps_insert_or_ignore.setNull(9, Types.BOOLEAN)
ps_insert_or_ignore.setNull(10, Types.VARCHAR)
ps_insert_or_ignore.setNull(11, Types.VARCHAR)
ps_insert_or_ignore.setNull(12, Types.INTEGER)
ps_insert_or_ignore.setNull(13, Types.BLOB)
ps_insert_or_ignore.setInt(14, api_layer)
ps_insert_or_ignore.addBatch()
} else if (msg is TLMessageEmpty) {
ps_insert_or_ignore.setInt(1, msg.getId())
ps_insert_or_ignore.setString(2, "empty_message")
ps_insert_or_ignore.setNull(3, Types.INTEGER)
ps_insert_or_ignore.setNull(4, Types.INTEGER)
ps_insert_or_ignore.setNull(5, Types.INTEGER)
ps_insert_or_ignore.setNull(6, Types.INTEGER)
ps_insert_or_ignore.setNull(7, Types.VARCHAR)
ps_insert_or_ignore.setNull(8, Types.INTEGER)
ps_insert_or_ignore.setNull(9, Types.BOOLEAN)
ps_insert_or_ignore.setNull(10, Types.VARCHAR)
ps_insert_or_ignore.setNull(11, Types.VARCHAR)
ps_insert_or_ignore.setNull(12, Types.INTEGER)
ps_insert_or_ignore.setNull(13, Types.BLOB)
ps_insert_or_ignore.setInt(14, api_layer)
ps_insert_or_ignore.addBatch()
} else {
throw RuntimeException("Unexpected Message type: " + msg.javaClass)
}
}
conn.setAutoCommit(false)
ps.executeBatch()
ps.clearBatch()
ps_insert_or_ignore.executeBatch()
ps_insert_or_ignore.clearBatch()
conn.commit()
conn.setAutoCommit(true)
ps.close()
ps_insert_or_ignore.close()
}
@Synchronized
fun saveChats(all: TLVector<TLAbsChat>) {
val ps_insert_or_replace = conn.prepareStatement(
"INSERT OR REPLACE INTO chats " +
"(id, name, type) " +
"VALUES " +
"(?, ?, ?)")
val ps_insert_or_ignore = conn.prepareStatement(
"INSERT OR IGNORE INTO chats " +
"(id, name, type) " +
"VALUES " +
"(?, ?, ?)")
for (abs in all) {
ps_insert_or_replace.setInt(1, abs.getId())
ps_insert_or_ignore.setInt(1, abs.getId())
if (abs is TLChatEmpty) {
ps_insert_or_ignore.setNull(2, Types.VARCHAR)
ps_insert_or_ignore.setString(3, "empty_chat")
ps_insert_or_ignore.addBatch()
} else if (abs is TLChatForbidden) {
ps_insert_or_replace.setString(2, abs.getTitle())
ps_insert_or_replace.setString(3, "chat")
ps_insert_or_replace.addBatch()
} else if (abs is TLChannelForbidden) {
ps_insert_or_replace.setString(2, abs.getTitle())
ps_insert_or_replace.setString(3, "channel")
ps_insert_or_replace.addBatch()
} else if (abs is TLChat) {
ps_insert_or_replace.setString(2, abs.getTitle())
ps_insert_or_replace.setString(3, "chat")
ps_insert_or_replace.addBatch()
} else if (abs is TLChannel) {
ps_insert_or_replace.setString(2, abs.getTitle())
ps_insert_or_replace.setString(3, "channel")
ps_insert_or_replace.addBatch()
} else {
throw RuntimeException("Unexpected " + abs.javaClass)
}
}
conn.setAutoCommit(false)
ps_insert_or_ignore.executeBatch()
ps_insert_or_ignore.clearBatch()
ps_insert_or_replace.executeBatch()
ps_insert_or_replace.clearBatch()
conn.commit()
conn.setAutoCommit(true)
ps_insert_or_ignore.close()
ps_insert_or_replace.close()
}
@Synchronized
fun saveUsers(all: TLVector<TLAbsUser>) {
val ps_insert_or_replace = conn.prepareStatement(
"INSERT OR REPLACE INTO users " +
"(id, first_name, last_name, username, type, phone) " +
"VALUES " +
"(?, ?, ?, ?, ?, ?)")
val ps_insert_or_ignore = conn.prepareStatement(
"INSERT OR IGNORE INTO users " +
"(id, first_name, last_name, username, type, phone) " +
"VALUES " +
"(?, ?, ?, ?, ?, ?)")
for (abs in all) {
if (abs is TLUser) {
val user = abs
ps_insert_or_replace.setInt(1, user.getId())
ps_insert_or_replace.setString(2, user.getFirstName())
ps_insert_or_replace.setString(3, user.getLastName())
ps_insert_or_replace.setString(4, user.getUsername())
ps_insert_or_replace.setString(5, "user")
ps_insert_or_replace.setString(6, user.getPhone())
ps_insert_or_replace.addBatch()
} else if (abs is TLUserEmpty) {
ps_insert_or_ignore.setInt(1, abs.getId())
ps_insert_or_ignore.setNull(2, Types.VARCHAR)
ps_insert_or_ignore.setNull(3, Types.VARCHAR)
ps_insert_or_ignore.setNull(4, Types.VARCHAR)
ps_insert_or_ignore.setString(5, "empty_user")
ps_insert_or_ignore.setNull(6, Types.VARCHAR)
ps_insert_or_ignore.addBatch()
} else {
throw RuntimeException("Unexpected " + abs.javaClass)
}
}
conn.setAutoCommit(false)
ps_insert_or_ignore.executeBatch()
ps_insert_or_ignore.clearBatch()
ps_insert_or_replace.executeBatch()
ps_insert_or_replace.clearBatch()
conn.commit()
conn.setAutoCommit(true)
ps_insert_or_ignore.close()
ps_insert_or_replace.close()
}
fun fetchSettings() = queryStringMap("SELECT key, value FROM settings WHERE key='${key}'")
fun saveSetting(key: String, value: String?) {
val ps = conn.prepareStatement("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)")
ps.setString(1, key)
if (value==null) {
ps.setNull(2, Types.VARCHAR)
} else {
ps.setString(2, value)
}
ps.execute()
ps.close()
}
fun getIdsFromQuery(query: String): LinkedList<Int> {
val list = LinkedList<Int>()
val rs = stmt.executeQuery(query)
while (rs.next()) {
list.add(rs.getInt(1))
}
rs.close()
return list
}
fun getMessageTypesWithCount(c: AbstractChat): HashMap<String, Int> {
val map = HashMap<String, Int>()
val rs = stmt.executeQuery("SELECT message_type, COUNT(message_id) FROM messages WHERE " + c.query + " GROUP BY message_type")
while (rs.next()) {
map.put("count.messages.type." + rs.getString(1), rs.getInt(2))
}
rs.close()
return map
}
fun getMessageMediaTypesWithCount(c: AbstractChat): HashMap<String, Int> {
val map = HashMap<String, Int>()
var count = 0
val rs = stmt.executeQuery("SELECT media_type, COUNT(message_id) FROM messages WHERE " + c.query + " GROUP BY media_type")
while (rs.next()) {
var type = rs.getString(1) ?: "null"
if (type != "null") count += rs.getInt(2)
map.put("count.messages.media_type.${type}", rs.getInt(2))
}
map.put("count.messages.media_type.any", count)
rs.close()
return map
}
fun getMessageAuthorsWithCount(c: AbstractChat): HashMap<String, Any> {
val map = HashMap<String, Any>()
val all_data = LinkedList<HashMap<String, String>>()
var count_others = 0
// Set a default value for 'me' to fix the charts for channels - cause I
// possibly didn't send any messages there.
map.put("authors.count.me", 0)
val rs = stmt.executeQuery("SELECT users.id, users.first_name, users.last_name, users.username, COUNT(messages.id) " +
"FROM messages " +
"LEFT JOIN users ON users.id=messages.sender_id " +
"WHERE " + c.query + " GROUP BY sender_id")
while (rs.next()) {
val u: User
val data = HashMap<String, String>()
if (rs.getString(2) != null || rs.getString(3) != null || rs.getString(4) != null) {
u = User(rs.getInt(1), rs.getString(2), rs.getString(3))
} else {
u = User(rs.getInt(1), "Unknown", "")
}
if (u.isMe) {
map.put("authors.count.me", rs.getInt(5))
} else {
count_others += rs.getInt(5)
data.put("name", u.name)
data.put("count", ""+rs.getInt(5))
all_data.add(data)
}
}
map.put("authors.count.others", count_others)
map.put("authors.all", all_data)
rs.close()
return map
}
fun getMessageCountForExport(c: AbstractChat): Int = queryInt("SELECT COUNT(*) FROM messages WHERE " + c.query)
fun getMessageTimesMatrix(c: AbstractChat): Array<IntArray> {
val result = Array(7) { IntArray(24) }
val rs = stmt.executeQuery("SELECT STRFTIME('%w', time, 'unixepoch') as DAY, " +
"STRFTIME('%H', time, 'unixepoch') AS hour, " +
"COUNT(id) FROM messages WHERE " + c.query + " GROUP BY hour, day " +
"ORDER BY hour, day")
while (rs.next()) {
result[if (rs.getInt(1) == 0) 6 else rs.getInt(1) - 1][rs.getInt(2)] = rs.getInt(3)
}
rs.close()
return result
}
fun getMessagesForExport(c: AbstractChat, limit: Int=-1, offset: Int=0): LinkedList<HashMap<String, Any>> {
var query = "SELECT messages.message_id as message_id, text, time*1000 as time, has_media, " +
"media_type, media_file, media_size, users.first_name as user_first_name, users.last_name as user_last_name, " +
"users.username as user_username, users.id as user_id, " +
"users_fwd.first_name as user_fwd_first_name, users_fwd.last_name as user_fwd_last_name, users_fwd.username as user_fwd_username " +
"FROM messages " +
"LEFT JOIN users ON users.id=messages.sender_id " +
"LEFT JOIN users AS users_fwd ON users_fwd.id=fwd_from_id WHERE " +
c.query + " " +
"ORDER BY messages.message_id"
if ( limit != -1 ) {
query = query + " LIMIT ${limit} OFFSET ${offset}"
}
val rs = stmt.executeQuery(query)
val format_time = SimpleDateFormat("HH:mm:ss")
val format_date = SimpleDateFormat("d MMM yy")
val meta = rs.getMetaData()
val columns = meta.getColumnCount()
val list = LinkedList<HashMap<String, Any>>()
var count = 0
var old_date: String? = null
var old_user = 0
while (rs.next()) {
val h = HashMap<String, Any>(columns)
for (i in 1..columns) {
h.put(meta.getColumnName(i), rs.getObject(i))
}
// Additional values to make up for Mustache's inability to format dates
val d = rs.getTime("time")
val date = format_date.format(d)
h.put("formatted_time", format_time.format(d))
h.put("formatted_date", date)
if (rs.getString("media_type") != null) {
h.put("media_" + rs.getString("media_type"), true)
}
h.put("from_me", rs.getInt("user_id") == user_manager.id)
h.put("is_new_date", !date.equals(old_date))
h.put("odd_even", if (count % 2 == 0) "even" else "odd")
h.put("same_user", old_user != 0 && rs.getInt("user_id") == old_user)
old_user = rs.getInt("user_id")
old_date = date
list.add(h)
count++
}
rs.close()
return list
}
abstract inner class AbstractChat {
abstract val query: String
abstract val type: String
}
inner class Dialog(var id: Int, var first_name: String?, var last_name: String?, var username: String?, var count: Int?) : AbstractChat() {
override val query = "source_type='dialog' AND source_id=" + id
override val type = "dialog"
}
inner class Chat(var id: Int, var name: String?, var count: Int?) : AbstractChat() {
override val query = "source_type IN('group', 'supergroup', 'channel') AND source_id=" + id
override val type = "chat"
}
inner class User(id: Int, first_name: String?, last_name: String?) {
var name: String
var isMe: Boolean = false
init {
isMe = id == user_manager.id
val s = StringBuilder()
if (first_name != null) s.append(first_name + " ")
if (last_name != null) s.append(last_name)
name = s.toString().trim()
}
}
inner class GlobalChat : AbstractChat() {
override val query = "1=1"
override val type = "GlobalChat"
}
companion object {
fun bytesToTLMessage(b: ByteArray?): TLMessage? {
try {
if (b == null) return null
val stream = ByteArrayInputStream(b)
val msg = TLMessage()
msg.deserializeBody(stream, TLApiContext.getInstance())
return msg
} catch (e: IOException) {
e.printStackTrace()
throw RuntimeException("Could not deserialize message.")
}
}
}
}