1
0
mirror of https://github.com/fabianonline/telegram_backup.git synced 2024-11-23 01:06:17 +00:00

Added pagination for the output.

This commit is contained in:
Fabian Schlenz 2018-01-30 18:13:05 +01:00
parent ab16c44de5
commit 0e2eeab5b9
9 changed files with 200 additions and 75 deletions

View File

@ -277,8 +277,9 @@ class CommandLineController {
println(" -t, --target <x> Target directory for the files.") println(" -t, --target <x> Target directory for the files.")
println(" -e, --export <format> Export the database. Valid formats are:") println(" -e, --export <format> Export the database. Valid formats are:")
println(" html - Creates HTML files.") println(" html - Creates HTML files.")
println(" --pagination <x> Splits the HTML export into multiple HTML pages with <x> messages per page. Default is 5000.")
println(" --no-pagination Disables pagination.")
println(" --license Displays the license of this program.") println(" --license Displays the license of this program.")
println(" -d, --daemon Keep running and automatically save new messages.")
println(" --anonymize (Try to) Remove all sensitive information from output. Useful for requesting support.") println(" --anonymize (Try to) Remove all sensitive information from output. Useful for requesting support.")
println(" --stats Print some usage statistics.") println(" --stats Print some usage statistics.")
println(" --with-channels Backup channels as well.") println(" --with-channels Backup channels as well.")

View File

@ -31,11 +31,13 @@ internal object CommandLineOptions {
var cmd_stats = false var cmd_stats = false
var cmd_channels = false var cmd_channels = false
var cmd_supergroups = false var cmd_supergroups = false
var cmd_no_pagination = false
var val_account: String? = null var val_account: String? = null
var val_limit_messages: Int? = null var val_limit_messages: Int? = null
var val_target: String? = null var val_target: String? = null
var val_export: String? = null var val_export: String? = null
var val_test: Int? = null var val_test: Int? = null
var val_pagination: Int = Config.DEFAULT_PAGINATION
@JvmStatic @JvmStatic
fun parseOptions(args: Array<String>) { fun parseOptions(args: Array<String>) {
var last_cmd: String? = null var last_cmd: String? = null
@ -47,6 +49,7 @@ internal object CommandLineOptions {
"--target" -> val_target = arg "--target" -> val_target = arg
"--export" -> val_export = arg "--export" -> val_export = arg
"--test" -> val_test = Integer.parseInt(arg) "--test" -> val_test = Integer.parseInt(arg)
"--pagination" -> val_pagination = Integer.parseInt(arg)
} }
last_cmd = null last_cmd = null
continue continue
@ -76,6 +79,11 @@ internal object CommandLineOptions {
last_cmd = "--export" last_cmd = "--export"
continue@loop continue@loop
} }
"--pagination" -> {
last_cmd = "--pagination"
continue@loop
}
"--no-pagination" -> cmd_no_pagination = true
"--license" -> cmd_license = true "--license" -> cmd_license = true
"-d", "--daemon" -> cmd_daemon = true "-d", "--daemon" -> cmd_daemon = true
"--no-media" -> cmd_no_media = true "--no-media" -> cmd_no_media = true

View File

@ -44,6 +44,8 @@ object Config {
var RENAMING_MAX_TRIES = 5 var RENAMING_MAX_TRIES = 5
var RENAMING_DELAY: Long = 1000 var RENAMING_DELAY: Long = 1000
var DEFAULT_PAGINATION = 5_000
val SECRET_GMAPS = "AIzaSyBEtUDhCQKEH6i2Mn1GAiQ9M_tLN0vxHIs" val SECRET_GMAPS = "AIzaSyBEtUDhCQKEH6i2Mn1GAiQ9M_tLN0vxHIs"
init { init {

View File

@ -595,7 +595,7 @@ class Database private constructor(var client: TelegramClient) {
fun getMessageAuthorsWithCount(c: AbstractChat): HashMap<String, Any> { fun getMessageAuthorsWithCount(c: AbstractChat): HashMap<String, Any> {
val map = HashMap<String, Any>() val map = HashMap<String, Any>()
val user_map = HashMap<User, Int>() val all_data = LinkedList<HashMap<String, String>>()
var count_others = 0 var count_others = 0
// Set a default value for 'me' to fix the charts for channels - cause I // Set a default value for 'me' to fix the charts for channels - cause I
// possibly didn't send any messages there. // possibly didn't send any messages there.
@ -607,6 +607,7 @@ class Database private constructor(var client: TelegramClient) {
"WHERE " + c.query + " GROUP BY sender_id") "WHERE " + c.query + " GROUP BY sender_id")
while (rs.next()) { while (rs.next()) {
val u: User val u: User
val data = HashMap<String, String>()
if (rs.getString(2) != null || rs.getString(3) != null || rs.getString(4) != null) { if (rs.getString(2) != null || rs.getString(3) != null || rs.getString(4) != null) {
u = User(rs.getInt(1), rs.getString(2), rs.getString(3)) u = User(rs.getInt(1), rs.getString(2), rs.getString(3))
} else { } else {
@ -615,17 +616,25 @@ class Database private constructor(var client: TelegramClient) {
if (u.isMe) { if (u.isMe) {
map.put("authors.count.me", rs.getInt(5)) map.put("authors.count.me", rs.getInt(5))
} else { } else {
user_map.put(u, rs.getInt(5))
count_others += rs.getInt(5) 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.count.others", count_others)
map.put("authors.all", user_map) map.put("authors.all", all_data)
return map return map
} catch (e: Exception) { } catch (e: Exception) {
throw RuntimeException(e) throw RuntimeException(e)
} }
}
fun getMessageCountForExport(c: AbstractChat): Int {
val rs = stmt!!.executeQuery("SELECT COUNT(*) FROM messages WHERE " + c.query);
rs.next()
return rs.getInt(1)
} }
fun getMessageTimesMatrix(c: AbstractChat): Array<IntArray> { fun getMessageTimesMatrix(c: AbstractChat): Array<IntArray> {
@ -645,10 +654,9 @@ class Database private constructor(var client: TelegramClient) {
} }
fun getMessagesForExport(c: AbstractChat): LinkedList<HashMap<String, Any>> { fun getMessagesForExport(c: AbstractChat, limit: Int=-1, offset: Int=0): LinkedList<HashMap<String, Any>> {
try { try {
var query = "SELECT messages.message_id as message_id, text, time*1000 as time, has_media, " +
val rs = stmt!!.executeQuery("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, " + "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.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 " + "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 " +
@ -656,7 +664,14 @@ class Database private constructor(var client: TelegramClient) {
"LEFT JOIN users ON users.id=messages.sender_id " + "LEFT JOIN users ON users.id=messages.sender_id " +
"LEFT JOIN users AS users_fwd ON users_fwd.id=fwd_from_id WHERE " + "LEFT JOIN users AS users_fwd ON users_fwd.id=fwd_from_id WHERE " +
c.query + " " + c.query + " " +
"ORDER BY messages.message_id") "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_time = SimpleDateFormat("HH:mm:ss")
val format_date = SimpleDateFormat("d MMM yy") val format_date = SimpleDateFormat("d MMM yy")
val meta = rs.getMetaData() val meta = rs.getMetaData()
@ -701,18 +716,25 @@ class Database private constructor(var client: TelegramClient) {
abstract inner class AbstractChat { abstract inner class AbstractChat {
abstract val query: String 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() { inner class Dialog(var id: Int, var first_name: String?, var last_name: String?, var username: String?, var count: Int?) : AbstractChat() {
override val query: String override val query: String
get() = "source_type='dialog' AND source_id=" + id get() = "source_type='dialog' AND source_id=" + id
override val type: String
get() = "dialog"
} }
inner class Chat(var id: Int, var name: String?, var count: Int?) : AbstractChat() { inner class Chat(var id: Int, var name: String?, var count: Int?) : AbstractChat() {
override val query: String override val query: String
get() = "source_type IN('group', 'supergroup', 'channel') AND source_id=" + id get() = "source_type IN('group', 'supergroup', 'channel') AND source_id=" + id
override val type: String
get() = "chat"
} }
inner class User(id: Int, first_name: String?, last_name: String?) { inner class User(id: Int, first_name: String?, last_name: String?) {
@ -731,6 +753,9 @@ class Database private constructor(var client: TelegramClient) {
inner class GlobalChat : AbstractChat() { inner class GlobalChat : AbstractChat() {
override val query: String override val query: String
get() = "1=1" get() = "1=1"
override val type: String
get() = "GlobalChat"
} }
companion object { companion object {

View File

@ -19,6 +19,8 @@ package de.fabianonline.telegram_backup.exporter
import de.fabianonline.telegram_backup.UserManager import de.fabianonline.telegram_backup.UserManager
import de.fabianonline.telegram_backup.Database import de.fabianonline.telegram_backup.Database
import de.fabianonline.telegram_backup.anonymize import de.fabianonline.telegram_backup.anonymize
import de.fabianonline.telegram_backup.toPrettyJson
import de.fabianonline.telegram_backup.CommandLineOptions
import java.io.File import java.io.File
import java.io.PrintWriter import java.io.PrintWriter
@ -41,12 +43,13 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
class HTMLExporter { class HTMLExporter {
val db = Database.getInstance()
val user = UserManager.getInstance()
@Throws(IOException::class) @Throws(IOException::class)
fun export() { fun export() {
try { try {
val user = UserManager.getInstance() val pagination = if (CommandLineOptions.cmd_no_pagination) -1 else CommandLineOptions.val_pagination
val db = Database.getInstance()
// Create base dir // Create base dir
logger.debug("Creating base dir") logger.debug("Creating base dir")
@ -95,26 +98,14 @@ class HTMLExporter {
w.close() w.close()
mustache = mf.compile("templates/html/chat.mustache") mustache = mf.compile("templates/html/chat.mustache")
val page_mustache = mf.compile("templates/html/page.mustache")
var i = 0 var i = 0
println("Generating ${dialogs.size} dialog pages...") println("Generating ${dialogs.size} dialog pages...")
for (d in dialogs) { for (d in dialogs) {
i++ i++
logger.trace("Dialog {}/{}: {}", i, dialogs.size, d.id.toString().anonymize()) logger.trace("Dialog {}/{}: {}", i, dialogs.size, d.id.toString().anonymize())
val messages = db.getMessagesForExport(d) processChat(chat=d, pagination=pagination, index_mustache=mustache, base_dir=base, page_mustache=page_mustache);
scope.clear()
scope.put("user", user)
scope.put("dialog", d)
scope.put("messages", messages)
scope.putAll(db.getMessageAuthorsWithCount(d))
scope.put("heatmap_data", intArrayToString(db.getMessageTimesMatrix(d)))
scope.putAll(db.getMessageTypesWithCount(d))
scope.putAll(db.getMessageMediaTypesWithCount(d))
w = getWriter(base + "dialogs" + File.separatorChar + "user_" + d.id + ".html")
mustache.execute(w, scope)
w.close()
print(".") print(".")
if (i % 100 == 0) { if (i % 100 == 0) {
println(" - $i/${dialogs.size}") println(" - $i/${dialogs.size}")
@ -127,20 +118,7 @@ class HTMLExporter {
for (c in chats) { for (c in chats) {
i++ i++
logger.trace("Chat {}/{}: {}", i, chats.size, c.id.toString().anonymize()) logger.trace("Chat {}/{}: {}", i, chats.size, c.id.toString().anonymize())
val messages = db.getMessagesForExport(c) processChat(chat=c, pagination=pagination, index_mustache=mustache, base_dir=base, page_mustache=page_mustache);
scope.clear()
scope.put("user", user)
scope.put("chat", c)
scope.put("messages", messages)
scope.putAll(db.getMessageAuthorsWithCount(c))
scope.put("heatmap_data", intArrayToString(db.getMessageTimesMatrix(c)))
scope.putAll(db.getMessageTypesWithCount(c))
scope.putAll(db.getMessageMediaTypesWithCount(c))
w = getWriter(base + "dialogs" + File.separatorChar + "chat_" + c.id + ".html")
mustache.execute(w, scope)
w.close()
print(".") print(".")
if (i % 100 == 0) { if (i % 100 == 0) {
println(" - $i/${chats.size}") println(" - $i/${chats.size}")
@ -164,6 +142,74 @@ class HTMLExporter {
} }
private fun processChat(chat: Database.AbstractChat, pagination: Int, index_mustache: Mustache, base_dir: String, page_mustache: Mustache) {
val scope = HashMap<String, Any>()
val count = db.getMessageCountForExport(chat)
val prefix = if (chat.type == "dialog") "user_" else "chat_"
val id = if (chat is Database.Chat) chat.id else if (chat is Database.Dialog) chat.id else throw IllegalArgumentException("Unexpected unknown id")
scope.put("user", user)
scope.put(chat.type, chat)
if (pagination>0 && count>pagination) { // pagination is enabled and we have more messages than allowed on one page
scope.put("paginated", true)
val pages_data = LinkedList<HashMap<String, String>>()
var offset = 0
var page = 1
val pages: Int = count / pagination + 1
val dir = "${base_dir}dialogs${File.separatorChar}"
val filename_base = "${prefix}${id}_p"
while (offset < count) {
val page_scope = HashMap<String, Any>()
val filename = "${filename_base}${page}.html"
page_scope.put("page", page)
page_scope.put("pages", pages)
page_scope.put(chat.type, chat)
if (page > 1) page_scope.put("previous_page", "${filename_base}${page-1}.html")
if (page < pages) page_scope.put("next_page", "${filename_base}${page+1}.html")
val messages = db.getMessagesForExport(chat, limit=pagination, offset=offset)
page_scope.put("messages", messages)
val w = getWriter(dir + filename)
page_mustache.execute(w, page_scope)
w.close()
val data = HashMap<String, String>()
data.put("page", ""+page)
data.put("filename", "${prefix}${id}_p${page}.html")
data.put("start_time", messages.getFirst().get("formatted_time") as String)
data.put("start_date", messages.getFirst().get("formatted_date") as String)
pages_data.add(data)
page += 1
offset += pagination
}
scope.put("pages_data", pages_data)
scope.put("pages", pages)
} else { // put all messages on one page
scope.put("paginated", false)
val messages = db.getMessagesForExport(chat)
scope.put("messages", messages)
}
scope.putAll(db.getMessageAuthorsWithCount(chat))
scope.put("heatmap_data", intArrayToString(db.getMessageTimesMatrix(chat)))
scope.putAll(db.getMessageTypesWithCount(chat))
scope.putAll(db.getMessageMediaTypesWithCount(chat))
val w = getWriter(base_dir + "dialogs" + File.separatorChar + prefix + id + ".html")
index_mustache.execute(w, scope)
w.close()
}
@Throws(FileNotFoundException::class) @Throws(FileNotFoundException::class)
private fun getWriter(filename: String): OutputStreamWriter { private fun getWriter(filename: String): OutputStreamWriter {
logger.trace("Creating writer for file {}", filename.anonymize()) logger.trace("Creating writer for file {}", filename.anonymize())

View File

@ -0,0 +1,17 @@
<ul class="messages">
{{#messages}}
{{#is_new_date}}
<li class="date">
{{formatted_date}}
</li>
{{/is_new_date}}
<li class="message {{#from_me}}from-me{{/from_me}} {{odd_even}} {{#same_user}}same-user{{/same_user}}" data-message-id="{{message_id}}" data-media="{{media_type}}">
<span class="time">{{formatted_time}}</span>
<span class="sender">{{user_first_name}}</span>
{{#text}}<span class="text">{{text}}</span>{{/text}}
{{#media_sticker}}<span class="sticker"><img src="../stickers/{{media_file}}" /></span>{{/media_sticker}}
{{#media_photo}}<span class="photo"><img src="../{{media_file}}" /></span>{{/media_photo}}
{{#media_document}}<span class="document"><a href="../{{media_file}}">{{media_file}}</a></span>{{/media_document}}
</li>
{{/messages}}
</ul>

View File

@ -121,7 +121,7 @@
var author_data = [ var author_data = [
{{#authors.all}} {{#authors.all}}
['{{key.name}}', {{value}}], ['{{name}}', {{count}}],
{{/authors.all}} {{/authors.all}}
]; ];
author_data.sort(function(a, b) { return b[1]-a[1]; }); author_data.sort(function(a, b) { return b[1]-a[1]; });

View File

@ -17,24 +17,17 @@
<a href="../index.html">Back to the overview</a> <a href="../index.html">Back to the overview</a>
<ul class="messages"> {{#paginated}}
{{#messages}} <h3>{{pages}} Pages</h3>
{{#is_new_date}} <ul>
<li class="date"> {{#pages_data}}
{{formatted_date}} <li><a href="{{filename}}">Page {{page}}</a> (starting {{start_date}} {{start_time}})</li>
</li> {{/pages_data}}
{{/is_new_date}}
<li class="message {{#from_me}}from-me{{/from_me}} {{odd_even}} {{#same_user}}same-user{{/same_user}}" data-message-id="{{message_id}}" data-media="{{media_type}}">
<span class="time">{{formatted_time}}</span>
<span class="sender">{{user_first_name}}</span>
{{#text}}<span class="text">{{text}}</span>{{/text}}
{{#media_sticker}}<span class="sticker"><img src="../stickers/{{media_file}}" /></span>{{/media_sticker}}
{{#media_photo}}<span class="photo"><img src="../{{media_file}}" /></span>{{/media_photo}}
{{#media_document}}<span class="document"><a href="../{{media_file}}">{{media_file}}</a></span>{{/media_document}}
</li>
{{/messages}}
</ul> </ul>
{{/paginated}}
{{^paginated}}
{{> _messages }}
{{/paginated}}
<a href="../index.html">Back to the overview</a> <a href="../index.html">Back to the overview</a>
{{> _stats }} {{> _stats }}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>Telegram Backup for {{user.getUserString}}</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="../style.css" type="text/css" />
</head>
<body>
<h1>Telegram Backup</h1>
{{#dialog}}
<h2>Dialog with {{first_name}} {{last_name}} {{#username}}(@{{username}}){{/username}}</h2>
{{/dialog}}
{{#chat}}
<h2>Chat {{name}}</h2>
{{/chat}}
<h3>Page {{page}} of {{pages}}</h3>
{{#previous_page}}
<a href="{{previous_page}}">Previous page</a>
{{/previous_page}}
{{#next_page}}
<a href="{{next_page}}">Next page</a>
{{/next_page}}
{{> _messages }}
{{#previous_page}}
<a href="{{previous_page}}">Previous page</a>
{{/previous_page}}
{{#next_page}}
<a href="{{next_page}}">Next page</a>
{{/next_page}}